From c24b978c51a7b6e041b614ccfe2aae197a440c92 Mon Sep 17 00:00:00 2001 From: vlofgren Date: Thu, 19 May 2022 17:45:26 +0200 Subject: [PATCH] first commit --- .gitignore | 3 + README.md | 9 + build.gradle | 74 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59821 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 234 + gradlew.bat | 89 + marginalia_nu/build.gradle | 133 + marginalia_nu/lombok.config | 2 + .../java/bs_vs_ls/BinSearchVsLinSearch.java | 37 + .../java/bs_vs_ls/BinSearchVsLinSearch2.java | 68 + .../java/nu/marginalia/gemini/BadBotList.java | 43 + .../gemini/GeminiConfigurationModule.java | 21 + .../nu/marginalia/gemini/GeminiService.java | 164 + .../gemini/client/GeminiClient.java | 130 + .../nu/marginalia/gemini/gmi/Gemtext.java | 53 + .../gemini/gmi/GemtextDatabase.java | 72 + .../gemini/gmi/GemtextDocument.java | 163 + .../gemini/gmi/line/AbstractGemtextLine.java | 18 + .../gemini/gmi/line/GemtextAside.java | 21 + .../gemini/gmi/line/GemtextHeading.java | 32 + .../gemini/gmi/line/GemtextLineVisitor.java | 18 + .../gmi/line/GemtextLineVisitorAdapter.java | 53 + .../gemini/gmi/line/GemtextLink.java | 33 + .../gemini/gmi/line/GemtextList.java | 23 + .../gemini/gmi/line/GemtextPragma.java | 21 + .../gemini/gmi/line/GemtextPreformat.java | 23 + .../gemini/gmi/line/GemtextQuote.java | 23 + .../gemini/gmi/line/GemtextTask.java | 42 + .../gemini/gmi/line/GemtextText.java | 21 + .../gemini/gmi/line/GemtextTextLiteral.java | 23 + .../gemini/gmi/parser/GemtextAsideParser.java | 20 + .../gmi/parser/GemtextHeadingParser.java | 26 + .../gemini/gmi/parser/GemtextLinkParser.java | 42 + .../gemini/gmi/parser/GemtextListParser.java | 17 + .../gemini/gmi/parser/GemtextParser.java | 135 + .../gmi/parser/GemtextPragmaParser.java | 26 + .../gemini/gmi/parser/GemtextQuoteParser.java | 17 + .../gemini/gmi/parser/GemtextTaskParser.java | 31 + .../gemini/gmi/renderer/GemtextRenderer.java | 91 + .../gmi/renderer/GemtextRendererFactory.java | 227 + .../gemini/io/GeminiConnection.java | 185 + .../marginalia/gemini/io/GeminiSSLSetUp.java | 49 + .../gemini/io/GeminiStatusCode.java | 11 + .../gemini/io/GeminiUserException.java | 8 + .../gemini/plugins/BareStaticPagePlugin.java | 53 + .../marginalia/gemini/plugins/FileType.java | 58 + .../nu/marginalia/gemini/plugins/Plugin.java | 19 + .../gemini/plugins/SearchPlugin.java | 78 + .../java/nu/marginalia/util/ByteFolder.java | 80 + .../java/nu/marginalia/util/FileSizeUtil.java | 18 + .../java/nu/marginalia/util/ParallelPipe.java | 101 + .../java/nu/marginalia/util/PrimeUtil.java | 41 + .../nu/marginalia/util/RandomWriteFunnel.java | 139 + .../nu/marginalia/util/SeekDictionary.java | 73 + .../nu/marginalia/util/btree/BTreeReader.java | 104 + .../nu/marginalia/util/btree/BTreeWriter.java | 110 + .../marginalia/util/btree/WriteCallback.java | 7 + .../util/btree/model/BTreeContext.java | 56 + .../util/btree/model/BTreeHeader.java | 46 + .../marginalia/util/dict/DictionaryData.java | 186 + .../util/dict/DictionaryHashMap.java | 208 + .../dithering/FloydSteinbergDither.java | 243 + .../util/graphics/dithering/Palettes.java | 29 + .../marginalia/util/hash/LongPairHashMap.java | 183 + .../util/multimap/MultimapFileLong.java | 366 + .../util/multimap/MultimapSearcher.java | 128 + .../util/multimap/MultimapSorter.java | 89 + .../wmsa/auth/AuthConfigurationModule.java | 12 + .../nu/marginalia/wmsa/auth/AuthMain.java | 28 + .../nu/marginalia/wmsa/auth/AuthService.java | 105 + .../nu/marginalia/wmsa/auth/api/ApiMain.java | 32 + .../marginalia/wmsa/auth/api/ApiService.java | 127 + .../wmsa/auth/api/model/ApiLicense.java | 19 + .../wmsa/auth/api/model/ApiSearchResult.java | 20 + .../wmsa/auth/api/model/ApiSearchResults.java | 17 + .../wmsa/auth/client/AuthClient.java | 42 + .../wmsa/auth/model/LoginFormModel.java | 10 + .../wmsa/client/AbortingScheduler.java | 63 + .../wmsa/client/AbstractClient.java | 501 + .../wmsa/client/AbstractDynamicClient.java | 52 + .../wmsa/client/HttpStatusCode.java | 19 + .../wmsa/client/exception/LocalException.java | 15 + .../client/exception/MessagingException.java | 20 + .../client/exception/NetworkException.java | 15 + .../client/exception/RemoteException.java | 16 + .../RouteNotConfiguredException.java | 15 + .../client/exception/TimeoutException.java | 15 + .../wmsa/configuration/MainClass.java | 56 + .../wmsa/configuration/ServiceDescriptor.java | 101 + .../wmsa/configuration/WmsaHome.java | 16 + .../wmsa/configuration/command/Command.java | 31 + .../configuration/command/ListCommand.java | 23 + .../configuration/command/StartCommand.java | 24 + .../configuration/command/VersionCommand.java | 20 + .../module/ConfigurationModule.java | 45 + .../configuration/module/DatabaseModule.java | 114 + .../module/HostnameProvider.java | 36 + .../module/LoggerConfiguration.java | 13 + .../module/MetricsPortProvider.java | 21 + .../configuration/module/PortProvider.java | 46 + .../wmsa/configuration/server/Context.java | 139 + .../configuration/server/Initialization.java | 47 + .../configuration/server/MetricsServer.java | 25 + .../configuration/server/RateLimiter.java | 71 + .../wmsa/configuration/server/Service.java | 146 + .../wmsa/data_store/DataStoreMain.java | 35 + .../wmsa/data_store/DataStoreModule.java | 14 + .../wmsa/data_store/DataStoreService.java | 152 + .../wmsa/data_store/EdgeDataStoreService.java | 201 + .../wmsa/data_store/FileRepository.java | 138 + .../data_store/client/DataStoreClient.java | 106 + .../data_store/meta/DomainInformation.java | 26 + .../wmsa/edge/archive/EdgeArchiveMain.java | 33 + .../wmsa/edge/archive/EdgeArchiveModule.java | 15 + .../wmsa/edge/archive/EdgeArchiveService.java | 182 + .../archive/archiver/ArchiveExtractor.java | 65 + .../edge/archive/archiver/ArchivedFile.java | 7 + .../wmsa/edge/archive/archiver/Archiver.java | 116 + .../edge/archive/client/ArchiveClient.java | 56 + .../request/EdgeArchiveSubmissionReq.java | 13 + .../edge/assistant/EdgeAssistantMain.java | 33 + .../edge/assistant/EdgeAssistantModule.java | 22 + .../edge/assistant/EdgeAssistantService.java | 192 + .../assistant/client/AssistantClient.java | 42 + .../edge/assistant/dict/DictionaryEntry.java | 14 + .../assistant/dict/DictionaryResponse.java | 14 + .../assistant/dict/DictionaryService.java | 185 + .../wmsa/edge/assistant/dict/NGramDict.java | 140 + .../edge/assistant/dict/SpellChecker.java | 22 + .../wmsa/edge/assistant/dict/SymSpell.java | 442 + .../edge/assistant/dict/WikiArticles.java | 23 + .../wmsa/edge/assistant/dict/WikiCleaner.java | 393 + .../edge/assistant/dict/WikiSearchResult.java | 55 + .../wmsa/edge/assistant/eval/MathParser.java | 394 + .../wmsa/edge/assistant/eval/Unit.java | 15 + .../wmsa/edge/assistant/eval/Units.java | 122 + .../screenshot/ScreenshotService.java | 132 + .../edge/assistant/suggest/Suggestions.java | 145 + .../converting/ConvertedDomainReader.java | 62 + .../wmsa/edge/converting/ConverterMain.java | 137 + .../wmsa/edge/converting/ConverterModule.java | 60 + .../converting/CrawledInstructionWriter.java | 64 + .../wmsa/edge/converting/LoaderMain.java | 140 + .../wmsa/edge/converting/TaskStats.java | 37 + .../converting/interpreter/Instruction.java | 8 + .../interpreter/InstructionTag.java | 23 + .../converting/interpreter/Interpreter.java | 28 + .../instruction/DocumentKeywords.java | 17 + .../interpreter/instruction/DomainLink.java | 6 + .../interpreter/instruction/LoadDomain.java | 31 + .../instruction/LoadDomainLink.java | 31 + .../instruction/LoadDomainRedirect.java | 31 + .../interpreter/instruction/LoadKeywords.java | 32 + .../instruction/LoadProcessedDocument.java | 35 + .../LoadProcessedDocumentWithError.java | 28 + .../instruction/LoadProcessedDomain.java | 26 + .../interpreter/instruction/LoadRssFeed.java | 32 + .../interpreter/instruction/LoadUrl.java | 31 + .../converting/loader/IndexLoadKeywords.java | 64 + .../wmsa/edge/converting/loader/Loader.java | 115 + .../edge/converting/loader/LoaderData.java | 43 + .../edge/converting/loader/LoaderFactory.java | 32 + .../converting/loader/SqlLoadDomainLinks.java | 69 + .../converting/loader/SqlLoadDomains.java | 124 + .../loader/SqlLoadProcessedDocument.java | 126 + .../loader/SqlLoadProcessedDomain.java | 87 + .../edge/converting/loader/SqlLoadUrls.java | 92 + .../model/DisqualifiedException.java | 17 + .../converting/model/ProcessedDocument.java | 25 + .../model/ProcessedDocumentDetails.java | 26 + .../converting/model/ProcessedDomain.java | 34 + .../processor/DocumentProcessor.java | 245 + .../converting/processor/DomainProcessor.java | 59 + .../processor/InstructionsCompiler.java | 116 + .../processor/logic/DocumentValuator.java | 70 + .../processor/logic/FeatureExtractor.java | 70 + .../processor/logic/LinkProcessor.java | 90 + .../processor/logic/SummaryExtractor.java | 62 + .../processor/logic/TitleExtractor.java | 57 + .../crawler/CrawlJobsSpecificationSet.java | 52 + .../wmsa/edge/crawler/EdgeCrawlerMain.java | 35 + .../wmsa/edge/crawler/EdgeCrawlerModule.java | 52 + .../wmsa/edge/crawler/EdgeCrawlerService.java | 107 + .../wmsa/edge/crawler/RssScraperMain.java | 31 + .../crawler/domain/DomainCrawlResults.java | 55 + .../edge/crawler/domain/DomainCrawler.java | 509 + .../crawler/domain/DomainCrawlerFactory.java | 40 + .../domain/DomainCrawlerRobotsTxt.java | 67 + .../edge/crawler/domain/FeedExtractor.java | 55 + .../wmsa/edge/crawler/domain/LinkParser.java | 166 + .../wmsa/edge/crawler/domain/RssCrawler.java | 217 + .../wmsa/edge/crawler/domain/UrlsCache.java | 110 + .../domain/language/DocumentDebugger.java | 132 + .../domain/language/LanguageFilter.java | 90 + .../domain/language/UnicodeRanges.java | 77 + .../crawler/domain/language/WordPatterns.java | 117 + .../domain/language/conf/LanguageModels.java | 15 + .../language/processing/AsciiFlattener.java | 51 + .../processing/DocumentKeywordExtractor.java | 150 + .../language/processing/KeywordCounter.java | 93 + .../language/processing/KeywordExtractor.java | 380 + .../language/processing/LongNameCounter.java | 63 + .../language/processing/NameCounter.java | 40 + .../processing/SentenceExtractor.java | 299 + .../language/processing/SubjectCounter.java | 47 + .../model/DocumentLanguageData.java | 32 + .../processing/model/DocumentSentence.java | 88 + .../language/processing/model/WordRef.java | 46 + .../language/processing/model/WordRep.java | 31 + .../language/processing/model/WordSpan.java | 53 + .../processing/model/tag/WordSeparator.java | 6 + .../processing/model/tag/WordTag.java | 8 + .../crawler/domain/processor/HtmlFeature.java | 28 + .../domain/processor/HtmlProcessor.java | 328 + .../processor/HtmlStandardExtractor.java | 84 + .../domain/processor/HtmlSummarizer.java | 139 + .../domain/processor/HtmlTagCleaner.java | 40 + .../domain/processor/PlainTextProcessor.java | 74 + .../crawler/fetcher/ContentTypeParser.java | 77 + .../wmsa/edge/crawler/fetcher/Cookies.java | 44 + .../edge/crawler/fetcher/HttpFetcher.java | 256 + .../crawler/fetcher/HttpRedirectResolver.java | 105 + .../edge/crawler/fetcher/NoSecuritySSL.java | 44 + .../crawler/worker/CrawlerDiscoverWorker.java | 131 + .../crawler/worker/CrawlerIndexWorker.java | 89 + .../edge/crawler/worker/GeoIpBlocklist.java | 101 + .../edge/crawler/worker/InetAddressCache.java | 23 + .../wmsa/edge/crawler/worker/IpBlockList.java | 88 + .../edge/crawler/worker/UploaderWorker.java | 200 + .../edge/crawler/worker/UrlBlocklist.java | 55 + .../wmsa/edge/crawler/worker/Worker.java | 15 + .../edge/crawler/worker/WorkerFactory.java | 47 + .../worker/data/CrawlJobsSpecification.java | 8 + .../crawler/worker/data/UploaderMetrics.java | 15 + .../crawler/worker/facade/TaskProvider.java | 10 + .../worker/facade/TaskProviderImpl.java | 47 + .../crawler/worker/facade/UploadFacade.java | 25 + .../worker/facade/UploadFacadeDirectImpl.java | 145 + .../worker/results/DomainAliasResult.java | 19 + .../results/DomainCrawlerWorkerResults.java | 16 + .../worker/results/InvalidTaskResult.java | 18 + .../crawler/worker/results/WorkerResults.java | 9 + .../wmsa/edge/crawling/AbortMonitor.java | 47 + .../edge/crawling/CrawlJobExtractorMain.java | 224 + .../CrawlJobExtractorPageRankMain.java | 203 + .../wmsa/edge/crawling/CrawlPlanLoader.java | 26 + .../edge/crawling/CrawledDomainReader.java | 28 + .../edge/crawling/CrawledDomainWriter.java | 66 + .../wmsa/edge/crawling/CrawlerMain.java | 131 + .../crawling/CrawlerSpecificationLoader.java | 32 + .../wmsa/edge/crawling/WorkLog.java | 86 + .../edge/crawling/model/CrawlLogEntry.java | 4 + .../edge/crawling/model/CrawledDocument.java | 25 + .../edge/crawling/model/CrawledDomain.java | 27 + .../crawling/model/CrawlerDocumentStatus.java | 10 + .../crawling/model/CrawlerDomainStatus.java | 5 + .../crawling/model/CrawlingSpecification.java | 13 + .../crawling/retreival/CrawlerRetreiver.java | 297 + .../edge/crawling/retreival/HttpFetcher.java | 305 + .../wmsa/edge/data/dao/EdgeDataStoreDao.java | 65 + .../edge/data/dao/EdgeDataStoreDaoImpl.java | 1180 + .../edge/data/dao/EdgeDataStoreTaskDao.java | 18 + .../dao/task/EdgeDataStoreTaskDaoImpl.java | 496 + .../task/EdgeDataStoreTaskOngoingJobs.java | 49 + .../data/dao/task/EdgeDataStoreTaskTuner.java | 142 + .../data/dao/task/EdgeDomainBlacklist.java | 17 + .../dao/task/EdgeDomainBlacklistImpl.java | 77 + .../data/dao/task/EdgeFinishTasksQueue.java | 91 + .../wmsa/edge/dating/DatingMain.java | 37 + .../wmsa/edge/dating/DatingModule.java | 6 + .../wmsa/edge/dating/DatingService.java | 184 + .../wmsa/edge/dating/DatingSessionObject.java | 89 + .../wmsa/edge/director/EdgeDirectorMain.java | 35 + .../edge/director/EdgeDirectorModule.java | 6 + .../edge/director/EdgeDirectorService.java | 109 + .../director/client/EdgeDirectorClient.java | 49 + .../wmsa/edge/index/EdgeIndexControl.java | 40 + .../wmsa/edge/index/EdgeIndexMain.java | 36 + .../wmsa/edge/index/EdgeIndexModule.java | 12 + .../wmsa/edge/index/EdgeIndexService.java | 506 + .../wmsa/edge/index/EdgeTablesModule.java | 28 + .../wmsa/edge/index/IndexServicesFactory.java | 223 + .../edge/index/client/EdgeIndexClient.java | 69 + .../index/model/EdgeIndexSearchTerms.java | 12 + .../edge/index/model/EdgePutWordsRequest.java | 20 + .../wmsa/edge/index/model/IndexBlock.java | 34 + .../edge/index/radix/EdgeIndexBucket.java | 155 + .../index/service/SearchEngineRanking.java | 56 + .../edge/index/service/SearchIndexDao.java | 113 + .../edge/index/service/SearchIndexes.java | 153 + .../wmsa/edge/index/service/SearchOrder.java | 6 + .../service/dictionary/DictionaryReader.java | 27 + .../service/dictionary/DictionaryWriter.java | 376 + .../service/dictionary/TokenCompressor.java | 83 + .../edge/index/service/index/SearchIndex.java | 115 + .../service/index/SearchIndexConverter.java | 383 + .../index/SearchIndexPreconverter.java | 135 + .../service/index/SearchIndexReader.java | 134 + .../index/SearchIndexScrubberMain.java | 79 + .../service/index/SearchIndexWriter.java | 16 + .../service/index/SearchIndexWriterImpl.java | 121 + .../index/wordstable/BtreeWordsTable.java | 88 + .../index/wordstable/IndexWordsTable.java | 48 + .../index/wordstable/WordsTableWriter.java | 85 + .../service/query/IndexQueryBuilder.java | 128 + .../service/query/IndexSearchBudget.java | 21 + .../wmsa/edge/index/service/query/Query.java | 10 + .../service/query/SearchIndexPartitioner.java | 168 + .../service/util/ranking/AcademiaRank.java | 49 + .../util/ranking/BetterReversePageRank.java | 46 + .../util/ranking/BetterStandardPageRank.java | 50 + .../util/ranking/BuggyReversePageRank.java | 43 + .../util/ranking/BuggyStandardPageRank.java | 49 + .../util/ranking/RankingAlgorithm.java | 476 + .../ranking/old/OldReversePageRankV2.java | 261 + .../util/ranking/old/StandardPageRank.java | 270 + .../service/util/ranking/tool/DedupTool.java | 89 + .../util/ranking/tool/PerusePageRankV2.java | 340 + .../ranking/tool/TestAcademiaRankTool.java | 30 + .../ranking/tool/UpdateDomainRanksTool.java | 95 + .../ranking/tool/UpdateDomainRanksTool2.java | 105 + .../edge/integration/BasicPageUploader.java | 57 + .../edge/integration/arxiv/ArxivParser.java | 29 + .../arxiv/model/ArxivMetadata.java | 21 + .../integration/model/BasicDocumentData.java | 22 + .../StackOverflowPostProcessor.java | 83 + .../StackOverflowPostsReader.java | 123 + .../model/StackOverflowPost.java | 14 + .../model/StackOverflowQuestionData.java | 14 + .../wikipedia/WikipediaProcessor.java | 82 + .../wikipedia/WikipediaReader.java | 46 + .../wikipedia/model/WikipediaArticle.java | 12 + .../wmsa/edge/model/EdgeCrawlPlan.java | 30 + .../wmsa/edge/model/EdgeDomain.java | 130 + .../nu/marginalia/wmsa/edge/model/EdgeId.java | 15 + .../marginalia/wmsa/edge/model/EdgeUrl.java | 124 + .../wmsa/edge/model/WideHashable.java | 5 + .../edge/model/crawl/EdgeContentType.java | 15 + .../model/crawl/EdgeDomainIndexingState.java | 27 + .../wmsa/edge/model/crawl/EdgeDomainLink.java | 10 + .../edge/model/crawl/EdgeHtmlStandard.java | 19 + .../wmsa/edge/model/crawl/EdgeIndexTask.java | 33 + .../edge/model/crawl/EdgePageContent.java | 25 + .../edge/model/crawl/EdgePageMetadata.java | 50 + .../edge/model/crawl/EdgePageWordSet.java | 48 + .../wmsa/edge/model/crawl/EdgePageWords.java | 34 + .../edge/model/crawl/EdgeRawPageContents.java | 24 + .../wmsa/edge/model/crawl/EdgeRobotsTxt.java | 10 + .../wmsa/edge/model/crawl/EdgeUrlState.java | 10 + .../wmsa/edge/model/crawl/EdgeUrlVisit.java | 21 + .../model/search/EdgePageScoreAdjustment.java | 30 + .../model/search/EdgeSearchResultItem.java | 39 + .../search/EdgeSearchResultKeywordScore.java | 14 + .../model/search/EdgeSearchResultSet.java | 19 + .../edge/model/search/EdgeSearchResults.java | 32 + .../model/search/EdgeSearchResultsKey.java | 13 + .../model/search/EdgeSearchSpecification.java | 32 + .../edge/model/search/EdgeSearchSubquery.java | 34 + .../edge/model/search/EdgeUrlDetails.java | 151 + .../wmsa/edge/search/BrowseResult.java | 11 + .../wmsa/edge/search/BrowseResultSet.java | 12 + .../edge/search/DecoratedSearchResultSet.java | 22 + .../edge/search/DecoratedSearchResults.java | 33 + .../wmsa/edge/search/EdgeSearchMain.java | 38 + .../wmsa/edge/search/EdgeSearchModule.java | 22 + .../wmsa/edge/search/EdgeSearchOperator.java | 334 + .../wmsa/edge/search/EdgeSearchProfile.java | 68 + .../edge/search/EdgeSearchRankingSymbols.java | 34 + .../wmsa/edge/search/EdgeSearchService.java | 369 + .../wmsa/edge/search/UnitConversion.java | 77 + .../edge/search/client/EdgeSearchClient.java | 29 + .../edge/search/query/EnglishDictionary.java | 161 + .../wmsa/edge/search/query/QueryFactory.java | 186 + .../wmsa/edge/search/query/QueryParser.java | 428 + .../wmsa/edge/search/query/QueryVariants.java | 396 + .../search/query/model/EdgeSearchQuery.java | 21 + .../query/model/EdgeUserSearchParameters.java | 12 + .../search/results/SearchResultDecorator.java | 113 + .../search/results/SearchResultValuator.java | 92 + .../edge/search/results/UrlDeduplicator.java | 41 + .../model/AccumulatedQueryResults.java | 39 + .../results/model/TieredSearchResult.java | 12 + .../wmsa/edge/tools/ConverterMain.java | 384 + .../wmsa/edge/tools/CrawlDomainMain.java | 390 + .../wmsa/edge/tools/DomainInserterMain.java | 83 + .../wmsa/edge/tools/IndexMergerMain.java | 198 + .../wmsa/edge/tools/ReindexMain.java | 354 + .../edge/tools/StackOverflowLoaderMain.java | 96 + .../edge/tools/TermFrequencyCounterMain.java | 142 + .../wmsa/edge/tools/ZimConverterMain.java | 211 + .../java/nu/marginalia/wmsa/memex/Memex.java | 244 + .../wmsa/memex/MemexConfigurationModule.java | 51 + .../nu/marginalia/wmsa/memex/MemexData.java | 150 + .../nu/marginalia/wmsa/memex/MemexLinks.java | 54 + .../nu/marginalia/wmsa/memex/MemexLoader.java | 265 + .../nu/marginalia/wmsa/memex/MemexMain.java | 32 + .../marginalia/wmsa/memex/MemexService.java | 280 + .../wmsa/memex/change/GemtextAppend.java | 70 + .../wmsa/memex/change/GemtextCreate.java | 20 + .../memex/change/GemtextCreateOrMutate.java | 27 + .../wmsa/memex/change/GemtextMutation.java | 20 + .../wmsa/memex/change/GemtextPrepend.java | 64 + .../wmsa/memex/change/GemtextReplace.java | 66 + .../GemtextTombstoneUpdateCaclulator.java | 48 + .../GemtextDocumentUpdateCalculator.java | 109 + .../change/update/GemtextTaskExtractor.java | 31 + .../change/update/GemtextTasksRewrite.java | 100 + .../wmsa/memex/client/MemexApiClient.java | 16 + .../wmsa/memex/model/GemtextSection.java | 11 + .../memex/model/GemtextSectionAction.java | 6 + .../wmsa/memex/model/MemexExternalUrl.java | 18 + .../wmsa/memex/model/MemexImage.java | 13 + .../wmsa/memex/model/MemexIndexTask.java | 13 + .../wmsa/memex/model/MemexLink.java | 26 + .../wmsa/memex/model/MemexNode.java | 40 + .../wmsa/memex/model/MemexNodeHeadingId.java | 74 + .../wmsa/memex/model/MemexNodeTaskId.java | 66 + .../wmsa/memex/model/MemexNodeType.java | 14 + .../wmsa/memex/model/MemexNodeUrl.java | 98 + .../wmsa/memex/model/MemexTaskState.java | 30 + .../wmsa/memex/model/MemexTaskTags.java | 40 + .../marginalia/wmsa/memex/model/MemexUrl.java | 13 + .../wmsa/memex/model/fs/MemexDirectory.java | 30 + .../wmsa/memex/model/fs/MemexFileSystem.java | 88 + .../render/MemexRenderCreateFormModel.java | 32 + .../render/MemexRenderUpdateFormModel.java | 19 + .../render/MemexRenderUploadFormModel.java | 32 + .../render/MemexRendererDeleteFormModel.java | 19 + .../model/render/MemexRendererImageModel.java | 36 + .../model/render/MemexRendererIndexModel.java | 69 + .../render/MemexRendererRenameFormModel.java | 19 + .../render/MemexRendererTombstoneModel.java | 16 + .../model/render/MemexRendererViewModel.java | 24 + .../model/render/MemexRendererableDirect.java | 7 + .../wmsa/memex/renderer/MemexGmiRenderer.java | 228 + .../memex/renderer/MemexHtmlRenderer.java | 197 + .../wmsa/memex/renderer/MemexRendererers.java | 22 + .../system/MemexFileSystemModifiedTimes.java | 23 + .../memex/system/MemexFileSystemMonitor.java | 115 + .../wmsa/memex/system/MemexFileWriter.java | 120 + .../wmsa/memex/system/MemexGitRepo.java | 135 + .../memex/system/MemexSourceFileSystem.java | 83 + .../wmsa/podcasts/PodcastFetcher.java | 112 + .../wmsa/podcasts/PodcastScraperMain.java | 31 + .../wmsa/podcasts/PodcastScraperService.java | 78 + .../wmsa/podcasts/model/Podcast.java | 16 + .../wmsa/podcasts/model/PodcastEpisode.java | 16 + .../wmsa/podcasts/model/PodcastListing.java | 16 + .../wmsa/podcasts/model/PodcastMetadata.java | 17 + .../podcasts/model/PodcastNewEpisodes.java | 11 + .../wmsa/renderer/PodcastRendererService.java | 130 + .../wmsa/renderer/RendererMain.java | 31 + .../wmsa/renderer/RendererModule.java | 8 + .../wmsa/renderer/RendererService.java | 46 + .../wmsa/renderer/ServerStatusModel.java | 10 + .../wmsa/renderer/SmhiRendererService.java | 83 + .../wmsa/renderer/StatusRendererService.java | 82 + .../wmsa/renderer/client/RendererClient.java | 65 + .../renderer/mustache/MustacheRenderer.java | 166 + .../renderer/mustache/RendererFactory.java | 13 + .../request/smhi/RenderSmhiIndexReq.java | 13 + .../request/smhi/RenderSmhiPrognosReq.java | 11 + .../resource_store/ResourceEntityStore.java | 248 + .../resource_store/ResourceStoreClient.java | 46 + .../resource_store/ResourceStoreMain.java | 32 + .../resource_store/ResourceStoreModule.java | 15 + .../resource_store/ResourceStoreService.java | 191 + .../model/RenderedResource.java | 46 + .../wmsa/smhi/SmhiScraperService.java | 79 + .../marginalia/wmsa/smhi/model/Parameter.java | 9 + .../nu/marginalia/wmsa/smhi/model/Plats.java | 42 + .../marginalia/wmsa/smhi/model/Platser.java | 16 + .../wmsa/smhi/model/PrognosData.java | 41 + .../marginalia/wmsa/smhi/model/Tidpunkt.java | 75 + .../wmsa/smhi/model/dyn/Dygnsdata.java | 40 + .../wmsa/smhi/model/index/IndexPlats.java | 13 + .../wmsa/smhi/model/index/IndexPlatser.java | 28 + .../wmsa/smhi/scraper/PlatsReader.java | 44 + .../wmsa/smhi/scraper/SmhiScraperMain.java | 32 + .../wmsa/smhi/scraper/SmhiScraperModule.java | 12 + .../smhi/scraper/crawler/SmhiBackendApi.java | 88 + .../smhi/scraper/crawler/SmhiCrawler.java | 106 + .../crawler/entity/SmhiEntityStore.java | 62 + marginalia_nu/src/main/nlp-models/README.md | 3 + .../src/main/nlp-models/en-token.bin | Bin 0 -> 439890 bytes .../src/main/nlp-models/se-token.bin | Bin 0 -> 219795 bytes .../src/main/resources/data/smhi/stader.csv | 134 + .../src/main/resources/dictionary/en-1000 | 1003 + .../main/resources/dictionary/en-stopwords | 181 + .../src/main/resources/dictionary/en-words | 102401 +++++++++++++++ .../src/main/resources/dictionary/latin-1000 | 984 + .../src/main/resources/dictionary/swe-1000 | 641 + .../main/resources/dictionary/word-frequency | 1003 + .../src/main/resources/fonts/LM-regular.ttf | Bin 0 -> 150124 bytes .../resources/fonts/STIXTwoMath-Regular.ttf | Bin 0 -> 1518116 bytes .../src/main/resources/ip-banned-cidr.txt | 24 + .../src/main/resources/log4j2.properties | 29 + .../main/resources/sql/data-store-init.sql | 9 + .../main/resources/sql/edge-crawler-cache.sql | 250 + .../main/resources/sql/monitor-log-init.sql | 11 + .../src/main/resources/sql/reference-data.sql | 23 + .../main/resources/static/dating/index.html | 71 + .../main/resources/static/dating/robots.txt | 4 + .../src/main/resources/static/edge/about.html | 23 + .../main/resources/static/edge/changelog.html | 23 + .../resources/static/edge/crawler-ips.txt | 1 + .../src/main/resources/static/edge/error.html | 23 + .../main/resources/static/edge/favicon.ico | Bin 0 -> 1211 bytes .../src/main/resources/static/edge/index.html | 159 + .../resources/static/edge/known-issues.html | 29 + .../resources/static/edge/maintenance.html | 10 + .../src/main/resources/static/edge/notes.html | 28 + .../main/resources/static/edge/opensearch.xml | 11 + .../src/main/resources/static/edge/robots.txt | 8 + .../main/resources/static/edge/style-new.css | 477 + .../src/main/resources/static/edge/tts.js | 101 + .../resources/static/edge/wiki-clean.html | 76 + .../resources/static/encyclopedia/index.html | 24 + .../resources/static/encyclopedia/robots.txt | 2 + .../static/encyclopedia/wiki-clean.html | 71 + .../static/encyclopedia/wiki-start.html | 26 + .../resources/static/fonts/LM-bold-italic.ttf | Bin 0 -> 191324 bytes .../static/fonts/LM-bold-italic.woff | Bin 0 -> 66172 bytes .../static/fonts/LM-bold-italic.woff2 | Bin 0 -> 39392 bytes .../main/resources/static/fonts/LM-bold.ttf | Bin 0 -> 144572 bytes .../main/resources/static/fonts/LM-bold.woff | Bin 0 -> 52888 bytes .../main/resources/static/fonts/LM-bold.woff2 | Bin 0 -> 33548 bytes .../main/resources/static/fonts/LM-italic.ttf | Bin 0 -> 191540 bytes .../resources/static/fonts/LM-italic.woff | Bin 0 -> 66180 bytes .../resources/static/fonts/LM-italic.woff2 | Bin 0 -> 39148 bytes .../resources/static/fonts/LM-regular.ttf | Bin 0 -> 150124 bytes .../resources/static/fonts/LM-regular.woff | Bin 0 -> 53760 bytes .../resources/static/fonts/LM-regular.woff2 | Bin 0 -> 33664 bytes .../main/resources/static/memex/ico/dir.png | Bin 0 -> 174 bytes .../main/resources/static/memex/ico/doc32.png | Bin 0 -> 247 bytes .../main/resources/static/memex/ico/file.png | Bin 0 -> 141 bytes .../resources/static/memex/ico/folder32.png | Bin 0 -> 240 bytes .../resources/static/memex/ico/folderup16.png | Bin 0 -> 251 bytes .../main/resources/static/memex/ico/nav16.png | Bin 0 -> 301 bytes .../main/resources/static/memex/ico/pic16.png | Bin 0 -> 296 bytes .../main/resources/static/memex/ico/pic32.png | Bin 0 -> 472 bytes .../main/resources/static/memex/ico/root.png | Bin 0 -> 301 bytes .../resources/static/memex/ico/shiba16.png | Bin 0 -> 316 bytes .../resources/static/memex/ico/world16.png | Bin 0 -> 343 bytes .../main/resources/static/memex/style-new.css | 235 + .../main/resources/static/podcast/style.css | 3 + .../main/resources/static/smhi/favicon.ico | Bin 0 -> 1211 bytes .../src/main/resources/static/smhi/font.css | 50 + .../main/resources/static/smhi/responsive.css | 74 + .../src/main/resources/static/smhi/style.css | 192 + .../src/main/resources/static/style.css | 246 + .../main/resources/templates/auth/login.hdb | 36 + .../templates/dating/dating-view.hdb | 150 + .../templates/edge/browse-result.hdb | 12 + .../templates/edge/browse-results.hdb | 49 + .../templates/edge/conversion-results-gmi.hdb | 12 + .../templates/edge/conversion-results.hdb | 37 + .../templates/edge/dictionary-results-gmi.hdb | 17 + .../templates/edge/dictionary-results.hdb | 48 + .../resources/templates/edge/error-page.hdb | 20 + .../templates/edge/parts/search-footer.hdb | 9 + .../templates/edge/parts/search-form.hdb | 26 + .../templates/edge/parts/search-header.hdb | 8 + .../templates/edge/search-result-gmi.hdb | 4 + .../templates/edge/search-result-metadata.hdb | 9 + .../templates/edge/search-result.hdb | 14 + .../templates/edge/search-results-gmi.hdb | 19 + .../templates/edge/search-results.hdb | 47 + .../templates/edge/site-info-gmi.hdb | 14 + .../resources/templates/edge/site-info.hdb | 58 + .../templates/encyclopedia/wiki-error.hdb | 28 + .../templates/encyclopedia/wiki-search.hdb | 35 + .../templates/memex/memex-create-form.hdb | 23 + .../templates/memex/memex-delete-form.hdb | 23 + .../resources/templates/memex/memex-image.hdb | 24 + .../templates/memex/memex-index-feed.hdb | 20 + .../resources/templates/memex/memex-index.hdb | 34 + .../templates/memex/memex-rename-form.hdb | 23 + .../templates/memex/memex-tombstone.hdb | 17 + .../templates/memex/memex-update-form.hdb | 20 + .../templates/memex/memex-upload-form.hdb | 26 + .../resources/templates/memex/memex-view.hdb | 36 + .../memex/partial/memex-backlinks-inline.hdb | 6 + .../memex/partial/memex-backlinks.hdb | 9 + .../memex/partial/memex-directories.hdb | 9 + .../memex/partial/memex-documents-inline.hdb | 10 + .../memex/partial/memex-documents.hdb | 10 + .../templates/memex/partial/memex-footer.hdb | 4 + .../templates/memex/partial/memex-head.hdb | 9 + .../templates/memex/partial/memex-images.hdb | 6 + .../memex/partial/memex-task-listing.hdb | 10 + .../templates/memex/partial/memex-topbar.hdb | 13 + .../resources/templates/podcast/episode.hdb | 35 + .../resources/templates/podcast/listing.hdb | 33 + .../main/resources/templates/podcast/new.hdb | 34 + .../resources/templates/podcast/podcast.hdb | 38 + .../main/resources/templates/smhi/index.hdb | 44 + .../main/resources/templates/smhi/prognos.hdb | 73 + .../templates/status/server-status.hdb | 25 + marginalia_nu/src/main/resources/units.csv | 62 + marginalia_nu/src/test/java/EmptyTest.java | 8 + .../gemini/gmi/GemtextDatabaseTest.java | 30 + .../gemini/gmi/GemtextDocumentTest.java | 88 + .../gmi/parser/GemtextTaskParserTest.java | 18 + .../marginalia/util/SeekDictionaryTest.java | 31 + .../java/nu/marginalia/util/TestUtil.java | 58 + .../util/btree/BTreeWriterTest.java | 329 + .../dithering/FloydSteinbergDitherTest.java | 32 + .../util/hash/LongPairHashMapTest.java | 58 + .../nu/marginalia/util/test/TestUtil.java | 31 + .../configuration/server/ServiceTest.java | 82 + .../wmsa/data_store/AssistantTest.java | 152 + .../wmsa/data_store/DataStoreServiceTest.java | 173 + .../wmsa/edge/EdgeDirectorServiceTest.java | 263 + .../wmsa/edge/archive/ArchiveTest.java | 72 + .../edge/archive/archiver/ArchiverTest.java | 18 + .../edge/assistant/dict/WikiCleanerTest.java | 47 + .../edge/assistant/eval/MathParserTest.java | 42 + .../wmsa/edge/assistant/eval/UnitsTest.java | 44 + .../assistant/suggest/SuggestionsTest.java | 47 + .../CrawlJobsSpecificationSetTest.java | 59 + .../domain/DomainCrawlerRobotsTxtTest.java | 33 + .../crawler/domain/DomainCrawlerTest.java | 287 + .../crawler/domain/DomainCrawlerTest2.java | 68 + .../crawler/domain/LanguageFilterTest.java | 25 + .../edge/crawler/domain/LinkParserTest.java | 53 + .../edge/crawler/domain/RssCrawlerTest.java | 66 + .../edge/crawler/domain/UrlsCacheTest.java | 58 + .../processing/SentenceExtractorTest.java | 255 + .../domain/processor/HtmlProcessorTest.java | 119 + .../domain/processor/HtmlTagCleanerTest.java | 27 + .../edge/crawler/fetcher/HttpFetcherTest.java | 79 + .../edge/crawler/worker/IpBlockListTest.java | 35 + .../edge/crawler/worker/UrlBlocklistTest.java | 22 + .../edge/crawling/CrawlPlanLoaderTest.java | 50 + .../wmsa/edge/crawling/WorkLogTest.java | 54 + .../wmsa/edge/data/EdgeDataStoreDaoTest.java | 213 + .../index/service/DictionaryWriterTest.java | 213 + .../index/service/EdgeIndexClientTest.java | 187 + .../edge/index/service/EdgeSearchTest.java | 524 + .../index/service/EdgeSearchTestLocal.java | 143 + .../edge/index/service/MultimapFileTest.java | 138 + .../service/SearchIndexConverterTest.java | 88 + .../index/service/SearchIndexWriterTest.java | 90 + .../index/service/TokenCompressorTest.java | 28 + .../index/service/util/ByteFolderTest.java | 35 + .../service/util/DictionaryDataTest.java | 21 + .../service/util/DictionaryHashMapTest.java | 67 + .../index/service/util/PrimeUtilTest.java | 31 + .../service/util/RandomWriteFunnelTest.java | 70 + .../integration/arxiv/ArxivParserTest.java | 50 + .../stackoverflow/StackOverflowPostsTest.java | 54 + .../integration/wikipedia/WikipediaTest.java | 78 + .../wmsa/edge/model/EdgeDomainTest.java | 106 + .../wmsa/edge/model/EdgeUrlTest.java | 22 + .../search/query/BodyQueryParserTest.java | 106 + .../search/query/EnglishDictionaryTest.java | 27 + .../edge/search/query/QueryParserTest.java | 40 + .../edge/search/query/QueryVariantsTest.java | 63 + .../wmsa/memex/MemexFileWriterTest.java | 43 + .../nu/marginalia/wmsa/memex/MemexTest.java | 12 + .../wmsa/memex/change/GemtextChangeTest.java | 274 + .../memex/change/GemtextTaskUpdateTest.java | 226 + .../GemtextTombstoneUpdateCaclulatorTest.java | 106 + .../memex/model/MemexNodeHeadingIdTest.java | 33 + .../wmsa/podcasts/PodcastFetcherTest.java | 15 + .../ResourceStoreServiceTest.java | 119 + .../scraper/crawler/SmhiBackendApiTest.java | 33 + .../src/test/resources/html/monadnock.html | 17 + .../src/test/resources/log4j2.properties | 15 + .../src/test/resources/model-data.json | 1 + settings.gradle | 4 + third_party/README.md | 11 + third_party/build.gradle | 111 + .../ca/rmen/porterstemmer/PorterStemmer.java | 373 + .../com/github/datquocnguyen/FWObject.java | 39 + .../github/datquocnguyen/InitialTagger.java | 78 + .../java/com/github/datquocnguyen/Node.java | 67 + .../github/datquocnguyen/RDRPOSTagger.java | 113 + .../java/com/github/datquocnguyen/Utils.java | 185 + .../com/github/datquocnguyen/WordTag.java | 21 + .../com/upserve/uppend/blobs/NativeIO.java | 75 + .../org/openzim/ZIMTypes/ArticleEntry.java | 46 + .../org/openzim/ZIMTypes/DirectoryEntry.java | 69 + .../org/openzim/ZIMTypes/RedirectEntry.java | 37 + .../java/org/openzim/ZIMTypes/ZIMFile.java | 201 + .../java/org/openzim/ZIMTypes/ZIMReader.java | 677 + .../util/RandomAcessFileZIMInputStream.java | 148 + .../main/java/org/openzim/util/Utilities.java | 84 + .../java/org/tukaani/xz/BlockInputStream.java | 212 + .../org/tukaani/xz/BlockOutputStream.java | 128 + .../tukaani/xz/CorruptedInputException.java | 37 + .../org/tukaani/xz/CountingInputStream.java | 42 + .../org/tukaani/xz/CountingOutputStream.java | 46 + .../main/java/org/tukaani/xz/DeltaCoder.java | 26 + .../java/org/tukaani/xz/DeltaDecoder.java | 32 + .../java/org/tukaani/xz/DeltaInputStream.java | 105 + .../main/java/org/tukaani/xz/FilterCoder.java | 16 + .../java/org/tukaani/xz/FilterDecoder.java | 17 + .../java/org/tukaani/xz/FilterEncoder.java | 16 + .../java/org/tukaani/xz/FilterOptions.java | 28 + .../tukaani/xz/FinishableOutputStream.java | 31 + .../tukaani/xz/IndexIndicatorException.java | 14 + .../main/java/org/tukaani/xz/LZMA2Coder.java | 26 + .../java/org/tukaani/xz/LZMA2Decoder.java | 35 + .../java/org/tukaani/xz/LZMA2Encoder.java | 35 + .../java/org/tukaani/xz/LZMA2InputStream.java | 329 + .../java/org/tukaani/xz/LZMA2Options.java | 139 + .../org/tukaani/xz/LZMA2OutputStream.java | 77 + .../org/tukaani/xz/MemoryLimitException.java | 60 + .../main/java/org/tukaani/xz/RawCoder.java | 33 + .../org/tukaani/xz/SingleXZInputStream.java | 285 + .../xz/UnsupportedOptionsException.java | 34 + .../src/main/java/org/tukaani/xz/XZ.java | 53 + .../org/tukaani/xz/XZFormatException.java | 24 + .../java/org/tukaani/xz/XZIOException.java | 28 + .../java/org/tukaani/xz/XZInputStream.java | 257 + .../java/org/tukaani/xz/XZOutputStream.java | 290 + .../main/java/org/tukaani/xz/check/CRC32.java | 33 + .../main/java/org/tukaani/xz/check/CRC64.java | 54 + .../main/java/org/tukaani/xz/check/Check.java | 57 + .../main/java/org/tukaani/xz/check/None.java | 24 + .../java/org/tukaani/xz/check/SHA256.java | 30 + .../org/tukaani/xz/common/DecoderUtil.java | 121 + .../org/tukaani/xz/common/EncoderUtil.java | 36 + .../org/tukaani/xz/common/StreamFlags.java | 15 + .../main/java/org/tukaani/xz/common/Util.java | 28 + .../java/org/tukaani/xz/delta/DeltaCoder.java | 27 + .../org/tukaani/xz/delta/DeltaDecoder.java | 24 + .../java/org/tukaani/xz/index/IndexBase.java | 56 + .../org/tukaani/xz/index/IndexEncoder.java | 59 + .../java/org/tukaani/xz/index/IndexHash.java | 94 + .../org/tukaani/xz/index/IndexRecord.java | 20 + .../java/org/tukaani/xz/lz/LZDecoder.java | 126 + .../java/org/tukaani/xz/lzma/LZMACoder.java | 139 + .../java/org/tukaani/xz/lzma/LZMADecoder.java | 189 + .../main/java/org/tukaani/xz/lzma/State.java | 65 + .../java/org/tukaani/xz/package-info.java | 21 + .../org/tukaani/xz/rangecoder/RangeCoder.java | 25 + .../tukaani/xz/rangecoder/RangeDecoder.java | 129 + 741 files changed, 162156 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 marginalia_nu/build.gradle create mode 100644 marginalia_nu/lombok.config create mode 100644 marginalia_nu/src/jmh/java/bs_vs_ls/BinSearchVsLinSearch.java create mode 100644 marginalia_nu/src/jmh/java/bs_vs_ls/BinSearchVsLinSearch2.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/BadBotList.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/GeminiConfigurationModule.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/GeminiService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/client/GeminiClient.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/Gemtext.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/GemtextDatabase.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/GemtextDocument.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/AbstractGemtextLine.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextAside.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextHeading.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextLineVisitor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextLineVisitorAdapter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextLink.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextList.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextPragma.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextPreformat.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextQuote.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextTask.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextText.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextTextLiteral.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextAsideParser.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextHeadingParser.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextLinkParser.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextListParser.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextParser.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextPragmaParser.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextQuoteParser.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextTaskParser.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/renderer/GemtextRenderer.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/renderer/GemtextRendererFactory.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/io/GeminiConnection.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/io/GeminiSSLSetUp.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/io/GeminiStatusCode.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/io/GeminiUserException.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/plugins/BareStaticPagePlugin.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/plugins/FileType.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/plugins/Plugin.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/gemini/plugins/SearchPlugin.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/ByteFolder.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/FileSizeUtil.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/ParallelPipe.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/PrimeUtil.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/RandomWriteFunnel.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/SeekDictionary.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/btree/BTreeReader.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/btree/BTreeWriter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/btree/WriteCallback.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/btree/model/BTreeContext.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/btree/model/BTreeHeader.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/dict/DictionaryData.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/dict/DictionaryHashMap.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/graphics/dithering/FloydSteinbergDither.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/graphics/dithering/Palettes.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/hash/LongPairHashMap.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/multimap/MultimapFileLong.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/multimap/MultimapSearcher.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/util/multimap/MultimapSorter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/AuthConfigurationModule.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/AuthMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/AuthService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/ApiMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/ApiService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/model/ApiLicense.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/model/ApiSearchResult.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/model/ApiSearchResults.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/client/AuthClient.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/model/LoginFormModel.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/client/AbortingScheduler.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/client/AbstractClient.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/client/AbstractDynamicClient.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/client/HttpStatusCode.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/LocalException.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/MessagingException.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/NetworkException.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/RemoteException.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/RouteNotConfiguredException.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/TimeoutException.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/MainClass.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/ServiceDescriptor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/WmsaHome.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/command/Command.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/command/ListCommand.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/command/StartCommand.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/command/VersionCommand.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/ConfigurationModule.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/DatabaseModule.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/HostnameProvider.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/LoggerConfiguration.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/MetricsPortProvider.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/PortProvider.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/Context.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/Initialization.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/MetricsServer.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/RateLimiter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/Service.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/DataStoreMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/DataStoreModule.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/DataStoreService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/EdgeDataStoreService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/FileRepository.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/client/DataStoreClient.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/meta/DomainInformation.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/EdgeArchiveMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/EdgeArchiveModule.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/EdgeArchiveService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/archiver/ArchiveExtractor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/archiver/ArchivedFile.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/archiver/Archiver.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/client/ArchiveClient.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/request/EdgeArchiveSubmissionReq.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/EdgeAssistantMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/EdgeAssistantModule.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/EdgeAssistantService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/client/AssistantClient.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/DictionaryEntry.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/DictionaryResponse.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/DictionaryService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/NGramDict.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/SpellChecker.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/SymSpell.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/WikiArticles.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/WikiCleaner.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/WikiSearchResult.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/eval/MathParser.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/eval/Unit.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/eval/Units.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/screenshot/ScreenshotService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/suggest/Suggestions.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/ConvertedDomainReader.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/ConverterMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/ConverterModule.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/CrawledInstructionWriter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/LoaderMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/TaskStats.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/Instruction.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/InstructionTag.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/Interpreter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/DocumentKeywords.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/DomainLink.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadDomain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadDomainLink.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadDomainRedirect.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadKeywords.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadProcessedDocument.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadProcessedDocumentWithError.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadProcessedDomain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadRssFeed.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadUrl.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/IndexLoadKeywords.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/Loader.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/LoaderData.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/LoaderFactory.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadDomainLinks.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadDomains.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadProcessedDocument.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadProcessedDomain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadUrls.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/model/DisqualifiedException.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/model/ProcessedDocument.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/model/ProcessedDocumentDetails.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/model/ProcessedDomain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/DocumentProcessor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/DomainProcessor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/InstructionsCompiler.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/DocumentValuator.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/FeatureExtractor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/LinkProcessor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/SummaryExtractor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/TitleExtractor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/CrawlJobsSpecificationSet.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/EdgeCrawlerMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/EdgeCrawlerModule.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/EdgeCrawlerService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/RssScraperMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlResults.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawler.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerFactory.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerRobotsTxt.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/FeedExtractor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/LinkParser.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/RssCrawler.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/UrlsCache.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/DocumentDebugger.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/LanguageFilter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/UnicodeRanges.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/WordPatterns.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/conf/LanguageModels.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/AsciiFlattener.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/DocumentKeywordExtractor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/KeywordCounter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/KeywordExtractor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/LongNameCounter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/NameCounter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/SentenceExtractor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/SubjectCounter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/DocumentLanguageData.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/DocumentSentence.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/WordRef.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/WordRep.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/WordSpan.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/tag/WordSeparator.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/tag/WordTag.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlFeature.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlProcessor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlStandardExtractor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlSummarizer.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlTagCleaner.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/PlainTextProcessor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/ContentTypeParser.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/Cookies.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/HttpFetcher.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/HttpRedirectResolver.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/NoSecuritySSL.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/CrawlerDiscoverWorker.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/CrawlerIndexWorker.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/GeoIpBlocklist.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/InetAddressCache.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/IpBlockList.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/UploaderWorker.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/UrlBlocklist.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/Worker.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/WorkerFactory.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/data/CrawlJobsSpecification.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/data/UploaderMetrics.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/facade/TaskProvider.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/facade/TaskProviderImpl.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/facade/UploadFacade.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/facade/UploadFacadeDirectImpl.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/results/DomainAliasResult.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/results/DomainCrawlerWorkerResults.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/results/InvalidTaskResult.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/results/WorkerResults.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/AbortMonitor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlJobExtractorMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlJobExtractorPageRankMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlPlanLoader.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawledDomainReader.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawledDomainWriter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlerMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlerSpecificationLoader.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/WorkLog.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawlLogEntry.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawledDocument.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawledDomain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawlerDocumentStatus.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawlerDomainStatus.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawlingSpecification.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/retreival/CrawlerRetreiver.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/retreival/HttpFetcher.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/EdgeDataStoreDao.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/EdgeDataStoreDaoImpl.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/EdgeDataStoreTaskDao.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDataStoreTaskDaoImpl.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDataStoreTaskOngoingJobs.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDataStoreTaskTuner.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDomainBlacklist.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDomainBlacklistImpl.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeFinishTasksQueue.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/dating/DatingMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/dating/DatingModule.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/dating/DatingService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/dating/DatingSessionObject.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/director/EdgeDirectorMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/director/EdgeDirectorModule.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/director/EdgeDirectorService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/director/client/EdgeDirectorClient.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeIndexControl.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeIndexMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeIndexModule.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeIndexService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeTablesModule.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/IndexServicesFactory.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/client/EdgeIndexClient.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/model/EdgeIndexSearchTerms.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/model/EdgePutWordsRequest.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/model/IndexBlock.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/radix/EdgeIndexBucket.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/SearchEngineRanking.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/SearchIndexDao.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/SearchIndexes.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/SearchOrder.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/dictionary/DictionaryReader.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/dictionary/DictionaryWriter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/dictionary/TokenCompressor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndex.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexConverter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexPreconverter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexReader.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexScrubberMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexWriter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexWriterImpl.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/wordstable/BtreeWordsTable.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/wordstable/IndexWordsTable.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/wordstable/WordsTableWriter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/query/IndexQueryBuilder.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/query/IndexSearchBudget.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/query/Query.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/query/SearchIndexPartitioner.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/AcademiaRank.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/BetterReversePageRank.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/BetterStandardPageRank.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/BuggyReversePageRank.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/BuggyStandardPageRank.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/RankingAlgorithm.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/old/OldReversePageRankV2.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/old/StandardPageRank.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/DedupTool.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/PerusePageRankV2.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/TestAcademiaRankTool.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/UpdateDomainRanksTool.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/UpdateDomainRanksTool2.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/BasicPageUploader.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/arxiv/ArxivParser.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/arxiv/model/ArxivMetadata.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/model/BasicDocumentData.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/stackoverflow/StackOverflowPostProcessor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/stackoverflow/StackOverflowPostsReader.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/stackoverflow/model/StackOverflowPost.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/stackoverflow/model/StackOverflowQuestionData.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/wikipedia/WikipediaProcessor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/wikipedia/WikipediaReader.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/wikipedia/model/WikipediaArticle.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/EdgeCrawlPlan.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/EdgeDomain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/EdgeId.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/EdgeUrl.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/WideHashable.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeContentType.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeDomainIndexingState.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeDomainLink.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeHtmlStandard.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeIndexTask.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgePageContent.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgePageMetadata.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgePageWordSet.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgePageWords.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeRawPageContents.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeRobotsTxt.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeUrlState.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeUrlVisit.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgePageScoreAdjustment.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResultItem.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResultKeywordScore.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResultSet.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResults.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResultsKey.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchSpecification.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchSubquery.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeUrlDetails.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/BrowseResult.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/BrowseResultSet.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/DecoratedSearchResultSet.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/DecoratedSearchResults.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchModule.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchOperator.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchProfile.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchRankingSymbols.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/UnitConversion.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/client/EdgeSearchClient.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/EnglishDictionary.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/QueryFactory.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/QueryParser.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/QueryVariants.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/model/EdgeSearchQuery.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/model/EdgeUserSearchParameters.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/SearchResultDecorator.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/SearchResultValuator.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/UrlDeduplicator.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/model/AccumulatedQueryResults.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/model/TieredSearchResult.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/ConverterMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/CrawlDomainMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/DomainInserterMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/IndexMergerMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/ReindexMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/StackOverflowLoaderMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/TermFrequencyCounterMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/ZimConverterMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/Memex.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexConfigurationModule.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexData.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexLinks.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexLoader.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextAppend.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextCreate.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextCreateOrMutate.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextMutation.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextPrepend.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextReplace.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextTombstoneUpdateCaclulator.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/update/GemtextDocumentUpdateCalculator.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/update/GemtextTaskExtractor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/update/GemtextTasksRewrite.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/client/MemexApiClient.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/GemtextSection.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/GemtextSectionAction.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexExternalUrl.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexImage.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexIndexTask.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexLink.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNode.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNodeHeadingId.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNodeTaskId.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNodeType.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNodeUrl.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexTaskState.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexTaskTags.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexUrl.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/fs/MemexDirectory.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/fs/MemexFileSystem.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRenderCreateFormModel.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRenderUpdateFormModel.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRenderUploadFormModel.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererDeleteFormModel.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererImageModel.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererIndexModel.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererRenameFormModel.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererTombstoneModel.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererViewModel.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererableDirect.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/renderer/MemexGmiRenderer.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/renderer/MemexHtmlRenderer.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/renderer/MemexRendererers.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexFileSystemModifiedTimes.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexFileSystemMonitor.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexFileWriter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexGitRepo.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexSourceFileSystem.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/PodcastFetcher.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/PodcastScraperMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/PodcastScraperService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/Podcast.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/PodcastEpisode.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/PodcastListing.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/PodcastMetadata.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/PodcastNewEpisodes.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/PodcastRendererService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/RendererMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/RendererModule.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/RendererService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/ServerStatusModel.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/SmhiRendererService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/StatusRendererService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/client/RendererClient.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/mustache/MustacheRenderer.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/mustache/RendererFactory.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/request/smhi/RenderSmhiIndexReq.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/request/smhi/RenderSmhiPrognosReq.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceEntityStore.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceStoreClient.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceStoreMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceStoreModule.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceStoreService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/model/RenderedResource.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/SmhiScraperService.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/Parameter.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/Plats.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/Platser.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/PrognosData.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/Tidpunkt.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/dyn/Dygnsdata.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/index/IndexPlats.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/index/IndexPlatser.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/PlatsReader.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/SmhiScraperMain.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/SmhiScraperModule.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/crawler/SmhiBackendApi.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/crawler/SmhiCrawler.java create mode 100644 marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/crawler/entity/SmhiEntityStore.java create mode 100644 marginalia_nu/src/main/nlp-models/README.md create mode 100644 marginalia_nu/src/main/nlp-models/en-token.bin create mode 100644 marginalia_nu/src/main/nlp-models/se-token.bin create mode 100644 marginalia_nu/src/main/resources/data/smhi/stader.csv create mode 100644 marginalia_nu/src/main/resources/dictionary/en-1000 create mode 100644 marginalia_nu/src/main/resources/dictionary/en-stopwords create mode 100644 marginalia_nu/src/main/resources/dictionary/en-words create mode 100644 marginalia_nu/src/main/resources/dictionary/latin-1000 create mode 100644 marginalia_nu/src/main/resources/dictionary/swe-1000 create mode 100644 marginalia_nu/src/main/resources/dictionary/word-frequency create mode 100644 marginalia_nu/src/main/resources/fonts/LM-regular.ttf create mode 100644 marginalia_nu/src/main/resources/fonts/STIXTwoMath-Regular.ttf create mode 100644 marginalia_nu/src/main/resources/ip-banned-cidr.txt create mode 100644 marginalia_nu/src/main/resources/log4j2.properties create mode 100644 marginalia_nu/src/main/resources/sql/data-store-init.sql create mode 100644 marginalia_nu/src/main/resources/sql/edge-crawler-cache.sql create mode 100644 marginalia_nu/src/main/resources/sql/monitor-log-init.sql create mode 100644 marginalia_nu/src/main/resources/sql/reference-data.sql create mode 100644 marginalia_nu/src/main/resources/static/dating/index.html create mode 100644 marginalia_nu/src/main/resources/static/dating/robots.txt create mode 100644 marginalia_nu/src/main/resources/static/edge/about.html create mode 100644 marginalia_nu/src/main/resources/static/edge/changelog.html create mode 100644 marginalia_nu/src/main/resources/static/edge/crawler-ips.txt create mode 100644 marginalia_nu/src/main/resources/static/edge/error.html create mode 100644 marginalia_nu/src/main/resources/static/edge/favicon.ico create mode 100644 marginalia_nu/src/main/resources/static/edge/index.html create mode 100644 marginalia_nu/src/main/resources/static/edge/known-issues.html create mode 100644 marginalia_nu/src/main/resources/static/edge/maintenance.html create mode 100644 marginalia_nu/src/main/resources/static/edge/notes.html create mode 100644 marginalia_nu/src/main/resources/static/edge/opensearch.xml create mode 100644 marginalia_nu/src/main/resources/static/edge/robots.txt create mode 100644 marginalia_nu/src/main/resources/static/edge/style-new.css create mode 100644 marginalia_nu/src/main/resources/static/edge/tts.js create mode 100644 marginalia_nu/src/main/resources/static/edge/wiki-clean.html create mode 100644 marginalia_nu/src/main/resources/static/encyclopedia/index.html create mode 100644 marginalia_nu/src/main/resources/static/encyclopedia/robots.txt create mode 100644 marginalia_nu/src/main/resources/static/encyclopedia/wiki-clean.html create mode 100644 marginalia_nu/src/main/resources/static/encyclopedia/wiki-start.html create mode 100644 marginalia_nu/src/main/resources/static/fonts/LM-bold-italic.ttf create mode 100644 marginalia_nu/src/main/resources/static/fonts/LM-bold-italic.woff create mode 100644 marginalia_nu/src/main/resources/static/fonts/LM-bold-italic.woff2 create mode 100644 marginalia_nu/src/main/resources/static/fonts/LM-bold.ttf create mode 100644 marginalia_nu/src/main/resources/static/fonts/LM-bold.woff create mode 100644 marginalia_nu/src/main/resources/static/fonts/LM-bold.woff2 create mode 100644 marginalia_nu/src/main/resources/static/fonts/LM-italic.ttf create mode 100644 marginalia_nu/src/main/resources/static/fonts/LM-italic.woff create mode 100644 marginalia_nu/src/main/resources/static/fonts/LM-italic.woff2 create mode 100644 marginalia_nu/src/main/resources/static/fonts/LM-regular.ttf create mode 100644 marginalia_nu/src/main/resources/static/fonts/LM-regular.woff create mode 100644 marginalia_nu/src/main/resources/static/fonts/LM-regular.woff2 create mode 100644 marginalia_nu/src/main/resources/static/memex/ico/dir.png create mode 100644 marginalia_nu/src/main/resources/static/memex/ico/doc32.png create mode 100644 marginalia_nu/src/main/resources/static/memex/ico/file.png create mode 100644 marginalia_nu/src/main/resources/static/memex/ico/folder32.png create mode 100644 marginalia_nu/src/main/resources/static/memex/ico/folderup16.png create mode 100644 marginalia_nu/src/main/resources/static/memex/ico/nav16.png create mode 100644 marginalia_nu/src/main/resources/static/memex/ico/pic16.png create mode 100644 marginalia_nu/src/main/resources/static/memex/ico/pic32.png create mode 100644 marginalia_nu/src/main/resources/static/memex/ico/root.png create mode 100644 marginalia_nu/src/main/resources/static/memex/ico/shiba16.png create mode 100644 marginalia_nu/src/main/resources/static/memex/ico/world16.png create mode 100644 marginalia_nu/src/main/resources/static/memex/style-new.css create mode 100644 marginalia_nu/src/main/resources/static/podcast/style.css create mode 100644 marginalia_nu/src/main/resources/static/smhi/favicon.ico create mode 100644 marginalia_nu/src/main/resources/static/smhi/font.css create mode 100644 marginalia_nu/src/main/resources/static/smhi/responsive.css create mode 100644 marginalia_nu/src/main/resources/static/smhi/style.css create mode 100644 marginalia_nu/src/main/resources/static/style.css create mode 100644 marginalia_nu/src/main/resources/templates/auth/login.hdb create mode 100644 marginalia_nu/src/main/resources/templates/dating/dating-view.hdb create mode 100644 marginalia_nu/src/main/resources/templates/edge/browse-result.hdb create mode 100644 marginalia_nu/src/main/resources/templates/edge/browse-results.hdb create mode 100644 marginalia_nu/src/main/resources/templates/edge/conversion-results-gmi.hdb create mode 100644 marginalia_nu/src/main/resources/templates/edge/conversion-results.hdb create mode 100644 marginalia_nu/src/main/resources/templates/edge/dictionary-results-gmi.hdb create mode 100644 marginalia_nu/src/main/resources/templates/edge/dictionary-results.hdb create mode 100644 marginalia_nu/src/main/resources/templates/edge/error-page.hdb create mode 100644 marginalia_nu/src/main/resources/templates/edge/parts/search-footer.hdb create mode 100644 marginalia_nu/src/main/resources/templates/edge/parts/search-form.hdb create mode 100644 marginalia_nu/src/main/resources/templates/edge/parts/search-header.hdb create mode 100644 marginalia_nu/src/main/resources/templates/edge/search-result-gmi.hdb create mode 100644 marginalia_nu/src/main/resources/templates/edge/search-result-metadata.hdb create mode 100644 marginalia_nu/src/main/resources/templates/edge/search-result.hdb create mode 100644 marginalia_nu/src/main/resources/templates/edge/search-results-gmi.hdb create mode 100644 marginalia_nu/src/main/resources/templates/edge/search-results.hdb create mode 100644 marginalia_nu/src/main/resources/templates/edge/site-info-gmi.hdb create mode 100644 marginalia_nu/src/main/resources/templates/edge/site-info.hdb create mode 100644 marginalia_nu/src/main/resources/templates/encyclopedia/wiki-error.hdb create mode 100644 marginalia_nu/src/main/resources/templates/encyclopedia/wiki-search.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/memex-create-form.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/memex-delete-form.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/memex-image.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/memex-index-feed.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/memex-index.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/memex-rename-form.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/memex-tombstone.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/memex-update-form.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/memex-upload-form.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/memex-view.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/partial/memex-backlinks-inline.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/partial/memex-backlinks.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/partial/memex-directories.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/partial/memex-documents-inline.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/partial/memex-documents.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/partial/memex-footer.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/partial/memex-head.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/partial/memex-images.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/partial/memex-task-listing.hdb create mode 100644 marginalia_nu/src/main/resources/templates/memex/partial/memex-topbar.hdb create mode 100644 marginalia_nu/src/main/resources/templates/podcast/episode.hdb create mode 100644 marginalia_nu/src/main/resources/templates/podcast/listing.hdb create mode 100644 marginalia_nu/src/main/resources/templates/podcast/new.hdb create mode 100644 marginalia_nu/src/main/resources/templates/podcast/podcast.hdb create mode 100644 marginalia_nu/src/main/resources/templates/smhi/index.hdb create mode 100644 marginalia_nu/src/main/resources/templates/smhi/prognos.hdb create mode 100644 marginalia_nu/src/main/resources/templates/status/server-status.hdb create mode 100644 marginalia_nu/src/main/resources/units.csv create mode 100644 marginalia_nu/src/test/java/EmptyTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/gemini/gmi/GemtextDatabaseTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/gemini/gmi/GemtextDocumentTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/gemini/gmi/parser/GemtextTaskParserTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/util/SeekDictionaryTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/util/TestUtil.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/util/btree/BTreeWriterTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/util/graphics/dithering/FloydSteinbergDitherTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/util/hash/LongPairHashMapTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/util/test/TestUtil.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/configuration/server/ServiceTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/data_store/AssistantTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/data_store/DataStoreServiceTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/EdgeDirectorServiceTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/archive/ArchiveTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/archive/archiver/ArchiverTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/assistant/dict/WikiCleanerTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/assistant/eval/MathParserTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/assistant/eval/UnitsTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/assistant/suggest/SuggestionsTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/CrawlJobsSpecificationSetTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerRobotsTxtTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerTest2.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/LanguageFilterTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/LinkParserTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/RssCrawlerTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/UrlsCacheTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/SentenceExtractorTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlProcessorTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlTagCleanerTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/fetcher/HttpFetcherTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/worker/IpBlockListTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/worker/UrlBlocklistTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawling/CrawlPlanLoaderTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawling/WorkLogTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/data/EdgeDataStoreDaoTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/DictionaryWriterTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/EdgeIndexClientTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/EdgeSearchTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/EdgeSearchTestLocal.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/MultimapFileTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/SearchIndexConverterTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/SearchIndexWriterTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/TokenCompressorTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/ByteFolderTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/DictionaryDataTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/DictionaryHashMapTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/PrimeUtilTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/RandomWriteFunnelTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/integration/arxiv/ArxivParserTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/integration/stackoverflow/StackOverflowPostsTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/integration/wikipedia/WikipediaTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/model/EdgeDomainTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/model/EdgeUrlTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/search/query/BodyQueryParserTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/search/query/EnglishDictionaryTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/search/query/QueryParserTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/search/query/QueryVariantsTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/MemexFileWriterTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/MemexTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/change/GemtextChangeTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/change/GemtextTaskUpdateTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/change/GemtextTombstoneUpdateCaclulatorTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/model/MemexNodeHeadingIdTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/podcasts/PodcastFetcherTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/resource_store/ResourceStoreServiceTest.java create mode 100644 marginalia_nu/src/test/java/nu/marginalia/wmsa/smhi/scraper/crawler/SmhiBackendApiTest.java create mode 100644 marginalia_nu/src/test/resources/html/monadnock.html create mode 100644 marginalia_nu/src/test/resources/log4j2.properties create mode 100644 marginalia_nu/src/test/resources/model-data.json create mode 100644 settings.gradle create mode 100644 third_party/README.md create mode 100644 third_party/build.gradle create mode 100644 third_party/src/main/java/ca/rmen/porterstemmer/PorterStemmer.java create mode 100644 third_party/src/main/java/com/github/datquocnguyen/FWObject.java create mode 100644 third_party/src/main/java/com/github/datquocnguyen/InitialTagger.java create mode 100644 third_party/src/main/java/com/github/datquocnguyen/Node.java create mode 100644 third_party/src/main/java/com/github/datquocnguyen/RDRPOSTagger.java create mode 100644 third_party/src/main/java/com/github/datquocnguyen/Utils.java create mode 100644 third_party/src/main/java/com/github/datquocnguyen/WordTag.java create mode 100644 third_party/src/main/java/com/upserve/uppend/blobs/NativeIO.java create mode 100644 third_party/src/main/java/org/openzim/ZIMTypes/ArticleEntry.java create mode 100644 third_party/src/main/java/org/openzim/ZIMTypes/DirectoryEntry.java create mode 100644 third_party/src/main/java/org/openzim/ZIMTypes/RedirectEntry.java create mode 100644 third_party/src/main/java/org/openzim/ZIMTypes/ZIMFile.java create mode 100644 third_party/src/main/java/org/openzim/ZIMTypes/ZIMReader.java create mode 100644 third_party/src/main/java/org/openzim/util/RandomAcessFileZIMInputStream.java create mode 100644 third_party/src/main/java/org/openzim/util/Utilities.java create mode 100644 third_party/src/main/java/org/tukaani/xz/BlockInputStream.java create mode 100644 third_party/src/main/java/org/tukaani/xz/BlockOutputStream.java create mode 100644 third_party/src/main/java/org/tukaani/xz/CorruptedInputException.java create mode 100644 third_party/src/main/java/org/tukaani/xz/CountingInputStream.java create mode 100644 third_party/src/main/java/org/tukaani/xz/CountingOutputStream.java create mode 100644 third_party/src/main/java/org/tukaani/xz/DeltaCoder.java create mode 100644 third_party/src/main/java/org/tukaani/xz/DeltaDecoder.java create mode 100644 third_party/src/main/java/org/tukaani/xz/DeltaInputStream.java create mode 100644 third_party/src/main/java/org/tukaani/xz/FilterCoder.java create mode 100644 third_party/src/main/java/org/tukaani/xz/FilterDecoder.java create mode 100644 third_party/src/main/java/org/tukaani/xz/FilterEncoder.java create mode 100644 third_party/src/main/java/org/tukaani/xz/FilterOptions.java create mode 100644 third_party/src/main/java/org/tukaani/xz/FinishableOutputStream.java create mode 100644 third_party/src/main/java/org/tukaani/xz/IndexIndicatorException.java create mode 100644 third_party/src/main/java/org/tukaani/xz/LZMA2Coder.java create mode 100644 third_party/src/main/java/org/tukaani/xz/LZMA2Decoder.java create mode 100644 third_party/src/main/java/org/tukaani/xz/LZMA2Encoder.java create mode 100644 third_party/src/main/java/org/tukaani/xz/LZMA2InputStream.java create mode 100644 third_party/src/main/java/org/tukaani/xz/LZMA2Options.java create mode 100644 third_party/src/main/java/org/tukaani/xz/LZMA2OutputStream.java create mode 100644 third_party/src/main/java/org/tukaani/xz/MemoryLimitException.java create mode 100644 third_party/src/main/java/org/tukaani/xz/RawCoder.java create mode 100644 third_party/src/main/java/org/tukaani/xz/SingleXZInputStream.java create mode 100644 third_party/src/main/java/org/tukaani/xz/UnsupportedOptionsException.java create mode 100644 third_party/src/main/java/org/tukaani/xz/XZ.java create mode 100644 third_party/src/main/java/org/tukaani/xz/XZFormatException.java create mode 100644 third_party/src/main/java/org/tukaani/xz/XZIOException.java create mode 100644 third_party/src/main/java/org/tukaani/xz/XZInputStream.java create mode 100644 third_party/src/main/java/org/tukaani/xz/XZOutputStream.java create mode 100644 third_party/src/main/java/org/tukaani/xz/check/CRC32.java create mode 100644 third_party/src/main/java/org/tukaani/xz/check/CRC64.java create mode 100644 third_party/src/main/java/org/tukaani/xz/check/Check.java create mode 100644 third_party/src/main/java/org/tukaani/xz/check/None.java create mode 100644 third_party/src/main/java/org/tukaani/xz/check/SHA256.java create mode 100644 third_party/src/main/java/org/tukaani/xz/common/DecoderUtil.java create mode 100644 third_party/src/main/java/org/tukaani/xz/common/EncoderUtil.java create mode 100644 third_party/src/main/java/org/tukaani/xz/common/StreamFlags.java create mode 100644 third_party/src/main/java/org/tukaani/xz/common/Util.java create mode 100644 third_party/src/main/java/org/tukaani/xz/delta/DeltaCoder.java create mode 100644 third_party/src/main/java/org/tukaani/xz/delta/DeltaDecoder.java create mode 100644 third_party/src/main/java/org/tukaani/xz/index/IndexBase.java create mode 100644 third_party/src/main/java/org/tukaani/xz/index/IndexEncoder.java create mode 100644 third_party/src/main/java/org/tukaani/xz/index/IndexHash.java create mode 100644 third_party/src/main/java/org/tukaani/xz/index/IndexRecord.java create mode 100644 third_party/src/main/java/org/tukaani/xz/lz/LZDecoder.java create mode 100644 third_party/src/main/java/org/tukaani/xz/lzma/LZMACoder.java create mode 100644 third_party/src/main/java/org/tukaani/xz/lzma/LZMADecoder.java create mode 100644 third_party/src/main/java/org/tukaani/xz/lzma/State.java create mode 100644 third_party/src/main/java/org/tukaani/xz/package-info.java create mode 100644 third_party/src/main/java/org/tukaani/xz/rangecoder/RangeCoder.java create mode 100644 third_party/src/main/java/org/tukaani/xz/rangecoder/RangeDecoder.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8b763f78 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.class +build/ +*~ diff --git a/README.md b/README.md new file mode 100644 index 00000000..098a6a57 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# marginalia.nu + +This is the source code for marginalia.nu, including the search engine, +the MEMEX/gemini server, the and the encyclopedia service. + +As it stands now, the project is a bit of a mess as it wasn't developed +with the intention of going open source, a lot of tests and so on make +assumptions about the directory structure, much configuration is hard coded +and so on. It's a work in progress. \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..b25c2872 --- /dev/null +++ b/build.gradle @@ -0,0 +1,74 @@ +plugins { + id 'java' + + id 'com.github.johnrengelman.shadow' version '6.0.0' +} + +group 'nu.marginalia' +version 'SNAPSHOT' + +compileJava.options.encoding = "UTF-8" +compileTestJava.options.encoding = "UTF-8" +repositories { + mavenLocal() + maven { url "https://artifactory.cronapp.io/public-release/" } + maven { url "https://repo1.maven.org/maven2/" } + maven { url "https://www2.ph.ed.ac.uk/maven2/" } + maven { url "https://jitpack.io/" } + exclusiveContent { + forRepository { + maven { + url = uri("https://jitpack.io") + } + } + filter { + // Only use JitPack for the `gson-record-type-adapter-factory` library + includeModule("com.github.Marcono1234", "gson-record-type-adapter-factory") + } + } +} + +shadowJar { +} +jar { + manifest { + attributes 'Main-Class': "nu.marginalia.wmsa.configuration.ServiceDescriptor" + } + from { + configurations.shadow.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +dependencies { + implementation project(':marginalia_nu') +} +task version() { // +} + +test { + maxParallelForks = 16 + forkEvery = 1 + maxHeapSize = "8G" + useJUnitPlatform { + excludeTags "db" + excludeTags "nobuild" + } +} + +task dbTest(type: Test) { + maxParallelForks = 1 + forkEvery = 1 + maxHeapSize = "8G" + + useJUnitPlatform { + includeTags "db" + } +} + + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..41d9927a4d4fb3f96a785543079b8df6723c946b GIT binary patch literal 59821 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..41dfb879 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..1b6c7873 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/marginalia_nu/build.gradle b/marginalia_nu/build.gradle new file mode 100644 index 00000000..dd6e06eb --- /dev/null +++ b/marginalia_nu/build.gradle @@ -0,0 +1,133 @@ +plugins { + id 'java' + id "io.freefair.lombok" version "5.3.3.3" + + id "me.champeau.jmh" version "0.6.6" +} + +repositories { + mavenLocal() + maven { url "https://artifactory.cronapp.io/public-release/" } + maven { url "https://repo1.maven.org/maven2/" } + maven { url "https://www2.ph.ed.ac.uk/maven2/" } + maven { url "https://jitpack.io/" } + exclusiveContent { + forRepository { + maven { + url = uri("https://jitpack.io") + } + } + filter { + // Only use JitPack for the `gson-record-type-adapter-factory` library + includeModule("com.github.Marcono1234", "gson-record-type-adapter-factory") + } + } +} + +dependencies { + implementation project(':third_party') + + implementation 'junit:junit:4.13.2' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' + + implementation 'org.projectlombok:lombok:1.18.22' + annotationProcessor 'org.projectlombok:lombok:1.18.22' + + testCompileOnly 'org.projectlombok:lombok:1.18.22' + testImplementation 'org.projectlombok:lombok:1.18.22' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.22' + + implementation 'com.github.jknack:handlebars:4.3.0' + implementation 'com.github.jknack:handlebars-markdown:4.2.1' + + implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.0' + implementation 'io.reactivex.rxjava3:rxjava:3.1.4' + implementation "com.sparkjava:spark-core:2.9.3" + implementation 'com.opencsv:opencsv:5.6' + + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.17.1' + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.17.1' + implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.17.1' + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.17.1' + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.17.1' + implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.17.1' + + implementation 'org.slf4j:slf4j-api:1.7.36' + + implementation 'com.google.guava:guava:31.1-jre' + implementation 'com.google.inject:guice:5.1.0' + implementation 'com.github.jnr:jnr-ffi:2.1.1' + implementation 'org.apache.httpcomponents:httpcore:4.4.15' + implementation 'org.apache.httpcomponents:httpclient:4.5.13' + implementation 'com.github.ThatJavaNerd:JRAW:1.1.0' + + implementation group: 'com.h2database', name: 'h2', version: '2.1.210' + testImplementation group: 'org.mockito', name: 'mockito-core', version: '4.3.1' + + implementation 'org.jsoup:jsoup:1.14.3' + implementation group: 'com.github.crawler-commons', name: 'crawler-commons', version: '1.2' + + implementation 'org.mariadb.jdbc:mariadb-java-client:3.0.3' + implementation group: 'net.sf.trove4j', name: 'trove4j', version: '3.0.3' + + implementation 'com.zaxxer:HikariCP:5.0.1' + + implementation 'org.apache.opennlp:opennlp-tools:1.9.4' + implementation 'io.prometheus:simpleclient:0.15.0' + implementation 'io.prometheus:simpleclient_servlet:0.15.0' + implementation 'io.prometheus:simpleclient_httpserver:0.15.0' + implementation 'io.prometheus:simpleclient_hotspot:0.15.0' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2.1' + implementation 'org.apache.opennlp:opennlp-tools:1.9.4' + implementation 'io.prometheus:simpleclient:0.15.0' + implementation 'io.prometheus:simpleclient_servlet:0.15.0' + implementation 'io.prometheus:simpleclient_httpserver:0.15.0' + implementation 'io.prometheus:simpleclient_hotspot:0.15.0' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2.1' + + implementation group: 'org.yaml', name: 'snakeyaml', version: '1.30' + + implementation 'com.syncthemall:boilerpipe:1.2.2' + implementation 'com.github.luben:zstd-jni:1.5.2-2' + implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.3.0' + implementation 'de.rototor.jeuclid:jeuclid-core:3.1.14' + + implementation 'org.imgscalr:imgscalr-lib:4.2' + implementation 'org.jclarion:image4j:0.7' + + implementation 'commons-net:commons-net:3.6' + implementation 'org.eclipse.jgit:org.eclipse.jgit:5.12.0.202106070339-r' + implementation 'org.eclipse.jgit:org.eclipse.jgit.ssh.jsch:5.12.0.202106070339-r' + implementation 'com.jcraft:jsch:0.1.55' + + implementation group: 'org.apache.commons', name: 'commons-compress', version: '1.21' + implementation 'edu.stanford.nlp:stanford-corenlp:4.4.0' + + implementation group: 'it.unimi.dsi', name: 'fastutil', version: '8.5.8' + implementation 'org.roaringbitmap:RoaringBitmap:[0.6,)' + implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.29' + + implementation 'com.github.Marcono1234:gson-record-type-adapter-factory:0.2.0' +} + +test { + maxParallelForks = 16 + forkEvery = 1 + maxHeapSize = "8G" + useJUnitPlatform { + excludeTags "db" + } +} + +task dbTest(type: Test) { + maxParallelForks = 1 + forkEvery = 1 + maxHeapSize = "8G" + + useJUnitPlatform { + includeTags "db" + } +} + + diff --git a/marginalia_nu/lombok.config b/marginalia_nu/lombok.config new file mode 100644 index 00000000..6aa51d71 --- /dev/null +++ b/marginalia_nu/lombok.config @@ -0,0 +1,2 @@ +# This file is generated by the 'io.freefair.lombok' Gradle plugin +config.stopBubbling = true diff --git a/marginalia_nu/src/jmh/java/bs_vs_ls/BinSearchVsLinSearch.java b/marginalia_nu/src/jmh/java/bs_vs_ls/BinSearchVsLinSearch.java new file mode 100644 index 00000000..c006838d --- /dev/null +++ b/marginalia_nu/src/jmh/java/bs_vs_ls/BinSearchVsLinSearch.java @@ -0,0 +1,37 @@ +package bs_vs_ls; + +import org.openjdk.jmh.annotations.*; + +import java.util.Arrays; +import java.util.stream.LongStream; + +public class BinSearchVsLinSearch { + static long[] data = LongStream.generate(() -> (long) (Long.MAX_VALUE * Math.random())).limit(512).sorted().toArray(); + + @State(Scope.Thread) + public static class Target { + long targetValue = 0; + + @Setup(Level.Invocation) + public void setUp() { + targetValue = data[(int)(data.length * Math.random())]; + } + + } + +// @Benchmark + public long testBs(Target t) { + return Arrays.binarySearch(data, t.targetValue); + } + +// @Benchmark + public long testLs(Target t) { + for (int i = 0; i < 512; i++) { + if (data[i] > t.targetValue) + break; + else if (data[i] == t.targetValue) + return i; + } + return -1; + } +} diff --git a/marginalia_nu/src/jmh/java/bs_vs_ls/BinSearchVsLinSearch2.java b/marginalia_nu/src/jmh/java/bs_vs_ls/BinSearchVsLinSearch2.java new file mode 100644 index 00000000..c449dd97 --- /dev/null +++ b/marginalia_nu/src/jmh/java/bs_vs_ls/BinSearchVsLinSearch2.java @@ -0,0 +1,68 @@ +package bs_vs_ls; + +import nu.marginalia.util.multimap.MultimapFileLong; +import nu.marginalia.util.multimap.MultimapSearcher; +import org.openjdk.jmh.annotations.*; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.LongStream; + +public class BinSearchVsLinSearch2 { + static long[] data = LongStream.generate(() -> (long) (Long.MAX_VALUE * Math.random())).limit(512).sorted().toArray(); + + @State(Scope.Benchmark) + public static class Target { + Path tf; + MultimapFileLong file; + MultimapSearcher searcher; + long[] data = new long[512]; + + { + try { + tf = Files.createTempFile("tmpFileIOTest", "dat"); + file = MultimapFileLong.forOutput(tf, 1024); + searcher = file.createSearcher(); + for (int i = 0; i < 65535; i++) { + file.put(i, i); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Measurement(iterations = 1) + @Warmup(iterations = 1) + @Benchmark + public long testLs(Target t) { + int target = (int)(4096 + 512 * Math.random()); + for (int i = 4096; i < (4096+512); i++) { + long val = t.file.get(i); + if (val > target) + break; + if (val == target) + return val; + } + return -1; + } + + @Measurement(iterations = 1) + @Warmup(iterations = 1) + @Benchmark + public long testLs2(Target t) { + int target = (int)(4096 + 512 * Math.random()); + + t.file.read(t.data, 4096); + for (int i = 0; i < (512); i++) { + long val = t.file.get(i); + if (val > target) + break; + if (val == target) + return val; + } + return -1; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/BadBotList.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/BadBotList.java new file mode 100644 index 00000000..36961ee0 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/BadBotList.java @@ -0,0 +1,43 @@ +package nu.marginalia.gemini; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetAddress; +import java.util.HashSet; +import java.util.Set; + +public class BadBotList { + private final Set shitlist = new HashSet<>(); + public static BadBotList INSTANCE = new BadBotList(); + private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName()); + + private BadBotList() {} + + public boolean isAllowed(InetAddress address) { + return !shitlist.contains(address); + } + + public boolean isQueryPermitted(InetAddress address, String query) { + if (isBadQuery(query)) { + logger.info("Banning {}", address); + shitlist.add(address); + return false; + } + return true; + } + + private boolean isBadQuery(String query) { + if (query.startsWith("GET")) { + return true; + } + if (query.startsWith("OPTIONS")) { + return true; + } + if (query.contains("mstshash")) { + return true; + } + + return false; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/GeminiConfigurationModule.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/GeminiConfigurationModule.java new file mode 100644 index 00000000..f9ce793a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/GeminiConfigurationModule.java @@ -0,0 +1,21 @@ +package nu.marginalia.gemini; + +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.name.Named; +import com.google.inject.name.Names; +import nu.marginalia.wmsa.memex.system.MemexFileWriter; + +import java.nio.file.Path; + +public class GeminiConfigurationModule extends AbstractModule { + public void configure() { + bind(Path.class).annotatedWith(Names.named("gemini-server-root")).toInstance(Path.of("/var/lib/wmsa/memex-gmi")); + bind(Path.class).annotatedWith(Names.named("gemini-cert-file")).toInstance(Path.of("/var/lib/wmsa/gemini/crypto.jks")); + bind(Path.class).annotatedWith(Names.named("gemini-cert-password-file")).toInstance(Path.of("/var/lib/wmsa/gemini/password.dat")); + bind(Integer.class).annotatedWith(Names.named("gemini-server-port")).toInstance(1965); + + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/GeminiService.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/GeminiService.java new file mode 100644 index 00000000..ffad695b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/GeminiService.java @@ -0,0 +1,164 @@ +package nu.marginalia.gemini; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import nu.marginalia.gemini.io.GeminiConnection; +import nu.marginalia.gemini.io.GeminiSSLSetUp; +import nu.marginalia.gemini.io.GeminiStatusCode; +import nu.marginalia.gemini.io.GeminiUserException; +import nu.marginalia.gemini.plugins.BareStaticPagePlugin; +import nu.marginalia.gemini.plugins.Plugin; +import nu.marginalia.gemini.plugins.SearchPlugin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSocket; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +@Singleton +public class GeminiService { + + public static final String DEFAULT_FILENAME = "index.gmi"; + public final Path serverRoot; + + private final Logger logger = LoggerFactory.getLogger("GeminiServer"); + private final Executor pool = Executors.newFixedThreadPool(32); + private final SSLServerSocket serverSocket; + + private final Plugin[] plugins; + private final BadBotList badBotList = BadBotList.INSTANCE; + + @Inject + public GeminiService(@Named("gemini-server-root") Path serverRoot, + @Named("gemini-server-port") Integer port, + GeminiSSLSetUp sslSetUp, + BareStaticPagePlugin pagePlugin, + SearchPlugin searchPlugin) throws Exception { + this.serverRoot = serverRoot; + logger.info("Setting up crypto"); + final SSLServerSocketFactory socketFactory = sslSetUp.getServerSocketFactory(); + + serverSocket = (SSLServerSocket) socketFactory.createServerSocket(port /* 1965 */); + serverSocket.setEnabledCipherSuites(socketFactory.getSupportedCipherSuites()); + serverSocket.setEnabledProtocols(new String[] {"TLSv1.3", "TLSv1.2"}); + + logger.info("Verifying setup"); + if (!Files.exists(this.serverRoot)) { + logger.error("Could not find SERVER_ROOT {}", this.serverRoot); + System.exit(255); + } + + plugins = new Plugin[] { + pagePlugin, + searchPlugin + }; + } + + public void run() { + logger.info("Awaiting connections"); + + try { + for (; ; ) { + SSLSocket connection = (SSLSocket) serverSocket.accept(); + connection.setSoTimeout(10_000); + + if (!badBotList.isAllowed(connection.getInetAddress())) { + connection.close(); + } else { + pool.execute(() -> serve(connection)); + } + } + } + catch (IOException ex) { + logger.error("IO Exception in gemini server", ex); + } + } + + private void serve(SSLSocket socket) { + final GeminiConnection connection; + try { + connection = new GeminiConnection(socket); + } + catch (IOException ex) { + logger.error("Failed to create connection object", ex); + return; + } + + try { + handleRequest(connection); + } + catch (GeminiUserException ex) { + errorResponse(connection, ex.getMessage()); + } + catch (SSLException ex) { + logger.error(connection.getAddress() + " SSL error"); + connection.close(); + } + catch (Exception ex) { + errorResponse(connection, "Error"); + logger.error(connection.getAddress(), ex); + } + finally { + connection.close(); + } + } + + private void errorResponse(GeminiConnection connection, String message) { + if (connection.isConnected()) { + try { + logger.error("=> " + connection.getAddress(), message); + connection.writeStatusLine(GeminiStatusCode.ERROR_PERMANENT, message); + } + catch (IOException ex) { + logger.error("Exception while sending error", ex); + } + } + } + + private void handleRequest(GeminiConnection connection) throws Exception { + + final String address = connection.getAddress(); + logger.info("Connect: " + address); + + final Optional maybeUri = connection.readUrl(); + if (maybeUri.isEmpty()) { + logger.info("Done: {}", address); + return; + } + + final URI uri = maybeUri.get(); + logger.info("Request {}", uri); + + if (!uri.getScheme().equals("gemini")) { + throw new GeminiUserException("Unsupported protocol"); + } + + servePage(connection, uri); + logger.info("Done: {}", address); + } + + private void servePage(GeminiConnection connection, URI url) throws IOException { + String path = url.getPath(); + + for (Plugin p : plugins) { + if (p.serve(url, connection)) { + return; + } + } + + logger.error("FileNotFound {}", path); + connection.writeStatusLine(GeminiStatusCode.ERROR_TEMPORARY, "No such file"); + } + + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/client/GeminiClient.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/client/GeminiClient.java new file mode 100644 index 00000000..e306e88f --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/client/GeminiClient.java @@ -0,0 +1,130 @@ +package nu.marginalia.gemini.client; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; + +/** Unstable code! */ +public class GeminiClient { + + private final SSLSocketFactory socketFactory; + + // Create a trust manager that does not validate anything + public static final TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, + String authType) { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, + String authType) { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } + }; + + + public static SSLSocketFactory buildSocketFactory() throws Exception { + // Install the all-trusting trust manager + final SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); + + return sslContext.getSocketFactory(); + } + + public GeminiClient() throws Exception { + socketFactory = buildSocketFactory(); + } + + public Response get(URI uri) throws IOException { + + final int port = uri.getPort() == -1 ? 1965 : uri.getPort(); + final String host = uri.getHost(); + var requestString = String.format("%s\r\n", uri).getBytes(StandardCharsets.UTF_8); + + try (var socket = socketFactory.createSocket(host, port)) { + socket.setSoTimeout(10_000); + socket.getOutputStream().write(requestString); + + var is = socket.getInputStream(); + String statusLine = new GeminiInput(is).get(); + + int code = Integer.parseInt(statusLine.substring(0,2)); + String meta = statusLine.substring(3); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + is.transferTo(baos); + + return new Response(code, meta, baos.toByteArray()); + } + + } + + public static class Response { + public final int code; + public final String meta; + public final byte[] data; + + Response(int code, String meta, byte[] data) { + this.code = code; + this.meta = meta; + this.data = data; + } + } + + + public static class GeminiInput { + private final InputStream is; + private final byte[] buffer = new byte[1024]; + private int idx; + + final String result; + + public GeminiInput(InputStream is) throws IOException { + this.is = is; + + for (idx = 0; idx < buffer.length; idx++) { + if (hasEndOfLine()) { + result = new String(buffer, 0, idx-2, StandardCharsets.UTF_8); + return; + } + + readCharacter(); + } + + throw new RuntimeException("String too long"); + } + + public String get() { + return result; + } + + private void readCharacter() throws IOException { + int rb = is.read(); + if (-1 == rb) { + throw new RuntimeException("URL incomplete (no CR LF)"); + } + buffer[idx] = (byte) rb; + } + + public boolean hasEndOfLine() { + return idx > 2 + && buffer[idx - 1] == (byte) '\n' + && buffer[idx - 2] == (byte) '\r'; + } + + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/Gemtext.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/Gemtext.java new file mode 100644 index 00000000..3b07f4cc --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/Gemtext.java @@ -0,0 +1,53 @@ +package nu.marginalia.gemini.gmi; + +import lombok.Getter; +import nu.marginalia.gemini.gmi.line.AbstractGemtextLine; +import nu.marginalia.gemini.gmi.parser.GemtextParser; +import nu.marginalia.gemini.gmi.renderer.GemtextRenderer; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; + +import java.io.IOException; +import java.io.Writer; +import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +@Getter +public class Gemtext { + private final AbstractGemtextLine[] lines; + private final MemexNodeUrl url; + + public Gemtext(MemexNodeUrl url, String[] lines, MemexNodeHeadingId headingRoot) { + this.lines = GemtextParser.parse(lines, headingRoot); + this.url = url; + } + public Gemtext(MemexNodeUrl url, String[] lines) { + this.lines = GemtextParser.parse(lines, new MemexNodeHeadingId(0)); + this.url = url; + } + + public String render(GemtextRenderer renderer) { + return Arrays.stream(lines).map(renderer::renderLine).collect(Collectors.joining()); + } + + public void render(GemtextRenderer renderer, Writer w) throws IOException { + for (var line : lines) { + w.write(renderer.renderLine(line)); + w.write('\n'); + } + } + + public Stream stream() { + return Arrays.stream(lines); + } + + public AbstractGemtextLine get(int idx) { + return lines[idx]; + } + public int size() { + return lines.length; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/GemtextDatabase.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/GemtextDatabase.java new file mode 100644 index 00000000..c86ce518 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/GemtextDatabase.java @@ -0,0 +1,72 @@ +package nu.marginalia.gemini.gmi; + +import com.google.common.collect.Sets; +import nu.marginalia.gemini.gmi.line.GemtextLineVisitorAdapter; +import nu.marginalia.gemini.gmi.line.GemtextLink; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import nu.marginalia.wmsa.memex.model.MemexUrl; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +public class GemtextDatabase extends Gemtext { + public Map links; + + public GemtextDatabase(MemexNodeUrl url, String[] lines) { + super(url, lines); + + links = new HashMap<>(); + for (int i = 0; i < size(); i++) { + int linkIdx = i; + + get(i).visit(new GemtextLineVisitorAdapter<>() { + @Override + public Object visit(GemtextLink g) { + links.put(g.getUrl().toString(), linkIdx); + return null; + } + }); + } + } + + public Set keys() { + return links.keySet(); + } + + public Optional getLinkData(MemexUrl url) { + Integer idx = links.get(url.getUrl()); + if (idx != null) { + return + Optional.of(get(idx).mapLink(GemtextLink::getTitle).orElse("")); + } + return Optional.empty(); + } + + + public static GemtextDatabase of(MemexNodeUrl url, String[] lines) { + return new GemtextDatabase(url, lines); + } + + public static GemtextDatabase of(MemexNodeUrl url, Path file) throws IOException { + try (var s = Files.lines(file)) { + return new GemtextDatabase(url, s.toArray(String[]::new)); + } + } + + public Set difference(GemtextDatabase other) { + Set differences = new HashSet<>(); + + Sets.difference(keys(), other.keys()).stream().map(MemexNodeUrl::new).forEach(differences::add); + + Sets.intersection(keys(), other.keys()) + .stream() + .map(MemexNodeUrl::new) + .filter(url -> !Objects.equals(getLinkData(url), other.getLinkData(url))) + .forEach(differences::add); + + return differences; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/GemtextDocument.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/GemtextDocument.java new file mode 100644 index 00000000..6f6bc40f --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/GemtextDocument.java @@ -0,0 +1,163 @@ +package nu.marginalia.gemini.gmi; + +import lombok.Getter; +import nu.marginalia.gemini.gmi.line.*; +import nu.marginalia.gemini.gmi.renderer.GemtextRenderer; +import nu.marginalia.gemini.gmi.renderer.GemtextRendererFactory; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import nu.marginalia.wmsa.memex.model.MemexNodeTaskId; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import nu.marginalia.wmsa.memex.model.MemexTaskState; +import org.apache.commons.lang3.tuple.Pair; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Getter +public class GemtextDocument extends Gemtext { + private final Map headings; + private final Map> headingsByName; + private final Set pragmas; + private final List tasks; + + private final String title; + private final String date; + private final List links; + private final int hashCode; + + private static final Pattern datePattern = Pattern.compile(".*(\\d{4}-\\d{2}-\\d{2}).*"); + private static final GemtextRenderer rawRenderer = new GemtextRendererFactory().gemtextRendererAsIs(); + + public GemtextDocument(MemexNodeUrl url, String[] lines, MemexNodeHeadingId headingRoot) { + super(url, lines, headingRoot); + + this.hashCode = Arrays.hashCode(lines); + + GemtextDataExtractor extractor = new GemtextDataExtractor(); + + Arrays.stream(this.getLines()).forEach(extractor::take); + + this.headings = extractor.getHeadings(); + this.links = extractor.getLinks(); + this.title = Objects.requireNonNullElse(extractor.getTitle(), url.getUrl()); + this.pragmas = extractor.getPragmas(); + this.headingsByName = extractor.getHeadingsByName(); + this.tasks = extractor.getTasks(); + this.date = extractor.getDate(); + } + + public String getHeadingForElement(AbstractGemtextLine line) { + return headings.getOrDefault(line.getHeading(), ""); + } + + public List getSection(MemexNodeHeadingId headingId) { + return stream() + .filter(line -> line.getHeading().isChildOf(headingId)) + .collect(Collectors.toList()); + } + + public String getSectionGemtext(MemexNodeHeadingId headingId) { + if (headingId.equals(new MemexNodeHeadingId(0))) { + return stream() + .map(rawRenderer::renderLine) + .collect(Collectors.joining("\n")); + } + + return stream() + .filter(line -> line.getHeading().isChildOf(headingId)) + .map(rawRenderer::renderLine) + .collect(Collectors.joining("\n")); + } + + public Map> getOpenTopTasks() { + return tasks.stream() + .filter(task -> MemexTaskState.TODO.equals(task.getState()) + || MemexTaskState.URGENT.equals(task.getState())) + .filter(task -> task.getId().level() == 1) + .collect(Collectors.toMap(GemtextTask::getId, task -> Pair.of(task.getTask(), task.getState()))); + } + + public static GemtextDocument of(MemexNodeUrl url, String... lines) { + return new GemtextDocument(url, lines, new MemexNodeHeadingId(0)); + } + + public static GemtextDocument of(MemexNodeUrl url, Path file) throws IOException { + try (var s = Files.lines(file)) { + return new GemtextDocument(url, s.toArray(String[]::new), new MemexNodeHeadingId(0)); + } + } + + public boolean isIndex() { + return getUrl().getFilename().equals("index.gmi"); + } + + @Override + public int hashCode() { + return hashCode; + } + + public Optional getHeading(MemexNodeHeadingId heading) { + return Optional.ofNullable(headings.get(heading)); + } + + public Optional getHeadingByName(MemexNodeHeadingId parent, String name) { + var headings = headingsByName.get(name); + if (null == headings) { + return Optional.empty(); + } + return headings.stream().filter(heading -> heading.isChildOf(parent)).findAny(); + } + + @Getter + private static class GemtextDataExtractor extends GemtextLineVisitorAdapter { + + private String title; + private String date; + private final Map headings = new TreeMap<>((a, b) -> Arrays.compare(a.getIds(), b.getIds())); + private final Map> headingsByName = new HashMap<>(); + private final Set pragmas = new HashSet<>(); + private final List links = new ArrayList<>(); + private final List tasks = new ArrayList<>(); + + @Override + public Object visit(GemtextHeading g) { + headings.put(g.getLevel(), g.getName()); + headingsByName.computeIfAbsent(g.getName(), t -> new ArrayList<>()).add(g.getLevel()); + + if (title == null) { + title = g.getName(); + var dateMatcher = datePattern.matcher(title); + if (dateMatcher.matches()) { + date = dateMatcher.group(1); + } + } + + return null; + } + + @Override + public Object visit(GemtextLink g) { + links.add(g); + + return null; + } + + @Override + public Object visit(GemtextTask g) { + tasks.add(g); + + return null; + } + + @Override + public Object visit(GemtextPragma g) { + pragmas.add(g.getLine()); + + return null; + } + }; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/AbstractGemtextLine.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/AbstractGemtextLine.java new file mode 100644 index 00000000..f1307b9b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/AbstractGemtextLine.java @@ -0,0 +1,18 @@ +package nu.marginalia.gemini.gmi.line; + +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; + +import java.util.Optional; +import java.util.function.Function; + +public abstract class AbstractGemtextLine { + public Optional mapLink(Function mapper) { + return Optional.empty(); + } + public Optional mapHeading(Function mapper) { return Optional.empty(); } + public Optional mapTask(Function mapper) { return Optional.empty(); } + public abstract T visit(GemtextLineVisitor visitor); + + public abstract boolean breaksTask(); + public abstract MemexNodeHeadingId getHeading(); +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextAside.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextAside.java new file mode 100644 index 00000000..ef73accc --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextAside.java @@ -0,0 +1,21 @@ +package nu.marginalia.gemini.gmi.line; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; + +@AllArgsConstructor @Getter @ToString +public class GemtextAside extends AbstractGemtextLine { + private final String line; + private final MemexNodeHeadingId heading; + + @Override + public T visit(GemtextLineVisitor visitor) { + return visitor.visit(this); + } + + public boolean breaksTask() { + return false; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextHeading.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextHeading.java new file mode 100644 index 00000000..a2c9f309 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextHeading.java @@ -0,0 +1,32 @@ +package nu.marginalia.gemini.gmi.line; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; + +import java.util.Optional; +import java.util.function.Function; + +@AllArgsConstructor +@Getter +@ToString +public class GemtextHeading extends AbstractGemtextLine { + private final MemexNodeHeadingId level; + private final String name; + private final MemexNodeHeadingId heading; + + public Optional mapHeading(Function mapper) { + return Optional.of(mapper.apply(this)); + } + + @Override + public T visit(GemtextLineVisitor visitor) { + return visitor.visit(this); + } + + public boolean breaksTask() { + return true; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextLineVisitor.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextLineVisitor.java new file mode 100644 index 00000000..219267ca --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextLineVisitor.java @@ -0,0 +1,18 @@ +package nu.marginalia.gemini.gmi.line; + +public interface GemtextLineVisitor { + default T take(AbstractGemtextLine line) { + return line.visit(this); + } + + T visit(GemtextHeading g); + T visit(GemtextLink g); + T visit(GemtextList g); + T visit(GemtextPreformat g); + T visit(GemtextQuote g); + T visit(GemtextText g); + T visit(GemtextTextLiteral g); + T visit(GemtextAside g); + T visit(GemtextTask g); + T visit(GemtextPragma g); +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextLineVisitorAdapter.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextLineVisitorAdapter.java new file mode 100644 index 00000000..cb0a7544 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextLineVisitorAdapter.java @@ -0,0 +1,53 @@ +package nu.marginalia.gemini.gmi.line; + +public class GemtextLineVisitorAdapter implements GemtextLineVisitor { + @Override + public T visit(GemtextHeading g) { + return null; + } + + @Override + public T visit(GemtextLink g) { + return null; + } + + @Override + public T visit(GemtextList g) { + return null; + } + + @Override + public T visit(GemtextPreformat g) { + return null; + } + + @Override + public T visit(GemtextQuote g) { + return null; + } + + @Override + public T visit(GemtextText g) { + return null; + } + + @Override + public T visit(GemtextTextLiteral g) { + return null; + } + + @Override + public T visit(GemtextAside g) { + return null; + } + + @Override + public T visit(GemtextTask g) { + return null; + } + + @Override + public T visit(GemtextPragma g) { + return null; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextLink.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextLink.java new file mode 100644 index 00000000..27aa1a5c --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextLink.java @@ -0,0 +1,33 @@ +package nu.marginalia.gemini.gmi.line; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import nu.marginalia.wmsa.memex.model.MemexUrl; + +import javax.annotation.Nullable; +import java.util.Optional; +import java.util.function.Function; + +@AllArgsConstructor @Getter @ToString +public class GemtextLink extends AbstractGemtextLine { + private final MemexUrl url; + + @Nullable + private final String title; + private final MemexNodeHeadingId heading; + + public Optional mapLink(Function mapper) { + return Optional.ofNullable(mapper.apply(this)); + } + + @Override + public T visit(GemtextLineVisitor visitor) { + return visitor.visit(this); + } + + public boolean breaksTask() { + return false; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextList.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextList.java new file mode 100644 index 00000000..c06c1e6a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextList.java @@ -0,0 +1,23 @@ +package nu.marginalia.gemini.gmi.line; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; + +import java.util.List; + +@AllArgsConstructor @Getter @ToString +public class GemtextList extends AbstractGemtextLine { + private final List items; + private final MemexNodeHeadingId heading; + + @Override + public T visit(GemtextLineVisitor visitor) { + return visitor.visit(this); + } + + public boolean breaksTask() { + return true; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextPragma.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextPragma.java new file mode 100644 index 00000000..082cef26 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextPragma.java @@ -0,0 +1,21 @@ +package nu.marginalia.gemini.gmi.line; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; + +@AllArgsConstructor @Getter @ToString +public class GemtextPragma extends AbstractGemtextLine { + private final String line; + private final MemexNodeHeadingId heading; + + @Override + public T visit(GemtextLineVisitor visitor) { + return visitor.visit(this); + } + + public boolean breaksTask() { + return false; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextPreformat.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextPreformat.java new file mode 100644 index 00000000..56a1f196 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextPreformat.java @@ -0,0 +1,23 @@ +package nu.marginalia.gemini.gmi.line; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; + +import java.util.List; + +@AllArgsConstructor @Getter @ToString +public class GemtextPreformat extends AbstractGemtextLine { + private final List items; + private final MemexNodeHeadingId heading; + + @Override + public T visit(GemtextLineVisitor visitor) { + return visitor.visit(this); + } + + public boolean breaksTask() { + return true; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextQuote.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextQuote.java new file mode 100644 index 00000000..ad9f2e9b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextQuote.java @@ -0,0 +1,23 @@ +package nu.marginalia.gemini.gmi.line; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; + +import java.util.List; + +@AllArgsConstructor @Getter @ToString +public class GemtextQuote extends AbstractGemtextLine { + private final List items; + private final MemexNodeHeadingId heading; + + @Override + public T visit(GemtextLineVisitor visitor) { + return visitor.visit(this); + } + + public boolean breaksTask() { + return true; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextTask.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextTask.java new file mode 100644 index 00000000..d2360afc --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextTask.java @@ -0,0 +1,42 @@ +package nu.marginalia.gemini.gmi.line; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import nu.marginalia.wmsa.memex.model.MemexNodeTaskId; +import nu.marginalia.wmsa.memex.model.MemexTaskState; +import nu.marginalia.wmsa.memex.model.MemexTaskTags; + +import java.util.Optional; +import java.util.function.Function; + +@AllArgsConstructor @Getter @ToString +public class GemtextTask extends AbstractGemtextLine { + private final MemexNodeTaskId id; + private final String task; + private final MemexNodeHeadingId heading; + private final MemexTaskTags tags; + + public MemexTaskState getState() { + return MemexTaskState.of(tags); + } + + public int getLevel() { + return id.level(); + } + @Override + public T visit(GemtextLineVisitor visitor) { + return visitor.visit(this); + } + + @Override + public boolean breaksTask() { + return true; + } + + @Override + public Optional mapTask(Function mapper) { + return Optional.of(mapper.apply(this)); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextText.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextText.java new file mode 100644 index 00000000..15394533 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextText.java @@ -0,0 +1,21 @@ +package nu.marginalia.gemini.gmi.line; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; + +@AllArgsConstructor @Getter @ToString +public class GemtextText extends AbstractGemtextLine { + private final String line; + private final MemexNodeHeadingId heading; + + @Override + public T visit(GemtextLineVisitor visitor) { + return visitor.visit(this); + } + + public boolean breaksTask() { + return !line.isBlank(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextTextLiteral.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextTextLiteral.java new file mode 100644 index 00000000..7e44702f --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/line/GemtextTextLiteral.java @@ -0,0 +1,23 @@ +package nu.marginalia.gemini.gmi.line; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; + +import java.util.List; + +@AllArgsConstructor @Getter @ToString +public class GemtextTextLiteral extends AbstractGemtextLine { + private final List items; + private final MemexNodeHeadingId heading; + + @Override + public T visit(GemtextLineVisitor visitor) { + return visitor.visit(this); + } + + public boolean breaksTask() { + return false; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextAsideParser.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextAsideParser.java new file mode 100644 index 00000000..541ada0c --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextAsideParser.java @@ -0,0 +1,20 @@ +package nu.marginalia.gemini.gmi.parser; + +import nu.marginalia.gemini.gmi.line.GemtextAside; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; + +import java.util.regex.Pattern; + +public class GemtextAsideParser { + private static final Pattern listItemPattern = Pattern.compile("^\\((.*)\\)$"); + + public static GemtextAside parse(String s, MemexNodeHeadingId heading) { + var matcher = listItemPattern.matcher(s); + + if (!matcher.matches()) { + return null; + } + + return new GemtextAside(matcher.group(1), heading); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextHeadingParser.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextHeadingParser.java new file mode 100644 index 00000000..c91d2a45 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextHeadingParser.java @@ -0,0 +1,26 @@ +package nu.marginalia.gemini.gmi.parser; + +import nu.marginalia.gemini.gmi.line.AbstractGemtextLine; +import nu.marginalia.gemini.gmi.line.GemtextHeading; +import nu.marginalia.gemini.gmi.line.GemtextText; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; + +import java.util.regex.Pattern; + +public class GemtextHeadingParser { + private static final Pattern headingPattern = Pattern.compile("^(#+)\\s*([^#].*|$)$"); + + public static AbstractGemtextLine parse(String s, MemexNodeHeadingId heading) { + var matcher = headingPattern.matcher(s); + + if (!matcher.matches()) { + return new GemtextText(s, heading); + } + + int level = matcher.group(1).length() - 1; + var newHeading = heading.next(level); + + return new GemtextHeading(newHeading, matcher.group(2), newHeading); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextLinkParser.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextLinkParser.java new file mode 100644 index 00000000..8ed5a281 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextLinkParser.java @@ -0,0 +1,42 @@ +package nu.marginalia.gemini.gmi.parser; + +import nu.marginalia.gemini.gmi.line.AbstractGemtextLine; +import nu.marginalia.gemini.gmi.line.GemtextLink; +import nu.marginalia.gemini.gmi.line.GemtextText; +import nu.marginalia.wmsa.memex.model.MemexExternalUrl; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import nu.marginalia.wmsa.memex.model.MemexUrl; + +import javax.annotation.Nullable; +import java.util.regex.Pattern; + +public class GemtextLinkParser { + private static Pattern linkPattern = Pattern.compile("^=>\\s?([^\\s]+)\\s*(.+)?$"); + + @Nullable + public static AbstractGemtextLine parse(String s, MemexNodeHeadingId heading) { + var matcher = linkPattern.matcher(s); + + if (!matcher.matches()) { + return new GemtextText(s, heading); + } + if (matcher.groupCount() == 2) { + return new GemtextLink(toMemexUrl(matcher.group(1)), matcher.group(2), heading); + } + else { + return new GemtextLink(toMemexUrl(matcher.group(1)), null, heading); + } + } + + private static MemexUrl toMemexUrl(String url) { + if (url.startsWith("/")) { + return new MemexNodeUrl(url); + } + else { + return new MemexExternalUrl(url); + } + } + + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextListParser.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextListParser.java new file mode 100644 index 00000000..8416895e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextListParser.java @@ -0,0 +1,17 @@ +package nu.marginalia.gemini.gmi.parser; + +import java.util.regex.Pattern; + +public class GemtextListParser { + private static final Pattern listItemPattern = Pattern.compile("^\\*\\s?(.+)$"); + + public static String parse(String s) { + var matcher = listItemPattern.matcher(s); + + if (!matcher.matches()) { + return null; + } + + return matcher.group(1); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextParser.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextParser.java new file mode 100644 index 00000000..ec15be17 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextParser.java @@ -0,0 +1,135 @@ +package nu.marginalia.gemini.gmi.parser; + +import nu.marginalia.gemini.gmi.line.*; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import nu.marginalia.wmsa.memex.model.MemexNodeTaskId; + +import java.util.*; + +public class GemtextParser { + + private static final String PREFORMAT_MARKER = "```"; + private static final String LITERAL_MARKER = " "; + private static final String LINK_MARKER = "=>"; + private static final String HEADING_MARKER = "#"; + private static final String LIST_MARKER = "*"; + private static final String QUOTE_MARKER = ">"; + private static final String ASIDE_MARKER = "("; + private static final String TASK_MARKER = "-"; + private static final String PRAGMA_MARKER = "%%%"; + + public static AbstractGemtextLine[] parse(String[] lines, MemexNodeHeadingId headingRoot) { + List items = new ArrayList<>(); + MemexNodeHeadingId heading = headingRoot; + MemexNodeTaskId task = new MemexNodeTaskId(0); + + Set pragmas = new HashSet<>(); + + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + + if (line.startsWith(PREFORMAT_MARKER)) { + i = getBlockQuote(items, lines, heading, i); + } + else if (line.startsWith(PRAGMA_MARKER)) { + var pragma = GemtextPragmaParser.parse(line, heading); + + if (pragma instanceof GemtextPragma) { + GemtextPragma gtp = (GemtextPragma) pragma; + pragmas.add(gtp.getLine()); + } + + items.add(pragma); + + } + else if (line.startsWith(LINK_MARKER)) { + var link = GemtextLinkParser.parse(line, heading); + items.add(link); + } + else if (line.startsWith(HEADING_MARKER)) { + var tag = GemtextHeadingParser.parse(line, heading); + + heading = tag.mapHeading(GemtextHeading::getHeading).orElse(heading); + + items.add(tag); + } + else if (line.startsWith(LIST_MARKER)) { + i = getList(items, lines, heading, i); + } + else if (line.startsWith(LITERAL_MARKER)) { + i = getLitteral(items, lines, heading, i); + } + else if (pragmas.contains("TASKS") + && line.startsWith(TASK_MARKER)) + { + var tag = GemtextTaskParser.parse(line, heading, task); + + task = tag.mapTask(GemtextTask::getId).orElse(task); + + items.add(tag); + } + else if (line.startsWith(QUOTE_MARKER)) { + i = getQuote(items, lines, heading, i); + } + else if (line.startsWith(ASIDE_MARKER)) { + var aside = GemtextAsideParser.parse(line, heading); + items.add(Objects.requireNonNullElse(aside, new GemtextText(line, heading))); + } + else { + items.add(new GemtextText(line, heading)); + } + } + return items.toArray(AbstractGemtextLine[]::new); + } + + private static int getBlockQuote(List items, String[] lines, MemexNodeHeadingId heading, int i) { + int j = i+1; + List quotedLines = new ArrayList<>(); + for (;j < lines.length; j++) { + if (lines[j].startsWith(PREFORMAT_MARKER)) { + break; + } + quotedLines.add(lines[j]); + } + items.add(new GemtextPreformat(quotedLines, heading)); + return j; + } + + private static int getList(List items, String[] lines, MemexNodeHeadingId heading, int i) { + int j = i; + List listLines = new ArrayList<>(); + for (;j < lines.length; j++) { + if (!lines[j].startsWith(LIST_MARKER)) { + break; + } + listLines.add(GemtextListParser.parse(lines[j])); + } + items.add(new GemtextList(listLines, heading)); + return j-1; + } + private static int getLitteral(List items, String[] lines, MemexNodeHeadingId heading, int i) { + int j = i; + List listLines = new ArrayList<>(); + for (;j < lines.length; j++) { + if (!lines[j].startsWith(LITERAL_MARKER)) { + break; + } + listLines.add(lines[j]); + } + items.add(new GemtextTextLiteral(listLines, heading)); + return j-1; + } + + private static int getQuote(List items, String[] lines, MemexNodeHeadingId heading, int i) { + int j = i; + List listLines = new ArrayList<>(); + for (;j < lines.length; j++) { + if (!lines[j].startsWith(QUOTE_MARKER)) { + break; + } + listLines.add(GemtextQuoteParser.parse(lines[j])); + } + items.add(new GemtextQuote(listLines, heading)); + return j-1; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextPragmaParser.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextPragmaParser.java new file mode 100644 index 00000000..192c4ba6 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextPragmaParser.java @@ -0,0 +1,26 @@ +package nu.marginalia.gemini.gmi.parser; + +import nu.marginalia.gemini.gmi.line.AbstractGemtextLine; +import nu.marginalia.gemini.gmi.line.GemtextPragma; +import nu.marginalia.gemini.gmi.line.GemtextText; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; + +import java.util.regex.Pattern; + +public class GemtextPragmaParser { + private static final Pattern pragmaPattern = Pattern.compile("^%%%\\s*(.*|$)$"); + + public static AbstractGemtextLine parse(String s, MemexNodeHeadingId heading) { + var matcher = pragmaPattern.matcher(s); + + if (!matcher.matches()) { + return new GemtextText(s, heading); + } + + String task = matcher.group(1); + + return new GemtextPragma(task, heading); + } + + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextQuoteParser.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextQuoteParser.java new file mode 100644 index 00000000..af72b3c9 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextQuoteParser.java @@ -0,0 +1,17 @@ +package nu.marginalia.gemini.gmi.parser; + +import java.util.regex.Pattern; + +public class GemtextQuoteParser { + private static final Pattern listItemPattern = Pattern.compile("^>(.+)$"); + + public static String parse(String s) { + var matcher = listItemPattern.matcher(s); + + if (!matcher.matches()) { + return null; + } + + return matcher.group(1); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextTaskParser.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextTaskParser.java new file mode 100644 index 00000000..d9b95f2e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/parser/GemtextTaskParser.java @@ -0,0 +1,31 @@ +package nu.marginalia.gemini.gmi.parser; + +import nu.marginalia.gemini.gmi.line.AbstractGemtextLine; +import nu.marginalia.gemini.gmi.line.GemtextTask; +import nu.marginalia.gemini.gmi.line.GemtextText; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import nu.marginalia.wmsa.memex.model.MemexNodeTaskId; +import nu.marginalia.wmsa.memex.model.MemexTaskTags; + +import java.util.regex.Pattern; + +public class GemtextTaskParser { + private static final Pattern taskPattern = Pattern.compile("^(-+)\\s*([^-].*|$)$"); + + public static AbstractGemtextLine parse(String s, MemexNodeHeadingId heading, + MemexNodeTaskId taskId) { + var matcher = taskPattern.matcher(s); + + if (!matcher.matches()) { + return new GemtextText(s, heading); + } + + int level = matcher.group(1).length() - 1; + + String task = matcher.group(2); + + return new GemtextTask(taskId.next(level), task, heading, new MemexTaskTags(task)); + } + + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/renderer/GemtextRenderer.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/renderer/GemtextRenderer.java new file mode 100644 index 00000000..1697c8df --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/renderer/GemtextRenderer.java @@ -0,0 +1,91 @@ +package nu.marginalia.gemini.gmi.renderer; + +import nu.marginalia.gemini.gmi.line.*; + +import java.util.function.Function; + +public class GemtextRenderer implements GemtextLineVisitor { + + private final Function headingConverter; + private final Function linkConverter; + private final Function listConverter; + private final Function preformatConverter; + private final Function quoteConverter; + private final Function textConverter; + private final Function asideConverter; + private final Function taskConverter; + private final Function literalConverter; + private final Function pragmaConverter; + + public GemtextRenderer(Function headingConverter, + Function linkConverter, + Function listConverter, + Function preformatConverter, + Function quoteConverter, + Function textConverter, + Function asideConverter, + Function taskConverter, + Function literalConverter, + Function pragmaConverter + ) { + this.headingConverter = headingConverter; + this.linkConverter = linkConverter; + this.listConverter = listConverter; + this.preformatConverter = preformatConverter; + this.quoteConverter = quoteConverter; + this.textConverter = textConverter; + this.asideConverter = asideConverter; + this.taskConverter = taskConverter; + this.literalConverter = literalConverter; + this.pragmaConverter = pragmaConverter; + } + + + public String renderLine(AbstractGemtextLine line) { + return line.visit(this); + } + + @Override + public String visit(GemtextHeading g) { + return headingConverter.apply(g); + } + + @Override + public String visit(GemtextLink g) { + return linkConverter.apply(g); + } + + @Override + public String visit(GemtextList g) { + return listConverter.apply(g); + } + + @Override + public String visit(GemtextPreformat g) { + return preformatConverter.apply(g); + } + + @Override + public String visit(GemtextQuote g) { + return quoteConverter.apply(g); + } + + @Override + public String visit(GemtextText g) { + return textConverter.apply(g); + } + + @Override + public String visit(GemtextTextLiteral g) { + return literalConverter.apply(g); + } + + @Override + public String visit(GemtextAside g) { return asideConverter.apply(g); } + + @Override + public String visit(GemtextTask g) { return taskConverter.apply(g); } + + @Override + public String visit(GemtextPragma g) { return pragmaConverter.apply(g); } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/renderer/GemtextRendererFactory.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/renderer/GemtextRendererFactory.java new file mode 100644 index 00000000..257cfc1c --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/gmi/renderer/GemtextRendererFactory.java @@ -0,0 +1,227 @@ +package nu.marginalia.gemini.gmi.renderer; + +import nu.marginalia.gemini.gmi.line.*; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import nu.marginalia.wmsa.memex.model.MemexUrl; +import org.apache.logging.log4j.util.Strings; + +import java.util.Objects; +import java.util.stream.Collectors; + +public class GemtextRendererFactory { + + public final String urlBase; + public final String docUrl; + + public GemtextRendererFactory(String urlBase, String docUrl) { + this.urlBase = Objects.requireNonNull(urlBase, "urlBase must not be null"); + this.docUrl = Objects.requireNonNull(docUrl, "docUrl must not be null"); + } + + public GemtextRendererFactory(String urlBase) { + this.urlBase = Objects.requireNonNull(urlBase, "urlBase must not be null"); + this.docUrl = null; + } + + public GemtextRendererFactory() { + this.urlBase = null; + this.docUrl = null; + } + + public GemtextRenderer htmlRendererEditable() { + return new GemtextRenderer(this::htmlHeadingEditable, + this::htmlLink, this::htmlList, + this::htmlPre, this::htmlQuote, + this::htmlText, this::htmlAside, + this::htmlTask, this::htmlLiteral, + this::htmlPragma); + } + + public GemtextRenderer htmlRendererReadOnly() { + return new GemtextRenderer(this::htmlHeadingReadOnly, + this::htmlLink, this::htmlList, + this::htmlPre, this::htmlQuote, + this::htmlText, this::htmlAside, + this::htmlTask, this::htmlLiteral, + this::htmlPragma); + } + + + public GemtextRenderer gemtextRendererAsIs() { + return new GemtextRenderer(this::rawHeading, + this::rawLink, this::rawList, + this::rawPre, this::rawQuote, + this::rawText, this::rawAside, + this::rawTask, this::rawLiteral, + this::rawPragma); + } + + + public GemtextRenderer gemtextRendererPublic() { + return new GemtextRenderer(this::rawHeading, + this::rawLink, this::rawList, + this::rawPre, this::rawQuote, + this::rawText, this::rawAside, + this::rawTask, this::rawLiteral, + this::rawSupressPragma); + } + + + private String htmlPragma(GemtextPragma gemtextPragma) { + return "\n"; + } + + public String htmlHeadingEditable(GemtextHeading g) { + if (docUrl == null) { + throw new UnsupportedOperationException("Wrong constructor used, need urlBase and docUrl"); + } +// String editLink = String.format("\nEdit\n", urlBase + docUrl, g.getLevel()); + + return htmlHeadingReadOnly(g); + } + + public String htmlHeadingReadOnly(GemtextHeading g) { + if (g.getLevel().getLevel() == 1) + return String.format("

%s

\n", g.getLevel(), sanitizeText(g.getName())); + if (g.getLevel().getLevel() == 2) + return String.format("

%s

\n", g.getLevel(), sanitizeText(g.getName())); + if (g.getLevel().getLevel() == 3) + return String.format("

%s

\n", g.getLevel(), sanitizeText(g.getName())); + + return String.format("

%s

\n", g.getLevel(), sanitizeText(g.getName())); + } + + public String htmlLink(GemtextLink g) { + if (urlBase == null) { + throw new UnsupportedOperationException("Wrong constructor used, need urlBase"); + } + final String linkClass = getLinkClass(g.getUrl()); + final String linkUrl = getLinkUrl(g.getUrl()).replaceFirst("^gemini://", "https://proxy.vulpes.one/gemini/"); + if (g.getTitle() != null) { + return String.format("
%s
%s
\n", + linkClass, linkUrl, g.getUrl(), sanitizeText(g.getTitle())); + } + else { + return String.format("%s
\n", + linkClass, linkUrl, g.getUrl()); + } + } + private String getLinkUrl(MemexUrl url) { + if (url instanceof MemexNodeUrl || url.getUrl().startsWith("/")) { + return urlBase + url; + } + return url.toString(); + } + + private String getLinkClass(MemexUrl url) { + if (url instanceof MemexNodeUrl) { + return "internal"; + } + return "external"; + } + public String htmlList(GemtextList g) { + return g.getItems() + .stream() + .map(s -> "
  • " + sanitizeText(s) + "
  • ") + .collect( + Collectors.joining("\n", "
      \n", "
    \n")); + } + + public String htmlPre(GemtextPreformat g) { + return g.getItems().stream() + .map(this::sanitizeText) + .collect( + Collectors.joining("\n", "
    \n", "
    \n")); + } + + public String htmlLiteral(GemtextTextLiteral g) { + return g.getItems().stream() + .map(this::sanitizeText) + .collect( + Collectors.joining("\n", "
    \n", "
    \n")); + } + public String htmlQuote(GemtextQuote g) { + return g.getItems().stream() + .map(this::sanitizeText) + .collect( + Collectors.joining("
    \n", "
    \n", "
    \n")); + + } + public String htmlText(GemtextText g) { + return sanitizeText(g.getLine()) + "
    \n"; + } + public String htmlAside(GemtextAside g) { + return "\n"; + } + + public String sanitizeText(String s) { + return s.replaceAll("<", "<").replaceAll(">", ">"); + } + + public String htmlTask(GemtextTask g) { + return String.format("
    %s %s
    \n", + g.getId(), + g.getState().style, + g.getId(), + "-".repeat(g.getLevel()), + g.getTask()); + } + + public String rawHeading(GemtextHeading g) { + if (g.getLevel().getLevel() == 1) + return "# " + g.getName(); + if (g.getLevel().getLevel() == 2) + return "## " + g.getName(); + if (g.getLevel().getLevel() == 3) + return "### " + g.getName(); + + return "### " + g.getName(); + } + + public String rawLink(GemtextLink g) { + if (g.getTitle() != null && !g.getTitle().isBlank()) { + return "=> " + g.getUrl().getUrl() + "\t" + g.getTitle(); + } + return "=> " + g.getUrl().getUrl(); + } + + public String rawList(GemtextList g) { + return g.getItems() + .stream() + .map(s -> "* " + s) + .collect(Collectors.joining("\n")); + } + + public String rawPre(GemtextPreformat g) { + return g.getItems().stream() + .collect(Collectors.joining("\n", "```\n", "\n```")); + } + + public String rawQuote(GemtextQuote g) { + return g.getItems().stream() + .map(s -> "> " + s) + .collect(Collectors.joining()); + + } + + public String rawText(GemtextText g) { + return g.getLine(); + } + + public String rawLiteral(GemtextTextLiteral g) { + return Strings.join(g.getItems(), '\n'); + } + + public String rawAside(GemtextAside g) { + return "(" + g.getLine() + ")"; + } + public String rawTask(GemtextTask g) { + return "-".repeat(Math.max(0, g.getLevel())) + " " + g.getTask(); + } + private String rawPragma(GemtextPragma gemtextPragma) { + return "%%% " + gemtextPragma.getLine(); + } + private String rawSupressPragma(GemtextPragma gemtextPragma) { + return ""; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/io/GeminiConnection.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/io/GeminiConnection.java new file mode 100644 index 00000000..6d032a2e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/io/GeminiConnection.java @@ -0,0 +1,185 @@ +package nu.marginalia.gemini.io; + +import nu.marginalia.gemini.BadBotList; +import nu.marginalia.gemini.plugins.FileType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLSocket; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.stream.Stream; + +public class GeminiConnection { + private final SSLSocket connection; + + private final Logger logger = LoggerFactory.getLogger("Server"); + private final OutputStream os; + private final InputStream is; + private static final BadBotList badBotList = BadBotList.INSTANCE; + + public GeminiConnection(SSLSocket connection) throws IOException { + this.connection = connection; + + this.os = connection.getOutputStream(); + this.is = connection.getInputStream(); + + } + + public String getAddress() { + return connection.getInetAddress().getHostAddress(); + } + + public Optional readUrl() throws Exception { + + var str = new GeminiInput().get(); + if (!badBotList.isQueryPermitted(connection.getInetAddress(), str)) { + return Optional.empty(); + } + if (!str.isBlank()) { + return Optional.of(new URI(str)); + } + throw new GeminiUserException("Bad URI"); + } + + public void redirect(String address) throws IOException { + writeStatusLine(GeminiStatusCode.REDIRECT, address); + } + public void redirectPermanent(String address) throws IOException { + writeStatusLine(GeminiStatusCode.REDIRECT_PERMANENT, address); + } + public GeminiConnection writeStatusLine(int code, String meta) throws IOException { + write(String.format("%2d %s", code, meta)); + return this; + } + + public GeminiConnection writeBytes(byte[] data) throws IOException { + write(data); + return this; + } + + public GeminiConnection printf(String pattern, Object...args) throws IOException { + write(String.format(pattern, args)); + return this; + } + + public GeminiConnection writeLines(String... lines) throws IOException { + for (String s : lines) { + write(s); + } + return this; + } + public GeminiConnection writeLinesFromFile(Path file) throws IOException { + try (Stream lines = Files.lines(file)) { + lines.forEach(line -> { + try { + write(line); + } catch (IOException e) { + logger.error("IO Error", e); + } + }); + } + return this; + } + + public GeminiConnection acceptLines(Stream lines) { + lines.forEach(line -> { + try { + write(line); + } catch (IOException e) { + logger.error("IO exception", e); + } + }); + return this; + } + + private void write(String s) throws IOException { + os.write(s.getBytes(StandardCharsets.UTF_8)); + os.write(new byte[] { '\r', '\n'}); + } + + private void write(byte[] bs) throws IOException { + os.write(bs); + } + // This is a weird pattern but it makes the listing code very much cleaner + + public void error(String message) { + logger.error("{}", message); + + throw new GeminiUserException(message); + } + + public void close() { + try { + connection.shutdownOutput(); + connection.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public boolean isConnected() { + return connection.isConnected(); + } + + public void respondWithFile(Path serverPath, FileType fileType) throws IOException { + if (fileType.binary) { + writeStatusLine(GeminiStatusCode.SUCCESS, fileType.mime) + .writeBytes(Files.readAllBytes(serverPath)); + } + else { + writeStatusLine(GeminiStatusCode.SUCCESS, fileType.mime) + .writeLinesFromFile(serverPath); + } + } + + public class GeminiInput { + private final byte[] buffer = new byte[1024]; + private int idx = 0; + + final String result; + + public GeminiInput() throws IOException { + + for (idx = 0; idx < buffer.length; idx++) { + if (hasEndOfLine()) { + result = new String(buffer, 0, idx-2, StandardCharsets.UTF_8); + return; + } + + readCharacter(); + } + + error("String too long"); + + // unreachable + result = ""; + } + + public String get() { + return result; + } + + private void readCharacter() throws IOException { + int rb = is.read(); + if (-1 == rb) { + error("URL incomplete (no CR LF)"); + } + buffer[idx] = (byte) rb; + } + + public boolean hasEndOfLine() { + return idx > 2 + && buffer[idx - 1] == (byte) '\n' + && buffer[idx - 2] == (byte) '\r'; + } + + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/io/GeminiSSLSetUp.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/io/GeminiSSLSetUp.java new file mode 100644 index 00000000..525515f3 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/io/GeminiSSLSetUp.java @@ -0,0 +1,49 @@ +package nu.marginalia.gemini.io; + +import com.google.inject.Inject; +import com.google.inject.name.Named; + +import javax.net.ssl.*; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.SecureRandom; + +public class GeminiSSLSetUp { + private final Path certPasswordFile; + private final Path certFile; + + @Inject + public GeminiSSLSetUp( + @Named("gemini-cert-file") Path certFile, + @Named("gemini-cert-password-file") Path certPasswordFile) { + this.certFile = certFile; + this.certPasswordFile = certPasswordFile; + } + public String getCertPassword() throws IOException { + return Files.readString(certPasswordFile); + } + + private SSLContext getContext() throws Exception { + KeyStore ks = KeyStore.getInstance("JKS", "SUN"); + ks.load(Files.newInputStream(certFile), getCertPassword().toCharArray()); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(ks, getCertPassword().toCharArray()); + KeyManager[] keyManagers = kmf.getKeyManagers(); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); + tmf.init(ks); + TrustManager[] trustManagers = tmf.getTrustManagers(); + + var ctx = SSLContext.getInstance("TLSv1.3"); + ctx.init(keyManagers, trustManagers, new SecureRandom()); + return ctx; + } + + + public SSLServerSocketFactory getServerSocketFactory() throws Exception { + return getContext().getServerSocketFactory(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/io/GeminiStatusCode.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/io/GeminiStatusCode.java new file mode 100644 index 00000000..f201e331 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/io/GeminiStatusCode.java @@ -0,0 +1,11 @@ +package nu.marginalia.gemini.io; + +public class GeminiStatusCode { + public static final int INPUT = 10; + public static final int SUCCESS = 20; + public static final int ERROR_PERMANENT = 50; + public static final int ERROR_TEMPORARY = 40; + public static final int PROXY_ERROR = 43; + public static final int REDIRECT = 30; + public static final int REDIRECT_PERMANENT = 31; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/io/GeminiUserException.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/io/GeminiUserException.java new file mode 100644 index 00000000..937da4fe --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/io/GeminiUserException.java @@ -0,0 +1,8 @@ +package nu.marginalia.gemini.io; + +/** Throw to report message to user */ +public class GeminiUserException extends RuntimeException { + public GeminiUserException(String message) { + super(message); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/plugins/BareStaticPagePlugin.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/plugins/BareStaticPagePlugin.java new file mode 100644 index 00000000..b0d4fe05 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/plugins/BareStaticPagePlugin.java @@ -0,0 +1,53 @@ +package nu.marginalia.gemini.plugins; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import nu.marginalia.gemini.io.GeminiConnection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; + +import static nu.marginalia.gemini.GeminiService.DEFAULT_FILENAME; + +public class BareStaticPagePlugin implements Plugin { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private Path geminiServerRoot; + + @Inject + public BareStaticPagePlugin(@Named("gemini-server-root") Path geminiServerRoot) { + this.geminiServerRoot = geminiServerRoot; + } + + @Override + public boolean serve(URI url, GeminiConnection connection) throws IOException { + + final Path serverPath = getServerPath(url.getPath()); + + if (!Files.isRegularFile(serverPath)) { + return false; + } + + verifyPath(geminiServerRoot, serverPath); + logger.info("Serving {}", serverPath); + + connection.respondWithFile(serverPath, FileType.match(serverPath)); + + return true; + } + + private Path getServerPath(String requestPath) { + final Path serverPath = Path.of(geminiServerRoot + requestPath); + + if (Files.isDirectory(serverPath) && Files.isRegularFile(serverPath.resolve(DEFAULT_FILENAME))) { + return serverPath.resolve(DEFAULT_FILENAME); + } + + return serverPath; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/plugins/FileType.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/plugins/FileType.java new file mode 100644 index 00000000..587a9894 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/plugins/FileType.java @@ -0,0 +1,58 @@ +package nu.marginalia.gemini.plugins; + +import java.nio.file.Path; + +public enum FileType { + GMI("gmi", "text/gemini", FileIcons.DOCUMENT, false), + GEM("gem", "text/gemini", FileIcons.DOCUMENT, false), + TXT("txt", "text/plain", FileIcons.DOCUMENT, false), + MARKDOWN("md", "text/markdown", FileIcons.DOCUMENT, false), + JAVA("java", "text/java", FileIcons.JAVA, false), + PROPERTIES("properties", "text/properties", FileIcons.SETTINGS, false), + GRADLE("gradle", "text/gradle", FileIcons.SETTINGS, false), + ZIP("zip", "application/zip", FileIcons.ZIP, true), + PNG("png", "image/png", FileIcons.IMAGE, true), + JPG("jpg", "image/jpg", FileIcons.IMAGE, true), + JPEG("jpeg", "image/jpg", FileIcons.IMAGE, true), + BIN("bin", "application/binary", FileIcons.BINARY, true), + SH("sh", "text/sh", FileIcons.SETTINGS, false), + XML("xml", "text/xml", FileIcons.DOCUMENT, false), + DOCKERFILE("Dockerfile", "text/dockerfile", FileIcons.SETTINGS, false) + ; + + public static FileType match(String fileName) { + for (var type : values()) { + if (fileName.endsWith(type.suffix)) { + return type; + } + } + return BIN; + } + + public static FileType match(Path path) { + return match(path.toString()); + } + + FileType(String suffix, String mime, String icon, boolean binary) { + this.suffix = suffix; + this.mime = mime; + + this.icon = icon; + this.binary = binary; + } + public final String suffix; + public final String mime; + public final String icon; + public final boolean binary; + +} + +class FileIcons { + public static final String DOCUMENT = "🗒"; + public static final String JAVA = "♨"; + public static final String SETTINGS = "💻"; + public static final String ZIP = "🗜"; + public static final String IMAGE = "🖼"; + public static final String DIRECTORY = "🗂"; + public static final String BINARY = "📚"; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/plugins/Plugin.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/plugins/Plugin.java new file mode 100644 index 00000000..3765e1ca --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/plugins/Plugin.java @@ -0,0 +1,19 @@ +package nu.marginalia.gemini.plugins; + +import nu.marginalia.gemini.io.GeminiConnection; +import nu.marginalia.gemini.io.GeminiUserException; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; + +public interface Plugin { + /** @return true if content served */ + boolean serve(URI url, GeminiConnection connection) throws IOException; + + default void verifyPath(Path root, Path p) { + if (!p.normalize().startsWith(root)) { + throw new GeminiUserException("ಠ_ಠ That path is off limits!"); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/gemini/plugins/SearchPlugin.java b/marginalia_nu/src/main/java/nu/marginalia/gemini/plugins/SearchPlugin.java new file mode 100644 index 00000000..e0122d75 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/gemini/plugins/SearchPlugin.java @@ -0,0 +1,78 @@ +package nu.marginalia.gemini.plugins; + +import com.google.inject.Inject; +import nu.marginalia.gemini.io.GeminiConnection; +import nu.marginalia.gemini.io.GeminiStatusCode; +import org.apache.http.HttpHost; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.conn.routing.HttpRoute; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +public class SearchPlugin implements Plugin { + private final PoolingHttpClientConnectionManager connectionManager; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Inject + public SearchPlugin() { + + connectionManager = new PoolingHttpClientConnectionManager(); + connectionManager.setMaxTotal(200); + connectionManager.setDefaultMaxPerRoute(20); + HttpHost host = new HttpHost("https://search.marginalia.nu/"); + connectionManager.setMaxPerRoute(new HttpRoute(host), 20); + } + + @Override + public boolean serve(URI url, GeminiConnection connection) throws IOException { + var client = HttpClients.custom() + .setConnectionManager(connectionManager) + .build(); + + if (!"/search".equals(url.getPath())) { + return false; + } + + String query = url.getRawQuery(); + + if (null == query || "".equals(query)) { + logger.info("Requesting search terms"); + connection.writeStatusLine(GeminiStatusCode.INPUT, "Please enter a search query"); + } + else { + logger.info("Delegating search query '{}'", query); + + final HttpGet get = new HttpGet(createSearchUri(query)); + final byte[] binaryResponse; + + try (var rsp = client.execute(get)) { + binaryResponse = rsp.getEntity().getContent().readAllBytes(); + } + catch (IOException ex) { + logger.error("backend error", ex); + + connection.writeStatusLine(GeminiStatusCode.PROXY_ERROR, "Failed to reach backend server"); + return true; + } + + connection + .writeStatusLine(GeminiStatusCode.SUCCESS, "text/gemini") + .writeBytes(binaryResponse); + } + return true; + } + + private URI createSearchUri(String query) { + try { + return new URI("https://search.marginalia.nu/search?format=gmi&query="+query); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/ByteFolder.java b/marginalia_nu/src/main/java/nu/marginalia/util/ByteFolder.java new file mode 100644 index 00000000..0406e06c --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/ByteFolder.java @@ -0,0 +1,80 @@ +package nu.marginalia.util; + +public class ByteFolder { + + public byte[] foldBytes(int p, int q) { + + int pw = bitWidth(p); + int qw = bitWidth(q); + int qpw = qw + pw; + + long qp = Integer.toUnsignedLong(q) << pw | Integer.toUnsignedLong(p); + + int qpwBytes = ((qpw - 1) / Byte.SIZE) + 1; + + byte[] bytes = new byte[qpwBytes + 1]; + bytes[0] = (byte) pw; + for (int i = 1; i < bytes.length; i++) { + bytes[i] = (byte) (qp >>> (qpwBytes - i) * Byte.SIZE & 0xff); + } + + return bytes; + } + + // Function such that (decodeBytes o foldBytes) = identity + public static int[] decodeBytes(byte[] data) { + int[] dest = new int[2]; + decodeBytes(data, data.length, dest); + return dest; + } + + public static void decodeBytes(byte[] data, int length, int[] dest) { + long val = 0; + + for (int i = 1; i < length; i++) { + val = (val << 8) | ((0xFF)&data[i]); + } + + dest[1] = (int)(val >>> data[0]); + dest[0] = (int)(val & ~(dest[1]<= 0; i--) { + s.append((b[j] & (1L << i)) > 0 ? 1 : 0); + } + } + return s.toString(); + } + public static String intBits(int v) { + StringBuilder s = new StringBuilder(); + for (int i = 32; i >=0; i--) { + s.append((v & (1L << i)) > 0 ? 1 : 0); + } + return s.toString(); + } + public static String longBits(long v) { + StringBuilder s = new StringBuilder(); + for (int i = 64; i >=0; i--) { + s.append((v & (1L << i)) > 0 ? 1 : 0); + } + return s.toString(); + } + + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/FileSizeUtil.java b/marginalia_nu/src/main/java/nu/marginalia/util/FileSizeUtil.java new file mode 100644 index 00000000..de0cb17b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/FileSizeUtil.java @@ -0,0 +1,18 @@ +package nu.marginalia.util; + +public class FileSizeUtil { + public static String readableSize(long byteCount) { + if (byteCount < 1024L) { + return String.format("%db", byteCount); + } + if (byteCount < 1024*1024L) { + return String.format("%2.2fKb", byteCount/1024.); + } + if (byteCount < 1024*1024*1024L) { + return String.format("%2.2fMb", byteCount/1024/1024.); + } + + return String.format("%2.2fGb", byteCount/1024/1024L/1024.); + + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/ParallelPipe.java b/marginalia_nu/src/main/java/nu/marginalia/util/ParallelPipe.java new file mode 100644 index 00000000..fd1ae119 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/ParallelPipe.java @@ -0,0 +1,101 @@ +package nu.marginalia.util; + +import lombok.SneakyThrows; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +public abstract class ParallelPipe { + private final LinkedBlockingQueue inputs; + private final LinkedBlockingQueue intermediates; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final List processThreads = new ArrayList<>(); + private final Thread receiverThread; + + private volatile boolean expectingInput = true; + private volatile boolean expectingOutput = true; + + public ParallelPipe(String name, int numberOfThreads, int inputQueueSize, int intermediateQueueSize) { + inputs = new LinkedBlockingQueue<>(inputQueueSize); + intermediates = new LinkedBlockingQueue<>(intermediateQueueSize); + + for (int i = 0; i < numberOfThreads; i++) { + processThreads.add(new Thread(this::runProcessThread, name + "-process["+i+"]")); + } + receiverThread = new Thread(this::runReceiverThread, name + "-receiver"); + + processThreads.forEach(Thread::start); + receiverThread.start(); + } + + public void clearQueues() { + inputs.clear(); + intermediates.clear(); + } + + @SneakyThrows + private void runProcessThread() { + while (expectingInput || !inputs.isEmpty()) { + var in = inputs.poll(1, TimeUnit.SECONDS); + + if (in != null) { + try { + var ret = onProcess(in); + if (ret != null) { + intermediates.put(ret); + } + } + catch (InterruptedException ex) { + throw ex; + } + catch (Exception ex) { + logger.error("Exception", ex); + } + + } + } + + logger.debug("Terminating {}", Thread.currentThread().getName()); + } + @SneakyThrows + private void runReceiverThread() { + while (expectingOutput || !inputs.isEmpty() || !intermediates.isEmpty()) { + var intermediate = intermediates.poll(997, TimeUnit.MILLISECONDS); + if (intermediate != null) { + try { + onReceive(intermediate); + } + catch (Exception ex) { + logger.error("Exception", ex); + } + } + } + + logger.info("Terminating {}", Thread.currentThread().getName()); + } + + @SneakyThrows + public void accept(INPUT input) { + inputs.put(input); + } + + protected abstract INTERMEDIATE onProcess(INPUT input) throws Exception; + protected abstract void onReceive(INTERMEDIATE intermediate) throws Exception; + + public void join() throws InterruptedException { + expectingInput = false; + + for (var thread : processThreads) { + thread.join(); + } + + expectingOutput = false; + receiverThread.join(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/PrimeUtil.java b/marginalia_nu/src/main/java/nu/marginalia/util/PrimeUtil.java new file mode 100644 index 00000000..0effbde9 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/PrimeUtil.java @@ -0,0 +1,41 @@ +package nu.marginalia.util; + +// This is not a fast way of finding primes +public class PrimeUtil { + + public static long nextPrime(long start, long step) { + if (isDivisible(start, 2)) { + start = start + step; + } + + long val; + for (val = start; !isPrime(val); val += 2*step) {} + return val; + } + + public static boolean isPrime(long v) { + if (v <= 2) { + return true; + } + if ((v & 1) == 0) { + return false; + } + for (long t = 3; t <= v/3; t++) { + if ((v % t) == 0) { + return false; + } + } + return true; + } + + public static boolean isDivisible(long a, long b) { + if (a == 0 || b == 0) { + return false; + } + + if (a > b) { + return (a % b) == 0; + } + return (b % a) == 0; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/RandomWriteFunnel.java b/marginalia_nu/src/main/java/nu/marginalia/util/RandomWriteFunnel.java new file mode 100644 index 00000000..94c1d3f4 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/RandomWriteFunnel.java @@ -0,0 +1,139 @@ +package nu.marginalia.util; + +import io.prometheus.client.Gauge; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; + +/** For managing random writes on SSDs + * + * See https://en.wikipedia.org/wiki/Write_amplification + * */ +public class RandomWriteFunnel implements AutoCloseable { + + private final static Gauge write_rate = Gauge.build("wmsa_rwf_write_bytes", "Bytes/s") + .register(); + private final static Gauge transfer_rate = Gauge.build("wmsa_rwf_transfer_bytes", "Bytes/s") + .register(); + private static final Logger logger = LoggerFactory.getLogger(RandomWriteFunnel.class); + private DataBin[] bins; + + private final int binSize; + + public RandomWriteFunnel(Path tempDir, long size, int binSize) throws IOException { + this.binSize = binSize; + + if (size > 0) { + int binCount = (int) (size / binSize + ((size % binSize) != 0L ? 1 : 0)); + bins = new DataBin[binCount]; + for (int i = 0; i < binCount; i++) { + bins[i] = new DataBin(tempDir, (int) Math.min(size - binSize * i, binSize)); + } + } + else { + bins = new DataBin[0]; + } + } + + public void put(long address, long data) throws IOException { + bins[((int)(address / binSize))].put((int)(address%binSize), data); + } + + public void write(FileChannel o) throws IOException { + ByteBuffer buffer = ByteBuffer.allocateDirect(binSize*8); + logger.debug("Writing from RWF"); + + for (int i = 0; i < bins.length; i++) { + var bin = bins[i]; + buffer.clear(); + bin.eval(buffer); + + while (buffer.hasRemaining()) { + int wb = o.write(buffer); + write_rate.set(wb); + } + } + logger.debug("Done"); + } + + @Override + public void close() throws IOException { + for (DataBin bin : bins) { + bin.close(); + } + } + + static class DataBin implements AutoCloseable { + private final ByteBuffer buffer; + private int size; + private final FileChannel channel; + private final File file; + + DataBin(Path tempDir, int size) throws IOException { + buffer = ByteBuffer.allocateDirect(360_000); + this.size = size; + file = Files.createTempFile(tempDir, "scatter-writer", ".dat").toFile(); + channel = new RandomAccessFile(file, "rw").getChannel(); + } + + void put(int address, long data) throws IOException { + buffer.putInt(address); + buffer.putLong(data); + + if (buffer.capacity() - buffer.position() < 12) { + flushBuffer(); + } + } + + private void flushBuffer() throws IOException { + if (buffer.position() == 0) + return; + + buffer.flip(); + while (channel.write(buffer) > 0); + buffer.clear(); + } + + private void eval(ByteBuffer dest) throws IOException { + flushBuffer(); + + channel.position(0); + buffer.clear(); + dest.clear(); + for (int i = 0; i < size; i++) { + dest.putLong(0L); + } + dest.position(0); + dest.limit(size*8); + while (channel.position() < channel.size()) { + int rb = channel.read(buffer); + if (rb < 0) { + break; + } + else { + transfer_rate.set(rb); + } + buffer.flip(); + while (buffer.limit() - buffer.position() >= 12) { + int addr = buffer.getInt(); + long data = buffer.getLong(); + dest.putLong(8*addr, data); + } + buffer.compact(); + } + } + + @Override + public void close() throws IOException { + channel.close(); + file.delete(); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/SeekDictionary.java b/marginalia_nu/src/main/java/nu/marginalia/util/SeekDictionary.java new file mode 100644 index 00000000..a49544e4 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/SeekDictionary.java @@ -0,0 +1,73 @@ +package nu.marginalia.util; + +import gnu.trove.list.array.TIntArrayList; + +import java.util.ArrayList; +import java.util.function.ToIntFunction; + +public abstract class SeekDictionary { + private final ArrayList banks = new ArrayList<>(); + private final TIntArrayList offsets = new TIntArrayList(); + + public static SeekDictionary of(ToIntFunction length) { + return new SeekDictionary() { + @Override + public int length(T obj) { + return length.applyAsInt(obj); + } + }; + } + public T last() { + return banks.get(banks.size()-1); + } + public int lastStart() { + return offsets.get(offsets.size()-1); + } + + public abstract int length(T obj); + public int end() { + if (banks.isEmpty()) return 0; + + return (offsets.getQuick(offsets.size()-1) + length(last())); + } + + public void add(T obj) { + + if (banks.isEmpty()) { + banks.add(obj); + offsets.add(0); + } + else { + offsets.add(end()); + banks.add(obj); + } + } + + public T bankForOffset(int offset) { + return banks.get(idxForOffset(offset)); + } + + public int idxForOffset(int offset) { + + int high = offsets.size() - 1; + int low = 0; + + while ( low <= high ) { + int mid = ( low + high ) >>> 1; + int midVal = offsets.getQuick(mid); + + if ( midVal < offset ) { + low = mid + 1; + } + else if ( midVal > offset ) { + high = mid - 1; + } + else { + return mid; + } + } + return low-1; + + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/btree/BTreeReader.java b/marginalia_nu/src/main/java/nu/marginalia/util/btree/BTreeReader.java new file mode 100644 index 00000000..ec8f204b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/btree/BTreeReader.java @@ -0,0 +1,104 @@ +package nu.marginalia.util.btree; + +import nu.marginalia.util.btree.model.BTreeContext; +import nu.marginalia.util.btree.model.BTreeHeader; +import nu.marginalia.util.multimap.MultimapFileLong; +import nu.marginalia.util.multimap.MultimapSearcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BTreeReader { + + private final MultimapFileLong file; + private final BTreeContext ctx; + private final Logger logger = LoggerFactory.getLogger(BTreeReader.class); + private final long mask; + private final MultimapSearcher searcher; + + public BTreeReader(MultimapFileLong file, BTreeContext ctx) { + this.file = file; + this.searcher = file.createSearcher(); + this.ctx = ctx; + this.mask = ctx.equalityMask(); + } + + public long fileSize() { + return file.size(); + } + + public BTreeHeader getHeader(long offset) { + return new BTreeHeader(file.get(offset), file.get(offset+1), file.get(offset+2)); + } + + public long offsetForEntry(BTreeHeader header, final long keyRaw) { + final long key = keyRaw & mask; + + if (header.layers() == 0) { + return trivialSearch(header, key); + } + + long p = searchEntireTopLayer(header, key); + if (p < 0) return -1; + + long cumOffset = p * ctx.BLOCK_SIZE_WORDS(); + for (int i = header.layers() - 2; i >= 0; --i) { + long offsetBase = header.indexOffsetLongs() + header.relativeLayerOffset(ctx, i); + p = searchLayerBlock(key, offsetBase+cumOffset); + if (p < 0) + return -1; + cumOffset = ctx.BLOCK_SIZE_WORDS()*(p + cumOffset); + } + + long dataMax = header.dataOffsetLongs() + (long) header.numEntries() * ctx.entrySize(); + return searchDataBlock(key, + header.dataOffsetLongs() + ctx.entrySize()*cumOffset, + dataMax); + } + + + private long searchEntireTopLayer(BTreeHeader header, long key) { + long offset = header.indexOffsetLongs(); + + return searcher.binarySearchUpperBound(key, offset, offset + ctx.BLOCK_SIZE_WORDS()) - offset; + } + + private long searchLayerBlock(long key, long blockOffset) { + if (blockOffset < 0) + return blockOffset; + + return searcher.binarySearchUpperBound(key, blockOffset, blockOffset + ctx.BLOCK_SIZE_WORDS()) - blockOffset; + } + + + private long searchDataBlock(long key, long blockOffset, long dataMax) { + if (blockOffset < 0) + return blockOffset; + + long lastOffset = Math.min(blockOffset+ctx.BLOCK_SIZE_WORDS()*(long)ctx.entrySize(), dataMax); + int length = (int)(lastOffset - blockOffset); + + if (ctx.entrySize() == 1) { + if (mask == ~0L) return searcher.binarySearchUpperBoundNoMiss(key, blockOffset, blockOffset+length); + return searcher.binarySearchUpperBoundNoMiss(key, blockOffset, blockOffset+length, mask); + } + + return searcher.binarySearchUpperBoundNoMiss(key, blockOffset, ctx.entrySize(), length/ctx.entrySize(), mask); + } + + private long trivialSearch(BTreeHeader header, long key) { + long offset = header.dataOffsetLongs(); + + if (ctx.entrySize() == 1) { + if (mask == ~0L) { + return searcher.binarySearchUpperBoundNoMiss(key, offset, offset+header.numEntries()); + } + else { + return searcher.binarySearchUpperBoundNoMiss(key, offset, offset+header.numEntries(), mask); + } + } + + return searcher.binarySearchUpperBoundNoMiss(key, offset, ctx.entrySize(), header.numEntries(), mask); + + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/btree/BTreeWriter.java b/marginalia_nu/src/main/java/nu/marginalia/util/btree/BTreeWriter.java new file mode 100644 index 00000000..28ac4914 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/btree/BTreeWriter.java @@ -0,0 +1,110 @@ +package nu.marginalia.util.btree; + +import nu.marginalia.util.btree.model.BTreeContext; +import nu.marginalia.util.btree.model.BTreeHeader; +import nu.marginalia.util.multimap.MultimapFileLong; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + + +public class BTreeWriter { + private final Logger logger = LoggerFactory.getLogger(BTreeWriter.class); + private final BTreeContext ctx; + private final MultimapFileLong map; + + public BTreeWriter(MultimapFileLong map, BTreeContext ctx) { + this.map = map; + this.ctx = ctx; + } + + private static long indexSize(BTreeContext ctx, int numWords, int numLayers) { + if (numLayers == 0) { + return 0; // Special treatment for small tables + } + + long size = 0; + for (int layer = 0; layer < numLayers; layer++) { + size += ctx.layerSize(numWords, layer); + } + return size; + } + + public long write(long offset, int numEntries, WriteCallback writeIndex) + throws IOException + { + var header = makeHeader(offset, numEntries); + + header.write(map, offset); + writeIndex.write(header.dataOffsetLongs()); + + if (header.layers() < 1) { + return ctx.calculateSize(numEntries); + } + + writeIndex(header); + + return ctx.calculateSize(numEntries); + } + + public static BTreeHeader makeHeader(BTreeContext ctx, long offset, int numEntries) { + final int numLayers = ctx.numLayers(numEntries); + + final int padding = BTreeHeader.getPadding(ctx, offset, numLayers); + + final long indexOffset = offset + BTreeHeader.BTreeHeaderSizeLongs + padding; + final long dataOffset = indexOffset + indexSize(ctx, numEntries, numLayers); + + return new BTreeHeader(numLayers, numEntries, indexOffset, dataOffset); + } + + public BTreeHeader makeHeader(long offset, int numEntries) { + return makeHeader(ctx, offset, numEntries); + } + + + private void writeIndex(BTreeHeader header) { + var layerOffsets = getRelativeLayerOffsets(header); + + long stride = ctx.BLOCK_SIZE_WORDS(); + for (int layer = 0; layer < header.layers(); layer++, + stride*=ctx.BLOCK_SIZE_WORDS()) { + long indexWord = 0; + long offsetBase = layerOffsets[layer] + header.indexOffsetLongs(); + long numEntries = header.numEntries(); + for (long idx = 0; idx < numEntries; idx += stride, indexWord++) { + long dataOffset = header.dataOffsetLongs() + (idx + (stride-1)) * ctx.entrySize(); + long val; + + if (idx + (stride-1) < numEntries) { + val = map.get(dataOffset) & ctx.equalityMask(); + } + else { + val = Long.MAX_VALUE; + } + if (offsetBase + indexWord < 0) { + logger.error("bad put @ {}", offsetBase + indexWord); + logger.error("layer{}", layer); + logger.error("layer offsets {}", layerOffsets); + logger.error("offsetBase = {}", offsetBase); + logger.error("numEntries = {}", numEntries); + logger.error("indexWord = {}", indexWord); + } + map.put(offsetBase + indexWord, val); + } + for (; (indexWord % ctx.BLOCK_SIZE_WORDS()) != 0; indexWord++) { + map.put(offsetBase + indexWord, Long.MAX_VALUE); + } + } + + } + + private long[] getRelativeLayerOffsets(BTreeHeader header) { + long[] layerOffsets = new long[header.layers()]; + for (int i = 0; i < header.layers(); i++) { + layerOffsets[i] = header.relativeLayerOffset(ctx, i); + } + return layerOffsets; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/btree/WriteCallback.java b/marginalia_nu/src/main/java/nu/marginalia/util/btree/WriteCallback.java new file mode 100644 index 00000000..70bd8132 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/btree/WriteCallback.java @@ -0,0 +1,7 @@ +package nu.marginalia.util.btree; + +import java.io.IOException; + +public interface WriteCallback { + void write(long offset) throws IOException; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/btree/model/BTreeContext.java b/marginalia_nu/src/main/java/nu/marginalia/util/btree/model/BTreeContext.java new file mode 100644 index 00000000..4655946c --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/btree/model/BTreeContext.java @@ -0,0 +1,56 @@ +package nu.marginalia.util.btree.model; + +import nu.marginalia.util.btree.BTreeWriter; + +public record BTreeContext(int MAX_LAYERS, + int entrySize, + long equalityMask, + int BLOCK_SIZE_BITS, + int BLOCK_SIZE_WORDS) { + + public BTreeContext(int MAX_LAYERS, int entrySize, long equalityMask, int BLOCK_SIZE_BITS) { + this(MAX_LAYERS, entrySize, equalityMask, BLOCK_SIZE_BITS, 1 << BLOCK_SIZE_BITS); + + } + + public long calculateSize(int numEntries) { + var header = BTreeWriter.makeHeader(this, 0, numEntries); + + return header.dataOffsetLongs() + (long)numEntries * entrySize; + } + + public int numLayers(int numEntries) { + if (numEntries <= BLOCK_SIZE_WORDS*2) { + return 0; + } + for (int i = 1; i < MAX_LAYERS; i++) { + long div = (1L << (BLOCK_SIZE_BITS*i)); + long frq = numEntries / div; + if (frq < (1L << BLOCK_SIZE_BITS)) { + if (numEntries == (numEntries & div)) { + return i; + } + return i+1; + } + } + return MAX_LAYERS; + } + + public long layerSize(int numEntries, int level) { + return BLOCK_SIZE_WORDS * numBlocks(numEntries, level); + } + + private long numBlocks(int numWords, int level) { + + long layerSize = 1L<<(BLOCK_SIZE_BITS*(level+1)); + int numBlocks = 0; + + numBlocks += numWords / layerSize; + if (numWords % layerSize != 0) { + numBlocks++; + } + + return numBlocks; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/btree/model/BTreeHeader.java b/marginalia_nu/src/main/java/nu/marginalia/util/btree/model/BTreeHeader.java new file mode 100644 index 00000000..17dae46a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/btree/model/BTreeHeader.java @@ -0,0 +1,46 @@ +package nu.marginalia.util.btree.model; + +import nu.marginalia.util.multimap.MultimapFileLong; + +public record BTreeHeader(int layers, int numEntries, long indexOffsetLongs, long dataOffsetLongs) { + public BTreeHeader { + assert (layers >= 0); + assert (numEntries >= 0); + assert (indexOffsetLongs >= 0); + assert (dataOffsetLongs >= 0); + assert (dataOffsetLongs >= indexOffsetLongs); + } + + public static int BTreeHeaderSizeLongs = 3; + + public BTreeHeader(long a, long b, long c) { + this((int)(a >>> 32), (int)(a & 0xFFFF_FFFFL), b, c); + } + + public static int getPadding(BTreeContext ctx, long offset, int numLayers) { + final int padding; + if (numLayers == 0) { + padding = 0; + } + else { + padding = (int) (ctx.BLOCK_SIZE_WORDS() - ((offset + BTreeHeader.BTreeHeaderSizeLongs) % ctx.BLOCK_SIZE_WORDS())); + } + return padding; + } + + public void write(MultimapFileLong dest, long offset) { + dest.put(offset, ((long) layers << 32L) | ((long)numEntries & 0xFFFF_FFFFL)); + dest.put(offset+1, indexOffsetLongs); + dest.put(offset+2, dataOffsetLongs); + } + + + public long relativeLayerOffset(BTreeContext ctx, int n) { + long offset = 0; + for (int i = n+1; i < layers; i++) { + offset += ctx.layerSize( numEntries, i); + } + return offset; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/dict/DictionaryData.java b/marginalia_nu/src/main/java/nu/marginalia/util/dict/DictionaryData.java new file mode 100644 index 00000000..847259db --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/dict/DictionaryData.java @@ -0,0 +1,186 @@ +package nu.marginalia.util.dict; + +import nu.marginalia.util.SeekDictionary; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +public class DictionaryData { + + private final int DICTIONARY_BANK_SIZE; + private static final Logger logger = LoggerFactory.getLogger(DictionaryData.class); + + private final SeekDictionary banks = SeekDictionary.of(DictionaryDataBank::getSize); + + public DictionaryData(int bankSize) { + DICTIONARY_BANK_SIZE = bankSize; + + banks.add(new DictionaryDataBank(0)); + } + + public int size() { + return banks.end(); + } + + public int add(byte[] data, int value) { + var activeBank = banks.last(); + int rb = activeBank.add(data, value); + + if (rb == -1) { + int end = activeBank.getEnd(); + logger.debug("Switching bank @ {}", end); + var newBank = new DictionaryDataBank(end); + rb = newBank.add(data, value); + + banks.add(newBank); + } + + return rb; + } + + + public byte[] getBytes(int offset) { + return banks.bankForOffset(offset).getBytes(offset); + } + public boolean keyEquals(int offset, byte[] data) { + return banks.bankForOffset(offset).keyEquals(offset, data); + } + + public int getValue(int offset) { + return banks.bankForOffset(offset).getValue(offset); + } + + public class DictionaryDataBank { + + private final int start_idx; + private final ByteBuffer data; + + private int size; + private int[] offset; + private int[] value; + + public DictionaryDataBank(int start_idx) { + this.start_idx = start_idx; + + data = ByteBuffer.allocateDirect(DICTIONARY_BANK_SIZE); + + offset = new int[DICTIONARY_BANK_SIZE/16]; + value = new int[DICTIONARY_BANK_SIZE/16]; + size = 0; + } + + public int getStart() { + return start_idx; + } + + public int getEnd() { + return start_idx + size; + } + + public byte[] getBytes(int idx) { + if (idx < start_idx || idx - start_idx >= size) { + throw new IndexOutOfBoundsException(idx); + } + + idx = idx - start_idx; + + final int start; + final int end = offset[idx]; + + if (idx == 0) start = 0; + else start = offset[idx-1]; + + byte[] dst = new byte[end-start]; + data.get(start, dst); + return dst; + } + + public int getValue(int idx) { + if (idx < start_idx || idx - start_idx >= size) { + throw new IndexOutOfBoundsException(idx); + } + return value[idx - start_idx]; + } + + public boolean keyEquals(int idx, byte[] data) { + if (idx < start_idx || idx - start_idx >= size) { + throw new IndexOutOfBoundsException(idx); + } + + idx = idx - start_idx; + int start; + int end = offset[idx]; + + if (idx == 0) { + start = 0; + } + else { + start = offset[idx-1]; + } + if (data.length != end - start) { + return false; + } + for (int i = 0; i < data.length; i++) { + if (this.data.get(start + i) != data[i]) { + return false; + } + } + return true; + } + + public long longHashCode(int idx) { + if (idx < start_idx || idx - start_idx >= size) { + throw new IndexOutOfBoundsException(idx); + } + + idx = idx - start_idx; + int start; + int end = offset[idx]; + + if (idx == 0) { + start = 0; + } + else { + start = offset[idx-1]; + } + + long result = 1; + for (int i = start; i < end; i++) + result = 31 * result + data.get(i); + + return result; + } + + public int add(byte[] newData, int newValue) { + if (size == offset.length) { + logger.debug("Growing bank from {} to {}", offset.length, offset.length*2); + offset = Arrays.copyOf(offset, offset.length*2); + value = Arrays.copyOf(value, value.length*2); + } + + if (size > 0 && offset[size-1]+newData.length >= DICTIONARY_BANK_SIZE) { + if (offset.length > size+1) { + logger.debug("Shrinking bank from {} to {}", offset.length, size - 1); + offset = Arrays.copyOf(offset, size + 1); + value = Arrays.copyOf(value, size + 1); + } + return -1; // Full + } + + int dataOffset = size > 0 ? offset[size-1] : 0; + + data.put(dataOffset, newData); + + offset[size] = dataOffset + newData.length; + value[size] = newValue; + + return start_idx + size++; + } + + public int getSize() { + return size; + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/dict/DictionaryHashMap.java b/marginalia_nu/src/main/java/nu/marginalia/util/dict/DictionaryHashMap.java new file mode 100644 index 00000000..57a8b6f3 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/dict/DictionaryHashMap.java @@ -0,0 +1,208 @@ +package nu.marginalia.util.dict; + +import io.prometheus.client.Gauge; +import nu.marginalia.util.PrimeUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; +import java.util.concurrent.atomic.AtomicInteger; + +import static java.lang.Math.round; +import static nu.marginalia.util.FileSizeUtil.readableSize; + +/** + * Spiritually influenced by GNU Trove's hash maps + * LGPL 2.1 + */ +public class DictionaryHashMap { + private static final Logger logger = LoggerFactory.getLogger(DictionaryHashMap.class); + private static final Gauge probe_count_metrics + = Gauge.build("wmsa_dictionary_hash_map_probe_count", "Probing Count") + .register(); + + private final int bufferCount; + private final IntBuffer[] buffers; + public static final int NO_VALUE = Integer.MIN_VALUE; + + private final DictionaryData dictionaryData; + + private final long hashTableSize; + private final int bufferSizeBytes; + private final int intsPerBuffer; + private final long maxProbeLength; + + private AtomicInteger sz = new AtomicInteger(0); + + public DictionaryHashMap(long sizeMemory) { + final int intSize = 4; + + bufferCount = 1 + (int) ((intSize*sizeMemory) / (1<<30)); + buffers = new IntBuffer[bufferCount]; + + // Actually use a prime size for Donald Knuth reasons + hashTableSize = PrimeUtil.nextPrime(sizeMemory, -1); + + intsPerBuffer = 1 + (int)(sizeMemory/ bufferCount); + bufferSizeBytes = intSize*intsPerBuffer; + maxProbeLength = sizeMemory/10; + + logger.info("Allocating dictionary hash map of size {}, capacity: {}", + readableSize((long) bufferCount * bufferSizeBytes), + hashTableSize); + + logger.info("available-size:{} memory-size:{} buffer-count: {}, buffer-size:{} ints-per-buffer:{} max-probe-length:{}", + hashTableSize, sizeMemory, bufferCount, bufferSizeBytes, intsPerBuffer, maxProbeLength); + + if (((long) bufferCount * intsPerBuffer) < sizeMemory) { + logger.error("Buffer memory is less than requested memory: {}*{} = {} < {}; this data structure is not safe to use", + bufferCount, + bufferSizeBytes, (long) bufferCount * bufferSizeBytes, + sizeMemory); + throw new Error("Irrecoverable logic error"); + } + else { + logger.debug("Buffer size sanity checked passed"); + } + + + dictionaryData = new DictionaryData(Math.min(1<<30, Math.max(32, (int)(sizeMemory/4)))); + + initializeBuffers(); + } + + private void initializeBuffers() { + for (int b = 0; b < bufferCount; b++) { + buffers[b] = ByteBuffer.allocateDirect(bufferSizeBytes).asIntBuffer(); + + for (int i = 0; i < intsPerBuffer; i++) { + buffers[b].put(i, NO_VALUE); + } + } + } + + public int memSz() { + return dictionaryData.size(); + } + public int size() { + return sz.get(); + } + + private int getCell(long idx) { + int buffer = (int)(idx / intsPerBuffer); + int bufferIdx = (int)(idx % intsPerBuffer); + return buffers[buffer].get(bufferIdx); + } + private void setCell(long idx, int val) { + int buffer = (int)(idx / intsPerBuffer); + int bufferIdx = (int)(idx % intsPerBuffer); + + buffers[buffer].put(bufferIdx, val); + } + + public int put(byte[] data, int value) { + + long hash = longHash(data) & 0x7FFF_FFFF_FFFF_FFFFL; + + long idx = hash % hashTableSize; + + if (getCell(idx) == NO_VALUE) { + return setValue(data, value, idx); + } + + return putRehash(data, value, idx, hash); + } + + private int putRehash(byte[] data, int value, long idx, long hash) { + final long pStride = 1 + (hash % (hashTableSize - 2)); + + for (long j = 1; j < maxProbeLength; j++) { + idx = idx - pStride; + + if (idx < 0) { + idx += hashTableSize; + } + + final int val = getCell(idx); + + if (val == NO_VALUE) { + probe_count_metrics.set(j); + + return setValue(data, value, idx); + } + else if (dictionaryData.keyEquals(val, data)) { + return val; + } + } + + throw new IllegalStateException("DictionaryHashMap full @ size " + size() + "/" + hashTableSize + ", " + round((100.0*size()) / hashTableSize) + "%"); + } + + private int setValue(byte[] data, int value, long cell) { + sz.incrementAndGet(); + + int di = dictionaryData.add(data, value); + setCell(cell, di); + return di; + } + + public int get(byte[] data) { + final long hash = longHash(data) & 0x7FFF_FFFF_FFFF_FFFFL; + final long cell = hash % hashTableSize; + + if (getCell(cell) == NO_VALUE) { + return NO_VALUE; + } + else { + int val = getCell(cell); + + if (dictionaryData.keyEquals(val, data)) { + return dictionaryData.getValue(val); + } + } + + return getRehash(data, cell, hash); + } + + private int getRehash(byte[] data, long idx, long hash) { + final long pStride = 1 + (hash % (hashTableSize - 2)); + + for (long j = 1; j < maxProbeLength; j++) { + idx = idx - pStride; + + if (idx < 0) { + idx += hashTableSize; + } + + final var val = getCell(idx); + + if (val == NO_VALUE) { + return NO_VALUE; + } + else if (dictionaryData.keyEquals(val, data)) { + return dictionaryData.getValue(val); + } + } + + throw new IllegalStateException("DictionaryHashMap full @ size " + size() + "/" + hashTableSize + ", " + round((100.0*size()) / hashTableSize) + "%"); + } + + private long longHash(byte[] bytes) { + if (bytes == null) + return 0; + + // https://cp-algorithms.com/string/string-hashing.html + int p = 127; + long m = (1L<<61)-1; + long p_power = 1; + long hash_val = 0; + + for (byte element : bytes) { + hash_val = (hash_val + (element+1) * p_power) % m; + p_power = (p_power * p) % m; + } + return hash_val; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/graphics/dithering/FloydSteinbergDither.java b/marginalia_nu/src/main/java/nu/marginalia/util/graphics/dithering/FloydSteinbergDither.java new file mode 100644 index 00000000..59d0f848 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/graphics/dithering/FloydSteinbergDither.java @@ -0,0 +1,243 @@ +package nu.marginalia.util.graphics.dithering; + +import lombok.AllArgsConstructor; +import net.sf.image4j.util.ConvertUtil; +import org.imgscalr.Scalr; + +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.util.Arrays; +import java.util.Comparator; + +public class FloydSteinbergDither { + private final Color[] palette; + + private final int maxWidth; + private final int maxHeight; + + public FloydSteinbergDither(int[] colors, int maxWidth, int maxHeight) { + this.maxWidth = maxWidth; + this.maxHeight = maxHeight; + + palette = Arrays.stream(colors) + .mapToObj(Color::new) + .toArray(Color[]::new); + } + + public BufferedImage convert(BufferedImage src) { + BufferedImage out = dither(resize(src)); + + if (palette.length <= 16) { + int[] cmap = new int[palette.length]; + for (int i = 0; i < palette.length; i++) { + cmap[i] = palette[i].toInt(); + } + return ConvertUtil.convert4(out, cmap); + } + return out; + } + + private BufferedImage dither(BufferedImage in) { + + Errors errors = new Errors(in.getWidth(), in.getHeight()); + + final BufferedImage out = createOutBuffer(in); + + for (int y = 0; y < in.getHeight(); y++) { + for (int x = 0; x < in.getWidth(); x++) { + setOutPixel(errors, out, in, x, y, 1); + } + if (++y >= in.getHeight()) { + break; + } + for (int x = in.getWidth()-1; x >= 0; x--) { + setOutPixel(errors, out, in, x, y, -1); + } + } + return out; + } + + private void setOutPixel(Errors errors, BufferedImage out, BufferedImage in, int x, int y, int dx) { + final Color color = new Color(in.getRGB(x, y)); + final Color adjustedColor = errors.adjust(color, x, y); + + final int newColor = getNearestColorAndDiffuseError(errors, + x, dx, y, + adjustedColor, color); + + out.setRGB(x, y, newColor); + } + + private BufferedImage createOutBuffer(BufferedImage in) { + + var indexModel = createIndexColorModel(); + + return new BufferedImage(indexModel, + indexModel.createCompatibleWritableRaster(in.getWidth(), in.getHeight()), + false, null); + + } + + private BufferedImage resize(BufferedImage src) { + if (maxWidth < 0 || maxHeight < 0) { + return src; + } + final int width = src.getWidth(); + final int height = src.getHeight(); + + double scaleF = Math.min(scaleFactor(width, maxWidth), + scaleFactor(height, maxHeight)); + + if (scaleF < 1.0) { + int newWidth = (int)Math.min(maxWidth, scaleF * width); + int newHeight = (int)Math.min(maxHeight, scaleF * height); + + return Scalr.resize(src, + Scalr.Method.QUALITY, + Scalr.Mode.AUTOMATIC, + newWidth, newHeight); + } + + return src; + } + + private double scaleFactor(int actualValue, int desiredValue) { + if (actualValue <= desiredValue) { + return 1.; + } + return desiredValue / (double) actualValue; + } + + private IndexColorModel createIndexColorModel() { + byte[] reds = new byte[palette.length]; + byte[] greens = new byte[palette.length]; + byte[] blues = new byte[palette.length]; + + for (int i = 0; i < palette.length; i++) { + int colorInt = palette[i].toInt(); + + reds[i] = (byte) ((colorInt >>> 16) & 0xFF); + greens[i] = (byte) ((colorInt >>> 8) & 0xFF); + blues[i] = (byte) ((colorInt) & 0xFF); + } + + return new IndexColorModel(getPaletteBits(palette), palette.length, reds, greens, blues); + + } + + private int getPaletteBits(Color[] palette) { + if (palette.length <= 16) { + return 4; + } + else { + return 8; + } + } + + private int getNearestColorAndDiffuseError(Errors errors, int x, int dx, int y, Color color, Color colorOrig) { + + var match = Arrays.stream(palette).min(Comparator.comparing(c -> c.delta(color))); + assert match.isPresent(); + + var retC = match.get(); + var error = colorOrig.minus(retC); + + errors.add(x+dx, y, error.scale(7/16.)); + errors.add(x+dx, y+1, error.scale(1/16.)); + errors.add(x, y+1, error.scale(5/16.)); + errors.add(x-dx, y+1, error.scale(3/16.)); + + return retC.toInt(); + } +} + +class Errors { + private final int width; + private final int height; + private final Color[] errors; + + Errors(int width, int height) { + this.width = width; + this.height = height; + + errors = new Color[width * height]; + } + + public void add(int x, int y, Color color) { + if (x > 0 && y > 0 && x + 1 < width && y + 1 < height) { + int index = getIndex(x, y); + if (errors[index] == null) { + errors[index] = color; + } + else { + errors[index] = errors[index].plus(color); + } + } + } + + public Color adjust(Color in, int x, int y) { + int idx = getIndex(x, y); + + if (errors[idx] != null) { + return in.plus(errors[idx]); + } + return in; + } + + private int getIndex(int x, int y) { + return x * height + y; + } +} + +@AllArgsConstructor +class Color { + private final double r; + private final double g; + private final double b; + + Color(int hex) { + this.b = ((hex) & 0xFF); + this.g = ((hex >>> 8) & 0xFF); + this.r = ((hex >>> 16) & 0xFF); + } + + int toInt() { + double bv = clampByteRange(b); + double gv = clampByteRange(g); + double rv = clampByteRange(r); + + return (((int)bv&0xFF) | (((int)gv & 0xFF) << 8) | (((int)rv & 0xFF) << 16)); + } + + double clampByteRange(double v) { + if (v < 0) return 0; + if (v > 255) return 255; + return v; + } + + public Color scale(double factor) { + return new Color(r*factor, g*factor, b*factor); + } + + public Color plus(Color other) { + return new Color(r+other.r, g+other.g, b+other.b); + } + + public Color minus(Color other) { + return new Color(r-other.r, g-other.g, b-other.b); + } + + public double delta(Color other) { + double avgr = (r + other.r)/2; + double dr = r - other.r; + double dg = g - other.g; + double db = b - other.b; + + if (avgr > 128) { + return Math.sqrt(2 * dr * dr + 4 * dg * dg + 3 * db * db); + } + else { + return Math.sqrt(3 * dr * dr + 4 * dg * dg + 2 * db * db); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/graphics/dithering/Palettes.java b/marginalia_nu/src/main/java/nu/marginalia/util/graphics/dithering/Palettes.java new file mode 100644 index 00000000..afd6e99a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/graphics/dithering/Palettes.java @@ -0,0 +1,29 @@ +package nu.marginalia.util.graphics.dithering; + +public class Palettes { + + public static int[] MARGINALIA_PALETTE = new int[] { + 0x000000, + 0x000000, + 0x808080, + 0x404040, + + 0xefefc0, + 0xf8f8ee, + 0x274fa5, + 0x85172f, + + 0x808060, + 0x60a060, + 0xFFFFFF, + }; + + public static int[] CGA_PALETTE = new int[]{ + 0x000000, 0xFFFFFF, 0x808080, 0xFF0000, + 0x800000, 0x00FF00, 0x008000, 0x0000FF, + 0x000080, 0xFFFF00, 0x808000, 0x00FFFF, + 0x008080, 0xFF00FF, 0x800080, 0x404040 + }; + + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/hash/LongPairHashMap.java b/marginalia_nu/src/main/java/nu/marginalia/util/hash/LongPairHashMap.java new file mode 100644 index 00000000..ae5f41ae --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/hash/LongPairHashMap.java @@ -0,0 +1,183 @@ +package nu.marginalia.util.hash; + +import io.prometheus.client.Gauge; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import nu.marginalia.wmsa.edge.index.service.index.wordstable.IndexWordsTable; +import nu.marginalia.util.multimap.MultimapFileLong; +import nu.marginalia.util.PrimeUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.lang.Math.round; + +/** + * Spiritually influenced by GNU Trove's hash maps + * LGPL 2.1 + */ +public class LongPairHashMap { + private static final Logger logger = LoggerFactory.getLogger(LongPairHashMap.class); + private static final Gauge probe_count_metrics + = Gauge.build("wmsa_wordfile_hash_map_probe_count", "Probing Count") + .register(); + + private final long hashTableSize; + private final MultimapFileLong data; + private final long maxProbeLength; + private int sz = 0; + private static final int HEADER_SIZE = 2; + + public LongPairHashMap(MultimapFileLong data, long size) { + this.data = data; + // Actually use a prime size for Donald Knuth reasons + hashTableSize = PrimeUtil.nextPrime(size, 1); + maxProbeLength = hashTableSize / 2; + + logger.debug("Table size = " + hashTableSize); + + data.put(0, IndexWordsTable.Strategy.HASH.ordinal()); + data.put(1, hashTableSize); + for (int i = 2; i < hashTableSize; i++) { + data.put(HEADER_SIZE + 2L*i, 0); + } + } + public LongPairHashMap(MultimapFileLong data) { + this.data = data; + hashTableSize = data.get(1); + maxProbeLength = hashTableSize / 10; + + logger.debug("Table size = " + hashTableSize); + } + + public int size() { + return sz; + } + + private CellData getCell(long idx) { + long bufferIdx = 2*idx + HEADER_SIZE; + long a = data.get(bufferIdx); + long b = data.get(bufferIdx+1); + return new CellData(a, b); + } + private void setCell(long idx, CellData cell) { + long bufferIdx = 2*idx + HEADER_SIZE; + data.put(bufferIdx, cell.first); + data.put(bufferIdx+1, cell.second); + } + + public CellData put(CellData data) { + + long hash = longHash(data.getKey()) & 0x7FFF_FFFFL; + + long idx = hash% hashTableSize; + if (!getCell(hash% hashTableSize).isSet()) { + return setValue(data, hash% hashTableSize); + } + + return putRehash(data, idx, hash); + + } + + private CellData putRehash(CellData data, long idx, long hash) { + final long pStride = 1 + (hash % (hashTableSize - 2)); + + for (long j = 1; j < maxProbeLength; j++) { + idx = idx - pStride; + + if (idx < 0) { + idx += hashTableSize; + } + + final var val = getCell(idx); + + if (!val.isSet()) { + probe_count_metrics.set(j); + + return setValue(data, idx); + } + else if (val.getKey() == data.getKey()) { + logger.error("Double write?"); + return val; + } + } + + throw new IllegalStateException("DictionaryHashMap full @ size " + size() + "/" + hashTableSize + ", " + round((100.0*size()) / hashTableSize) + "%, key = " + data.getKey() + ",#"+hash); + } + + private CellData setValue(CellData data, long cell) { + sz++; + + setCell(cell, data); + return data; + } + + public CellData get(int key) { + if (hashTableSize == 0) { + return new CellData(0, 0); + } + final long hash = longHash(key) & 0x7FFF_FFFFL; + + var val = getCell(hash % hashTableSize); + if (!val.isSet()) { + return val; + } + else if (val.getKey() == key) { + return val; + } + + return getRehash(key, hash % hashTableSize, hash); + } + + private CellData getRehash(int key, long idx, long hash) { + final long pStride = 1 + (hash % (hashTableSize - 2)); + + for (long j = 1; j < maxProbeLength; j++) { + idx = idx - pStride; + + if (idx < 0) { + idx += hashTableSize; + } + + final var val = getCell(idx); + + if (!val.isSet()) { + return val; + } + else if (val.getKey() == key) { + return val; + } + } + + throw new IllegalStateException("DictionaryHashMap full @ size " + size() + "/" + hashTableSize + ", " + round((100.0*size()) / hashTableSize) + "%"); + } + + private long longHash(long x) { + return x; + } + + @Getter @EqualsAndHashCode + public static class CellData { + long first; + long second; + + public CellData(long key, long offset) { + first = key | 0x8000_0000_000_000L; + second = offset; + } + + public long getKey() { + return first & ~0x8000_0000_000_000L; + } + public long getOffset() { + return second; + } + + public boolean isSet() { + return first != 0 || second != 0L; + } + } + + public void close() throws Exception { + data.close(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/multimap/MultimapFileLong.java b/marginalia_nu/src/main/java/nu/marginalia/util/multimap/MultimapFileLong.java new file mode 100644 index 00000000..1788fb0a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/multimap/MultimapFileLong.java @@ -0,0 +1,366 @@ +package nu.marginalia.util.multimap; + +import com.upserve.uppend.blobs.NativeIO; +import lombok.SneakyThrows; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.LongBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; + +import static java.nio.channels.FileChannel.MapMode.READ_ONLY; +import static java.nio.channels.FileChannel.MapMode.READ_WRITE; +import static nu.marginalia.util.FileSizeUtil.readableSize; + + +public class MultimapFileLong implements AutoCloseable { + + private final ArrayList buffers = new ArrayList<>(); + private final ArrayList mappedByteBuffers = new ArrayList<>(); + private final FileChannel.MapMode mode; + private final int bufferSize; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final FileChannel channel; + + private final long mapSize; + private final long fileLength; + private long mappedSize; + final static long WORD_SIZE = 8; + + private boolean loadAggressively; + + private NativeIO.Advice advice = null; + + public static MultimapFileLong forReading(Path file) throws IOException { + long fileSize = Files.size(file); + int bufferSize = getBufferSize(fileSize, false); + + return new MultimapFileLong(file.toFile(), READ_ONLY, Files.size(file), bufferSize); + } + + public static MultimapFileLong forOutput(Path file, long estimatedSize) throws IOException { + return new MultimapFileLong(file.toFile(), READ_WRITE, 0, getBufferSize(estimatedSize, true)); + } + + private static int getBufferSize(long totalSize, boolean write) { + if (totalSize > Integer.MAX_VALUE/WORD_SIZE) { + return (int)(Integer.MAX_VALUE/WORD_SIZE); + } + else if (write && totalSize < 8*1024*1024) { + return 8*1024*1024; + } + else { + return (int) Math.min(totalSize, Integer.MAX_VALUE/WORD_SIZE); + } + } + + + public MultimapFileLong(File file, + FileChannel.MapMode mode, + long mapSize, + int bufferSize) throws IOException { + + this(new RandomAccessFile(file, translateToRAFMode(mode)), mode, mapSize, bufferSize, false); + } + + public MultimapFileLong loadAggressively(boolean v) { + this.loadAggressively = v; + return this; + } + + private static String translateToRAFMode(FileChannel.MapMode mode) { + if (READ_ONLY.equals(mode)) { + return "r"; + } else if (READ_WRITE.equals(mode)) { + return "rw"; + } + return "rw"; + } + + + public MultimapFileLong(RandomAccessFile file, + FileChannel.MapMode mode, + long mapSizeBytes, + int bufferSizeWords, + boolean loadAggressively) throws IOException { + this.mode = mode; + this.bufferSize = bufferSizeWords; + this.mapSize = mapSizeBytes; + this.fileLength = file.length(); + this.loadAggressively = loadAggressively; + + channel = file.getChannel(); + mappedSize = 0; + + logger.debug("Creating multimap file size = {} / buffer size = {}, mode = {}", + readableSize(mapSizeBytes), readableSize(8L*bufferSizeWords), mode); + } + + public MultimapSearcher createSearcher() { + return new MultimapSearcher(this); + } + public MultimapSorter createSorter(Path tmpFile, int internalSortLimit) { + return new MultimapSorter(this, tmpFile, internalSortLimit); + } + + @SneakyThrows + public void advice(NativeIO.Advice advice) { + for (var buffer : mappedByteBuffers) { + NativeIO.madvise(buffer, advice); + }; + } + + @SneakyThrows + public void advice0(NativeIO.Advice advice) { + NativeIO.madvise(mappedByteBuffers.get(0), advice); + } + + @SneakyThrows + public void adviceRange(NativeIO.Advice advice, long startLongs, long lengthLongs) { + long endLongs = (startLongs+lengthLongs); + + if (endLongs >= mappedSize) + grow(endLongs); + + var buff = mappedByteBuffers.get((int)(startLongs / bufferSize)); + + if ((int)(startLongs / bufferSize) != (int)((endLongs) / bufferSize)) { + logger.warn("Misaligned madvise, skipping"); + return; + } + + NativeIO.madviseRange(buff, advice, (startLongs % bufferSize) * WORD_SIZE, (int)(lengthLongs*WORD_SIZE)); + } + + public void pokeRange(long offset, int length) { + for (int i = 0; i < length; i += 4096/8) { + get(offset + i); + } + } + + public void force() { + logger.debug("Forcing"); + + for (MappedByteBuffer buffer: mappedByteBuffers) { + buffer.force(); + } + } + + @SneakyThrows + private void grow(long posIdxRequired) { + if (posIdxRequired*WORD_SIZE > mapSize && mode == READ_ONLY) { + throw new IndexOutOfBoundsException(posIdxRequired + " (max " + mapSize + ")"); + } + logger.trace("Growing to encompass {}i/{}b", posIdxRequired, posIdxRequired*WORD_SIZE); + long start; + if (buffers.isEmpty()) { + start = 0; + } + else { + start = (long) buffers.size() * bufferSize; + } + for (long posIdx = start; posIdxRequired >= posIdx; posIdx += bufferSize) { + long posBytes = posIdx * WORD_SIZE; + long bzBytes; + if (mode == READ_ONLY) { + bzBytes = Math.min(WORD_SIZE*bufferSize, mapSize - posBytes); + } + else { + bzBytes = WORD_SIZE*bufferSize; + } + logger.trace("Allocating {}-{}", posBytes, posBytes+bzBytes); + + var buffer = channel.map(mode, posBytes, bzBytes); + + if (loadAggressively) + buffer.load(); + + if (advice != null) { + NativeIO.madvise(buffer, advice); + } + + buffers.add(buffer.asLongBuffer()); + mappedByteBuffers.add(buffer); + + mappedSize += bzBytes/WORD_SIZE; + } + } + + public long size() { + return fileLength; + } + + public void put(long idx, long val) { + if (idx >= mappedSize) + grow(idx); + + try { + buffers.get((int)(idx / bufferSize)).put((int) (idx % bufferSize), val); + } + catch (IndexOutOfBoundsException ex) { + logger.error("Index out of bounds {} -> {}:{} cap {}", idx, buffers.get((int)(idx / bufferSize)), idx % bufferSize, + buffers.get((int)(idx / bufferSize)).capacity()); + throw new RuntimeException(ex); + } + } + + public long get(long idx) { + if (idx >= mappedSize) + grow(idx); + + try { + return buffers.get((int)(idx / bufferSize)).get((int)(idx % bufferSize)); + } + catch (IndexOutOfBoundsException ex) { + logger.error("Index out of bounds {} -> {}:{} cap {}", idx, buffers.get((int)(idx / bufferSize)), idx % bufferSize, + buffers.get((int)(idx / bufferSize)).capacity()); + throw new RuntimeException(ex); + } + } + + + public void read(long[] vals, long idx) { + read(vals, vals.length, idx); + } + + public void read(long[] vals, int n, long idx) { + if (idx+n >= mappedSize) { + grow(idx+n); + } + + int iN = (int)((idx + n) / bufferSize); + + for (int i = 0; i < n; ) { + int i0 = (int)((idx + i) / bufferSize); + int bufferOffset = (int) ((idx+i) % bufferSize); + var buffer = buffers.get(i0); + + final int l; + + if (i0 < iN) l = bufferSize - bufferOffset; + else l = Math.min(n - i, bufferSize - bufferOffset); + + buffer.get(bufferOffset, vals, i, l); + i+=l; + + } + + } + + public void write(long[] vals, long idx) { + write(vals, vals.length, idx); + } + + public void write(long[] vals, int n, long idx) { + if (idx+n >= mappedSize) { + grow(idx+n); + } + + int iN = (int)((idx + n) / bufferSize); + + for (int i = 0; i < n; ) { + int i0 = (int)((idx + i) / bufferSize); + int bufferOffset = (int) ((idx+i) % bufferSize); + var buffer = buffers.get(i0); + + final int l; + + if (i0 < iN) l = bufferSize - bufferOffset; + else l = Math.min(n - i, bufferSize - bufferOffset); + + buffer.put(bufferOffset, vals, i, l); + i+=l; + + } + + } + + public void write(LongBuffer vals, long idx) { + int n = vals.limit() - vals.position(); + if (idx+n >= mappedSize) { + grow(idx+n); + } + int iN = (int)((idx + n) / bufferSize); + + for (int i = 0; i < n; ) { + int i0 = (int)((idx + i) / bufferSize); + + int bufferOffset = (int) ((idx+i) % bufferSize); + var buffer = buffers.get(i0); + + final int l; + + if (i0 < iN) l = bufferSize - bufferOffset; + else l = Math.min(n - i, bufferSize - bufferOffset); + + buffer.put(bufferOffset, vals, vals.position() + i, l); + i+=l; + } + + } + + + public void transferFromFileChannel(FileChannel sourceChannel, long destOffset, long sourceStart, long sourceEnd) throws IOException { + + int length = (int)(sourceEnd - sourceStart); + + if (destOffset+length >= mappedSize) { + grow(destOffset+length); + } + + int i0 = (int)((destOffset) / bufferSize); + int iN = (int)((destOffset + length) / bufferSize); + + int numBuffers = iN - i0 + 1; + ByteBuffer[] buffers = new ByteBuffer[numBuffers]; + for (int i = 0; i < numBuffers; i++) { + buffers[i] = mappedByteBuffers.get(i0 + i); + buffers[i].clear(); + } + if (i0 != iN) { + int startBuf0 = (int) ((destOffset) % bufferSize) * 8; + int endBuf0 = buffers[0].capacity() - (int) ((destOffset) % bufferSize) * 8; + int endBufN = (int)((destOffset + length) % bufferSize)*8; + buffers[0] = buffers[0].slice(startBuf0, endBuf0); + buffers[numBuffers-1] = buffers[numBuffers-1].slice(0, endBufN); + } + else { + buffers[0] = buffers[0].slice((int) ((destOffset) % bufferSize) * 8, 8*length); + } + + sourceChannel.position(sourceStart*8); + + long twb = 0; + while (twb < length * 8L) { + long rb = sourceChannel.read(buffers, 0, buffers.length); + if (rb < 0) + throw new IOException(); + twb += rb; + } + + } + + + @Override + public void close() throws IOException { + force(); + mappedByteBuffers.clear(); + buffers.clear(); + channel.close(); + + // I want to believe + System.runFinalization(); + System.gc(); + } + + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/multimap/MultimapSearcher.java b/marginalia_nu/src/main/java/nu/marginalia/util/multimap/MultimapSearcher.java new file mode 100644 index 00000000..c961ac0e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/multimap/MultimapSearcher.java @@ -0,0 +1,128 @@ +package nu.marginalia.util.multimap; + +import lombok.experimental.Delegate; + +public class MultimapSearcher { + @Delegate + private final MultimapFileLong mmf; + + public MultimapSearcher(MultimapFileLong mmf) { + this.mmf = mmf; + } + + public boolean binarySearch(long key, long fromIndex, long toIndex) { + + long low = fromIndex; + long high = toIndex - 1; + + while (low <= high) { + long mid = (low + high) >>> 1; + long midVal = get(mid); + + if (midVal < key) + low = mid + 1; + else if (midVal > key) + high = mid - 1; + else + return true; // key found + } + return false; // key not found. + } + + public long binarySearchUpperBound(long key, long fromIndex, long toIndex) { + + long low = fromIndex; + long high = toIndex - 1; + + while (low <= high) { + long mid = (low + high) >>> 1; + long midVal = get(mid); + + if (midVal < key) + low = mid + 1; + else if (midVal > key) + high = mid - 1; + else + return mid; + } + return low; + } + + public long binarySearchUpperBound(long key, long fromIndex, long toIndex, long mask) { + + long low = fromIndex; + long high = toIndex - 1; + + while (low <= high) { + long mid = (low + high) >>> 1; + long midVal = get(mid) & mask; + + if (midVal < key) + low = mid + 1; + else if (midVal > key) + high = mid - 1; + else + return mid; + } + return low; + } + + public long binarySearchUpperBoundNoMiss(long key, long fromIndex, long toIndex) { + + long low = fromIndex; + long high = toIndex - 1; + + while (low <= high) { + long mid = (low + high) >>> 1; + long midVal = get(mid); + + if (midVal < key) + low = mid + 1; + else if (midVal > key) + high = mid - 1; + else + return mid; + } + return -1; + } + + + public long binarySearchUpperBoundNoMiss(long key, long fromIndex, long toIndex, long mask) { + + long low = fromIndex; + long high = toIndex - 1; + + while (low <= high) { + long mid = (low + high) >>> 1; + long midVal = get(mid) & mask; + + if (midVal < key) + low = mid + 1; + else if (midVal > key) + high = mid - 1; + else + return mid; + } + return -1; + } + + + public long binarySearchUpperBoundNoMiss(long key, long fromIndex, long step, long steps, long mask) { + + long low = 0; + long high = steps - 1; + + while (low <= high) { + long mid = (low + high) >>> 1; + long midVal = get(fromIndex + mid*step) & mask; + + if (midVal < key) + low = mid + 1; + else if (midVal > key) + high = mid - 1; + else + return fromIndex + mid*step; + } + return -1; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/util/multimap/MultimapSorter.java b/marginalia_nu/src/main/java/nu/marginalia/util/multimap/MultimapSorter.java new file mode 100644 index 00000000..6ca4f64f --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/util/multimap/MultimapSorter.java @@ -0,0 +1,89 @@ +package nu.marginalia.util.multimap; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.LongBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; + +import static nu.marginalia.util.multimap.MultimapFileLong.WORD_SIZE; + +public class MultimapSorter { + private final Path tmpFileDir; + private final int internalSortLimit; + private final MultimapFileLong multimapFileLong; + private final long[] buffer; + + public MultimapSorter(MultimapFileLong multimapFileLong, Path tmpFileDir, int internalSortLimit) { + this.multimapFileLong = multimapFileLong; + this.tmpFileDir = tmpFileDir; + this.internalSortLimit = internalSortLimit; + buffer = new long[internalSortLimit]; + } + + public void sort(long start, int length) throws IOException { + if (length <= internalSortLimit) { + multimapFileLong.read(buffer, length, start); + Arrays.sort(buffer, 0, length); + multimapFileLong.write(buffer, length, start); + } + else { + externalSort(start, length); + } + } + + + private void externalSort(long start, int length) throws IOException { + Path tmpFile = Files.createTempFile(tmpFileDir,"sort-"+start+"-"+(start+length), ".dat"); + + try (var raf = new RandomAccessFile(tmpFile.toFile(), "rw"); var channel = raf.getChannel()) { + var workBuffer = + channel.map(FileChannel.MapMode.READ_WRITE, 0, length * WORD_SIZE) + .asLongBuffer(); + + int width = Math.min(Integer.highestOneBit(length), Integer.highestOneBit(internalSortLimit)); + + // Do in-memory sorting up until internalSortLimit first + for (int i = 0; i < length; i += width) { + sort(start + i, Math.min(width, length-i)); + } + + // Then merge sort on disk for the rest + for (; width < length; width*=2) { + + for (int i = 0; i < length; i += 2*width) { + merge(start, i, Math.min(i+width, length), Math.min(i+2*width, length), workBuffer); + } + + workBuffer.clear(); + multimapFileLong.write(workBuffer, start); + } + + } + finally { + tmpFile.toFile().delete(); + } + } + + void merge(long offset, int left, int right, int end, LongBuffer workBuffer) { + int i = left; + int j = right; + + for (int k = left; k < end; k++) { + final long bufferI = multimapFileLong.get(offset+i); + final long bufferJ = multimapFileLong.get(offset+j); + + if (i < right && (j >= end || bufferI < bufferJ)) { + workBuffer.put(k, bufferI); + i++; + } + else { + workBuffer.put(k, bufferJ); + j++; + } + } + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/AuthConfigurationModule.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/AuthConfigurationModule.java new file mode 100644 index 00000000..3a6772ae --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/AuthConfigurationModule.java @@ -0,0 +1,12 @@ +package nu.marginalia.wmsa.auth; + +import com.google.inject.AbstractModule; +import com.google.inject.name.Names; + +import java.nio.file.Path; + +public class AuthConfigurationModule extends AbstractModule { + public void configure() { + bind(Path.class).annotatedWith(Names.named("password-file")).toInstance(Path.of("/var/lib/wmsa/password.dat")); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/AuthMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/AuthMain.java new file mode 100644 index 00000000..385dfc47 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/AuthMain.java @@ -0,0 +1,28 @@ +package nu.marginalia.wmsa.auth; + +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import nu.marginalia.wmsa.configuration.MainClass; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.module.ConfigurationModule; +import nu.marginalia.wmsa.configuration.server.Initialization; + +import java.io.IOException; + +public class AuthMain extends MainClass { + + @Inject + public AuthMain(AuthService service) throws IOException { + } + + public static void main(String... args) { + init(ServiceDescriptor.AUTH, args); + + Injector injector = Guice.createInjector( + new AuthConfigurationModule(), + new ConfigurationModule()); + injector.getInstance(AuthMain.class); + injector.getInstance(Initialization.class).setReady(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/AuthService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/AuthService.java new file mode 100644 index 00000000..d34de843 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/AuthService.java @@ -0,0 +1,105 @@ +package nu.marginalia.wmsa.auth; + +import com.github.jknack.handlebars.internal.Files; +import com.google.inject.Inject; +import com.google.inject.name.Named; +import nu.marginalia.wmsa.auth.model.LoginFormModel; +import nu.marginalia.wmsa.configuration.server.*; +import nu.marginalia.wmsa.renderer.mustache.MustacheRenderer; +import nu.marginalia.wmsa.renderer.mustache.RendererFactory; +import org.apache.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Request; +import spark.Response; +import spark.Spark; + +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Objects; +import java.util.Optional; + +import static spark.Spark.*; + +public class AuthService extends Service { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private String password; + + private final RateLimiter rateLimiter = RateLimiter.forLogin(); + private final MustacheRenderer loginFormRenderer; + + @Inject + public AuthService(@Named("service-host") String ip, + @Named("service-port") Integer port, + @Named("password-file") Path topSecretPasswordFile, + RendererFactory rendererFactory, + Initialization initialization, + MetricsServer metricsServer) throws IOException { + + super(ip, port, initialization, metricsServer); + + try (var is = new FileReader(topSecretPasswordFile.toFile())) { + password = Files.read(is); + } catch (IOException e) { + logger.error("Could not read password from file " + topSecretPasswordFile, e); + } + loginFormRenderer = rendererFactory.renderer("auth/login"); + + Spark.path("public/api", () -> { + before((req, rsp) -> { + logger.info("{} {}", req.requestMethod(), req.pathInfo()); + }); + + post("/login", this::login); + get("/login", this::loginForm); + }); + Spark.path("api", () -> { + get("/is-logged-in", this::isLoggedIn); + }); + } + + private Object loginForm(Request request, Response response) throws IOException { + String redir = Objects.requireNonNull(request.queryParams("redirect")); + String service = Objects.requireNonNull(request.queryParams("service")); + + return loginFormRenderer.render(new LoginFormModel(service, redir)); + } + + private Object login(Request request, Response response) { + var redir = Objects.requireNonNullElse(request.queryParams("redirect"), "/"); + + if (isLoggedIn(request, response)) { + response.redirect(redir); + return ""; + } + + if (!rateLimiter.isAllowed(Context.fromRequest(request))) { + Spark.halt(429, "Too many requests"); + return null; + } + + if (Objects.equals(password, request.queryParams("password"))) { + request.session(true).attribute("logged-in", true); + response.redirect(redir); + return ""; + } + + response.status(HttpStatus.SC_FORBIDDEN); + return "

    Bad password!

    "; + } + + public boolean isLoggedIn(Request request, Response response) { + var session = request.session(false); + + if (null == session) { + return false; + } + + return Optional.ofNullable(session.attribute("logged-in")) + .map(Boolean.class::cast) + .orElse(false); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/ApiMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/ApiMain.java new file mode 100644 index 00000000..03f7ef31 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/ApiMain.java @@ -0,0 +1,32 @@ +package nu.marginalia.wmsa.auth.api; + +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import nu.marginalia.wmsa.auth.AuthConfigurationModule; +import nu.marginalia.wmsa.auth.AuthMain; +import nu.marginalia.wmsa.auth.AuthService; +import nu.marginalia.wmsa.configuration.MainClass; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.module.ConfigurationModule; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.configuration.server.Initialization; + +import java.io.IOException; + +public class ApiMain extends MainClass { + + @Inject + public ApiMain(ApiService service) throws IOException { + } + + public static void main(String... args) { + init(ServiceDescriptor.API, args); + + Injector injector = Guice.createInjector( + new DatabaseModule(), + new ConfigurationModule()); + injector.getInstance(ApiMain.class); + injector.getInstance(Initialization.class).setReady(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/ApiService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/ApiService.java new file mode 100644 index 00000000..45874c92 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/ApiService.java @@ -0,0 +1,127 @@ +package nu.marginalia.wmsa.auth.api; + +import com.google.common.base.Strings; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.inject.Inject; +import com.google.inject.name.Named; +import com.zaxxer.hikari.HikariDataSource; +import nu.marginalia.wmsa.auth.api.model.ApiLicense; +import nu.marginalia.wmsa.configuration.server.*; +import nu.marginalia.wmsa.edge.search.client.EdgeSearchClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Request; +import spark.Response; +import spark.Spark; + +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; + +public class ApiService extends Service { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Gson gson = new GsonBuilder().create(); + private final EdgeSearchClient searchClient; + private final HikariDataSource dataSource; + private final ConcurrentHashMap licenseCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap rateLimiters = new ConcurrentHashMap<>(); + + @Inject + public ApiService(@Named("service-host") String ip, + @Named("service-port") Integer port, + Initialization initialization, + MetricsServer metricsServer, + EdgeSearchClient searchClient, + HikariDataSource dataSource) + throws IOException + { + super(ip, port, initialization, metricsServer); + this.searchClient = searchClient; + this.dataSource = dataSource; + + Spark.get("/public/api/", (rq, rsp) -> { + logger.info("Redireting to info"); + rsp.redirect("https://memex.marginalia.nu/projects/edge/api.gmi"); + return ""; + }); + Spark.get("/public/api/:key/", this::getKeyInfo, gson::toJson); + Spark.get("/public/api/:key/search/*", this::search, gson::toJson); + } + + private Object getKeyInfo(Request request, Response response) { + return getLicense(request); + } + + private Object search(Request request, Response response) { + response.type("application/json"); + + String[] args = request.splat(); + if (args.length != 1) { + Spark.halt(400); + } + + var license = getLicense(request); + if (null == license) { + Spark.halt(401); + return "Forbidden"; + } + + RateLimiter rl = getRateLimiter(license); + + if (rl != null && !rl.isAllowed()) { + Spark.halt(503); + return "Slow down"; + } + + int count = Integer.parseInt(request.queryParamOrDefault("count", "20")); + int index = Integer.parseInt(request.queryParamOrDefault("index", "3")); + + logger.info("{} Search {}", license.key, args[0]); + + return searchClient.query(Context.fromRequest(request), args[0], count, index) + .blockingFirst().withLicense(license.getLicense()); + } + + private RateLimiter getRateLimiter(ApiLicense license) { + if (license.rate > 0) { + return rateLimiters.computeIfAbsent(license, l -> RateLimiter.custom(license.rate)); + } + else { + return null; + } + } + + + private ApiLicense getLicense(Request request) { + final String key = request.params("key"); + + if (Strings.isNullOrEmpty(key)) { + Spark.halt(400); + } + + var cachedLicense = licenseCache.get(key.toLowerCase()); + if (cachedLicense != null) { + return cachedLicense; + } + + try (var conn = dataSource.getConnection()) { + try (var stmt = conn.prepareStatement("SELECT LICENSE,NAME,RATE FROM EC_API_KEY WHERE LICENSE_KEY=?")) { + stmt.setString(1, key); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + var license = new ApiLicense(key.toLowerCase(), rsp.getString(1), rsp.getString(2), rsp.getInt(3)); + licenseCache.put(key.toLowerCase(), license); + return license; + } + } + } + catch (Exception ex) { + logger.error("Bad request", ex); + Spark.halt(500); + } + + Spark.halt(401); + return null; // unreachable + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/model/ApiLicense.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/model/ApiLicense.java new file mode 100644 index 00000000..21997a2b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/model/ApiLicense.java @@ -0,0 +1,19 @@ +package nu.marginalia.wmsa.auth.api.model; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; + +@Getter +@AllArgsConstructor +@EqualsAndHashCode +public class ApiLicense { + @NonNull + public String key; + @NonNull + public String license; + @NonNull + public String name; + public int rate; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/model/ApiSearchResult.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/model/ApiSearchResult.java new file mode 100644 index 00000000..1a6baf26 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/model/ApiSearchResult.java @@ -0,0 +1,20 @@ +package nu.marginalia.wmsa.auth.api.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import nu.marginalia.wmsa.edge.model.search.EdgeUrlDetails; + +@AllArgsConstructor @Getter +public class ApiSearchResult { + public String url; + public String title; + public String description; + public double quality; + + public ApiSearchResult(EdgeUrlDetails url) { + this.url = url.url.toString(); + this.title = url.getTitle(); + this.description = url.getDescription(); + this.quality = url.getTermScore(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/model/ApiSearchResults.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/model/ApiSearchResults.java new file mode 100644 index 00000000..4551755f --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/api/model/ApiSearchResults.java @@ -0,0 +1,17 @@ +package nu.marginalia.wmsa.auth.api.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.With; + +import java.util.List; + +@AllArgsConstructor +@Getter +@With +public class ApiSearchResults { + private final String license; + + private final String query; + private final List results; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/client/AuthClient.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/client/AuthClient.java new file mode 100644 index 00000000..81cc95ba --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/client/AuthClient.java @@ -0,0 +1,42 @@ +package nu.marginalia.wmsa.auth.client; + +import com.google.inject.Inject; +import io.reactivex.rxjava3.core.Observable; +import kotlin.text.Charsets; +import nu.marginalia.wmsa.client.AbstractDynamicClient; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.server.Context; +import org.apache.http.HttpStatus; +import spark.Request; +import spark.Response; +import spark.Spark; + +import java.net.URLEncoder; +import java.util.concurrent.TimeUnit; + + +public class AuthClient extends AbstractDynamicClient { + @Inject + public AuthClient() { + super(ServiceDescriptor.AUTH); + } + + public Observable isLoggedIn(Context ctx) { + return get(ctx, "/api/is-logged-in").map(Boolean::parseBoolean); + } + + public void redirectToLoginIfUnauthenticated(String domain, Request req, Response rsp) { + if (!isLoggedIn(Context.fromRequest(req)).timeout(1, TimeUnit.SECONDS).blockingFirst()) { + rsp.redirect(req.headers("X-Extern-Domain") + "/auth/login?service="+domain + +"&redirect="+ URLEncoder.encode(req.headers("X-Extern-Url"), Charsets.UTF_8)); + Spark.halt(); + } + } + + + public void requireLogIn(Context ctx) { + if (!isLoggedIn(ctx).timeout(1, TimeUnit.SECONDS).blockingFirst()) { + Spark.halt(HttpStatus.SC_FORBIDDEN); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/model/LoginFormModel.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/model/LoginFormModel.java new file mode 100644 index 00000000..09029161 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/auth/model/LoginFormModel.java @@ -0,0 +1,10 @@ +package nu.marginalia.wmsa.auth.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter @AllArgsConstructor +public class LoginFormModel { + public final String service; + public final String redirect; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/AbortingScheduler.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/AbortingScheduler.java new file mode 100644 index 00000000..fc06bd26 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/AbortingScheduler.java @@ -0,0 +1,63 @@ +package nu.marginalia.wmsa.client; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.schedulers.Schedulers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +public class AbortingScheduler implements AutoCloseable { + private final String name; + private final ThreadFactory threadFactory; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Nullable + private ExecutorService executorService; + + public AbortingScheduler(String name) { + this.name = name; + threadFactory = new ThreadFactoryBuilder() + .setNameFormat(name+"client--%d") + .setUncaughtExceptionHandler(this::handleException) + .build(); + } + + private void handleException(Thread thread, Throwable throwable) { + logger.error("Uncaught exception during Client IO in thread {}", thread.getName(), throwable); + } + + public synchronized Scheduler get() { + return Schedulers.from(getExecutorService(), + true, + false); + } + + public synchronized void abort() { + if (null != executorService) { + executorService.shutdownNow(); + executorService = Executors.newFixedThreadPool(16, threadFactory); + } + } + + @Nonnull + private synchronized ExecutorService getExecutorService() { + if (null == executorService) { + executorService = Executors.newFixedThreadPool(16, threadFactory); + } + return executorService; + } + + @Override + public synchronized void close() { + if (null != executorService) { + executorService.shutdown(); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/AbstractClient.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/AbstractClient.java new file mode 100644 index 00000000..f82e7167 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/AbstractClient.java @@ -0,0 +1,501 @@ +package nu.marginalia.wmsa.client; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.ObservableSource; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.client.exception.LocalException; +import nu.marginalia.wmsa.client.exception.NetworkException; +import nu.marginalia.wmsa.client.exception.RemoteException; +import nu.marginalia.wmsa.client.exception.RouteNotConfiguredException; +import nu.marginalia.wmsa.configuration.server.Context; +import okhttp3.*; +import org.apache.http.HttpHost; +import org.apache.logging.log4j.ThreadContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.net.ConnectException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.zip.GZIPOutputStream; + +public abstract class AbstractClient implements AutoCloseable { + public static final String CONTEXT_OUTBOUND_REQUEST = "outbound-request"; + private final Gson gson = new GsonBuilder().create(); + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Marker httpMarker = MarkerFactory.getMarker("HTTP"); + + private final OkHttpClient client; + + private boolean quiet; + private String url; + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + private int timeout; + private volatile boolean alive; + + private final Thread livenessMonitor; + + public AbstractClient(String host, int port, int timeout) { + logger.info("Creating client for {}", getClass().getSimpleName()); + + this.timeout = timeout; + client = new OkHttpClient.Builder() + .connectTimeout(100, TimeUnit.MILLISECONDS) + .readTimeout(6000, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .followRedirects(true) + .build(); + url = new HttpHost(host, port).toURI(); + + RxJavaPlugins.setErrorHandler(e -> { + if (e.getMessage() == null) { + logger.error("Error", e); + } + else { + logger.error("Error {}: {}", e.getClass().getSimpleName(), e.getMessage()); + } + }); + livenessMonitor = new Thread(this::monitorLiveness, host + "-monitor"); + livenessMonitor.setDaemon(true); + livenessMonitor.start(); + + logger.info("Finished creating client for {}", getClass().getSimpleName()); + } + + public void setServiceRoute(String hostname, int port) { + scheduler().abort(); + url = new HttpHost(hostname, port).toURI(); + } + + @SneakyThrows + private void monitorLiveness() { + Thread.sleep(100); // Wait for initialization + for (;;) { + try { + alive = isResponsive(); + } + catch (java.util.concurrent.TimeoutException tex) { + // + } + catch (Exception ex) { + logger.warn("Oops", ex); + } + synchronized (livenessMonitor) { + if (alive) { + livenessMonitor.wait(1000); + } + } + if (!alive) { + Thread.sleep(100); + } + } + } + + @Override + public void close() { + livenessMonitor.interrupt(); + scheduler().close(); + } + + public abstract AbortingScheduler scheduler(); + + public void setQuiet(boolean quiet) { + this.quiet = quiet; + } + + public abstract String name(); + + public synchronized boolean isResponsive() throws java.util.concurrent.TimeoutException { + Context ctx = Context.internal("ping"); + var req = ctx.paint(new Request.Builder()).url(url + "/internal/ping").get().build(); + + var call = client.newCall(req); + + return Observable.just(call) + .subscribeOn(scheduler().get()) + .map(Call::execute) + .map(this::getResponseStatus) + .flatMap(line -> validateStatus(line, req).timeout(5000, TimeUnit.SECONDS).onErrorReturn(e -> 500)) + .onErrorReturn(error -> 500) + .map(HttpStatusCode::new) + .map(HttpStatusCode::isGood) + .blockingFirst(); + } + + public synchronized boolean isAccepting() { + Context ctx = Context.internal("ready"); + + var req = ctx.paint(new Request.Builder()).url(url + "/internal/ready").get().build(); + + var call = client.newCall(req); + + return Observable.just(call) + .subscribeOn(scheduler().get()) + .map(Call::execute) + .map(this::getResponseStatus) + .flatMap(line -> validateStatus(line, req)) + .timeout(100, TimeUnit.MILLISECONDS) + .onErrorReturn(error -> 500) + .map(HttpStatusCode::new) + .map(HttpStatusCode::isGood) + .blockingFirst(); + } + + @SneakyThrows + protected synchronized Observable post(Context ctx, String endpoint, Object data) { + + ensureAlive(); + + RequestBody body = RequestBody.create( + MediaType.parse("application/json; charset=utf-8"), + json(data)); + + var req = ctx.paint(new Request.Builder()).url(url + endpoint).post(body).build(); + var call = client.newCall(req); + + return Observable + .just(call) + .map((c) -> { + ThreadContext.put("outbound-request", url + endpoint); + return c; + }) + .subscribeOn(scheduler().get()) + .map(this::logInbound) + .map(Call::execute) + .map(this::logOutbound) + .map(this::getResponseStatus) + .retryWhen(this::retryHandler) + .flatMap(line -> validateStatus(line, req)) + .map(HttpStatusCode::new) + .timeout(timeout, TimeUnit.SECONDS) + .doFinally(() -> ThreadContext.remove("outbound-request")); + } + + + @SneakyThrows + protected synchronized Observable postGet(Context ctx, String endpoint, Object data, Class returnType) { + + ensureAlive(); + + RequestBody body = RequestBody.create( + MediaType.parse("application/json"), + json(data)); + + var req = ctx.paint(new Request.Builder()).url(url + endpoint).post(body).build(); + var call = client.newCall(req); + + return Observable.just(call) + .map((c) -> { + ThreadContext.put("outbound-request", url + endpoint); + return c; + }) + .subscribeOn(scheduler().get()) + .map(this::logInbound) + .map(Call::execute) + .map(this::logOutbound) + .retryWhen(this::retryHandler) + .map(rsp -> validateResponseStatus(rsp, req, 200)) + .map(rsp -> getEntity(rsp, returnType)) + .timeout(timeout, TimeUnit.SECONDS) + .doFinally(() -> ThreadContext.remove("outbound-request")); + } + + protected synchronized Observable post(Context ctx, String endpoint, String data, MediaType mediaType) { + ensureAlive(); + + var body = RequestBody.create(mediaType, data); + + var req = ctx.paint(new Request.Builder()).url(url + endpoint).post(body).build(); + var call = client.newCall(req); + + + return Observable.just(call) + .map((c) -> { + ThreadContext.put(CONTEXT_OUTBOUND_REQUEST, url + endpoint); + return c; + }) + .subscribeOn(scheduler().get()) + .map(this::logInbound) + .map(Call::execute) + .map(this::logOutbound) + .map(this::getResponseStatus) + .retryWhen(this::retryHandler) + .flatMap(line -> validateStatus(line, req)) + .map(HttpStatusCode::new) + .timeout(timeout, TimeUnit.SECONDS) + .doFinally(() -> ThreadContext.remove("outbound-request")); + } + + protected synchronized Observable get(Context ctx, String endpoint, Class type) { + ensureAlive(); + + var req = ctx.paint(new Request.Builder()).url(url + endpoint).get().build(); + var call = client.newCall(req); + + return Observable.just(call) + .map((c) -> { + ThreadContext.put("outbound-request", url + endpoint); + return c; + }) + .subscribeOn(scheduler().get()) + .map(this::logInbound) + .map(Call::execute) + .map(this::logOutbound) + .map(rsp -> validateResponseStatus(rsp, req, 200)) + .map(rsp -> getEntity(rsp, type)) + .retryWhen(this::retryHandler) + .timeout(timeout, TimeUnit.SECONDS) + .doFinally(() -> ThreadContext.remove("outbound-request")); + } + + @SuppressWarnings("unchecked") + protected synchronized Observable> getList(Context ctx, String endpoint, Class type) { + ensureAlive(); + + var req = ctx.paint(new Request.Builder()).url(url + endpoint).get().build(); + var call = client.newCall(req); + + return Observable.just(call) + .map((c) -> { + ThreadContext.put("outbound-request", url + endpoint); + return c; + }) + .subscribeOn(scheduler().get()) + .map(this::logInbound) + .map(Call::execute) + .map(this::logOutbound) + .map(rsp -> validateResponseStatus(rsp, req, 200)) + .map(rsp -> Arrays.asList((T[])getEntity(rsp, type.arrayType()))) + .retryWhen(this::retryHandler) + .timeout(timeout, TimeUnit.SECONDS) + .doFinally(() -> ThreadContext.remove("outbound-request")); + } + + + protected synchronized Observable getBinary(Context ctx, String endpoint) { + ensureAlive(); + + var req = ctx.paint(new Request.Builder()).url(url + endpoint).get().build(); + var call = client.newCall(req); + + return Observable.just(call) + .map((c) -> { + ThreadContext.put("outbound-request", url + endpoint); + return c; + }) + .subscribeOn(scheduler().get()) + .map(this::logInbound) + .map(Call::execute) + .map(this::logOutbound) + .map(rsp -> validateResponseStatus(rsp, req, 200)) + .map(this::getBinaryEntity) + .retryWhen(this::retryHandler) + .timeout(timeout, TimeUnit.SECONDS) + .doFinally(() -> ThreadContext.remove("outbound-request")); + } + + protected synchronized Observable get(Context ctx, String endpoint) { + ensureAlive(); + + var req = ctx.paint(new Request.Builder()).url(url + endpoint).get().build(); + var call = client.newCall(req); + + return Observable.just(call) + .map((c) -> { + ThreadContext.put("outbound-request", url + endpoint); + return c; + }) + .subscribeOn(scheduler().get()) + .map(this::logInbound) + .map(Call::execute) + .map(this::logOutbound) + .map(rsp -> validateResponseStatus(rsp, req,200)) + .map(this::getText) + .retryWhen(this::retryHandler) + .timeout(timeout, TimeUnit.SECONDS) + .doFinally(() -> ThreadContext.remove("outbound-request")); + } + + protected synchronized Observable delete(Context ctx, String endpoint) { + ensureAlive(); + + var req = ctx.paint(new Request.Builder()).url(url + endpoint).delete().build(); + var call = client.newCall(req); + + return Observable.just(call) + .map((c) -> { + ThreadContext.put("outbound-request", url + endpoint); + return c; + }) + .subscribeOn(scheduler().get()) + .map(this::logInbound) + .map(Call::execute) + .map(this::logOutbound) + .map(this::getResponseStatus) + .flatMap(line -> validateStatus(line, req)) + .map(HttpStatusCode::new) + .retryWhen(this::retryHandler) + .timeout(timeout, TimeUnit.SECONDS) + .doFinally(() -> ThreadContext.remove("outbound-request")); + } + + + @SneakyThrows + private Call logInbound(Call outgoing) { + return outgoing; + } + + @SneakyThrows + private Response logOutbound(Response incoming) { + return incoming; + } + + @SneakyThrows + private void ensureAlive() { + if (!isAlive()) { + wait(2000); + if (!isAlive()) { + throw new RouteNotConfiguredException("Route not configured for " + name()); + } + } + } + + + @SneakyThrows + public void waitReady() { + boolean accepting = isAccepting(); + if (accepting) { + return; + } + + logger.info("Waiting for " + name()); + do { + Thread.sleep(1000); + } while (!isAccepting()); + } + + + private ObservableSource retryHandler(Observable error) { + return error.flatMap(this::filterRetryableExceptions); + } + + private Observable filterRetryableExceptions(Throwable error) throws Throwable { + + synchronized (livenessMonitor) { + livenessMonitor.notifyAll(); + } + + if (error.getClass().equals(RouteNotConfiguredException.class)) { + logger.error("Network error {}", error.getMessage()); + return Observable.empty().delay(50, TimeUnit.MILLISECONDS); + } + else if (error.getClass().equals(NetworkException.class)) { + logger.error("Network error {}", error.getMessage()); + return Observable.empty().delay(1, TimeUnit.SECONDS); + } + else if (error.getClass().equals(ConnectException.class)) { + logger.error("Network error {}", error.getMessage()); + return Observable.empty().delay(1, TimeUnit.SECONDS); + } + + if (!quiet) { + if (error.getMessage() != null) { + logger.error("{} {}", error.getClass().getSimpleName(), error.getMessage()); + } + else { + logger.error("Error ", error); + } + } + throw error; + } + + private Observable validateStatus(int status, Request request) { + if (status == org.apache.http.HttpStatus.SC_OK) + return Observable.just(status); + if (status == org.apache.http.HttpStatus.SC_ACCEPTED) + return Observable.just(status); + if (status == org.apache.http.HttpStatus.SC_CREATED) + return Observable.just(status); + + return Observable.error(new RemoteException(name() + " responded status code " + status + " " + request.url())); + } + + private Response validateResponseStatus(Response response, Request req, int expected) { + if (expected != response.code()) { + response.close(); + + throw new RemoteException(name() + " responded status code " + response.code() + ", " + req.method() + " " + req.url().toString()); + } + return response; + } + + private int getResponseStatus(Response response) { + try (response) { + return response.code(); + } + } + + + @SneakyThrows + private T getEntity(Response response, Class clazz) { + try (response) { + return gson.fromJson(response.body().charStream(), clazz); + } + catch (Exception ex) { + throw ex; + } + } + @SneakyThrows + private String getText(Response response) { + try (response) { + return response.body().string(); + } + + } + + @SneakyThrows + private byte[] getBinaryEntity(Response response) { + try (response) { + return response.body().bytes(); + } + } + public boolean isAlive() { + return alive; + } + + private String json(Object o) { + try { + return gson.toJson(o); + } + catch (Exception ex) { + throw new LocalException(ex); + } + } + + private byte[] compressedJson(Object o) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + GZIPOutputStream gos = new GZIPOutputStream(baos); + try { + gson.toJson(o, new OutputStreamWriter(gos)); + gos.finish(); + return baos.toByteArray(); + } + catch (Exception ex) { + throw new LocalException(ex); + } + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/AbstractDynamicClient.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/AbstractDynamicClient.java new file mode 100644 index 00000000..dcdbbbc7 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/AbstractDynamicClient.java @@ -0,0 +1,52 @@ +package nu.marginalia.wmsa.client; + +import io.reactivex.rxjava3.core.Observable; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.server.Context; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; + +public class AbstractDynamicClient extends AbstractClient { + private final ServiceDescriptor service; + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final AbortingScheduler scheduler; + + public AbstractDynamicClient(@Nonnull ServiceDescriptor service) { + super("localhost", service.port, 10); + + this.service = service; + this.scheduler = new AbortingScheduler(name()); + } + + @Override + public String name() { + return service.name; + } + + public ServiceDescriptor getService() { + return service; + } + + @SneakyThrows + public void blockingWait() { + logger.info("Waiting for route to {}", service); + while (!isAlive()) { + Thread.sleep(1000); + } + } + + @Override + public AbortingScheduler scheduler() { + return scheduler; + } + + public Observable who(Context ctx) { + return get(ctx, "/public/who"); + } + public Observable ping(Context ctx) { + return get(ctx, "/internal/ping"); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/HttpStatusCode.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/HttpStatusCode.java new file mode 100644 index 00000000..3b39ae84 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/HttpStatusCode.java @@ -0,0 +1,19 @@ +package nu.marginalia.wmsa.client; + +public final class HttpStatusCode { + public final int code; + + public HttpStatusCode(int code) { + this.code = code; + } + + public boolean isGood() { + if (code == org.apache.http.HttpStatus.SC_OK) + return true; + if (code == org.apache.http.HttpStatus.SC_ACCEPTED) + return true; + if (code == org.apache.http.HttpStatus.SC_CREATED) + return true; + return false; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/LocalException.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/LocalException.java new file mode 100644 index 00000000..f721de69 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/LocalException.java @@ -0,0 +1,15 @@ +package nu.marginalia.wmsa.client.exception; + +public class LocalException extends MessagingException { + public LocalException() { + } + public LocalException(String message) { + super(message); + } + public LocalException(Throwable cause) { + super(cause); + } + public LocalException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/MessagingException.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/MessagingException.java new file mode 100644 index 00000000..f08b47b7 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/MessagingException.java @@ -0,0 +1,20 @@ +package nu.marginalia.wmsa.client.exception; + +public class MessagingException extends RuntimeException { + public MessagingException() { + } + public MessagingException(String message) { + super(message); + } + public MessagingException(Throwable cause) { + super(cause); + } + public MessagingException(String message, Throwable cause) { + super(message, cause); + } + + @Override + public Throwable fillInStackTrace() { + return this; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/NetworkException.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/NetworkException.java new file mode 100644 index 00000000..e39028fb --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/NetworkException.java @@ -0,0 +1,15 @@ +package nu.marginalia.wmsa.client.exception; + +public class NetworkException extends MessagingException { + public NetworkException() { + } + public NetworkException(String message) { + super(message); + } + public NetworkException(Throwable cause) { + super(cause); + } + public NetworkException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/RemoteException.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/RemoteException.java new file mode 100644 index 00000000..ed2c8645 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/RemoteException.java @@ -0,0 +1,16 @@ +package nu.marginalia.wmsa.client.exception; + +public class RemoteException extends MessagingException { + public RemoteException() { + } + public RemoteException(String message) { + super(message); + } + public RemoteException(Throwable cause) { + super(cause); + } + public RemoteException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/RouteNotConfiguredException.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/RouteNotConfiguredException.java new file mode 100644 index 00000000..7f2a4c40 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/RouteNotConfiguredException.java @@ -0,0 +1,15 @@ +package nu.marginalia.wmsa.client.exception; + +public class RouteNotConfiguredException extends MessagingException { + public RouteNotConfiguredException() { + } + public RouteNotConfiguredException(String message) { + super(message); + } + public RouteNotConfiguredException(Throwable cause) { + super(cause); + } + public RouteNotConfiguredException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/TimeoutException.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/TimeoutException.java new file mode 100644 index 00000000..35adc152 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/client/exception/TimeoutException.java @@ -0,0 +1,15 @@ +package nu.marginalia.wmsa.client.exception; + +public class TimeoutException extends MessagingException { + public TimeoutException() { + } + public TimeoutException(String message) { + super(message); + } + public TimeoutException(Throwable cause) { + super(cause); + } + public TimeoutException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/MainClass.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/MainClass.java new file mode 100644 index 00000000..ab347f46 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/MainClass.java @@ -0,0 +1,56 @@ +package nu.marginalia.wmsa.configuration; + +import io.prometheus.client.hotspot.DefaultExports; +import io.reactivex.rxjava3.exceptions.UndeliverableException; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.client.exception.NetworkException; +import org.mariadb.jdbc.Driver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.util.Arrays; + +public abstract class MainClass { + private Logger logger = LoggerFactory.getLogger(getClass()); + + public MainClass() { + + RxJavaPlugins.setErrorHandler(ex -> { + if (ex instanceof UndeliverableException) { + ex = ex.getCause(); + } + + if (ex instanceof SocketTimeoutException) { + logger.warn("SocketTimeoutException"); + } + else if (ex instanceof UnknownHostException) { + logger.warn("UnknownHostException"); + } + else if (ex instanceof NetworkException) { + logger.warn("NetworkException", ex); + } + else { + logger.error("Uncaught exception", ex); + } + }); + + } + + @SneakyThrows + protected static void init(ServiceDescriptor service, String... args) { + System.setProperty("log4j2.isThreadContextMapInheritable", "true"); + System.setProperty("isThreadContextMapInheritable", "true"); + System.setProperty("service-name", service.name); + + org.mariadb.jdbc.Driver driver = new Driver(); + + if (Arrays.asList(args).contains("go-no-go")) { + System.setProperty("go-no-go", "true"); + } + DefaultExports.initialize(); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/ServiceDescriptor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/ServiceDescriptor.java new file mode 100644 index 00000000..9bfe2a2b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/ServiceDescriptor.java @@ -0,0 +1,101 @@ +package nu.marginalia.wmsa.configuration; + +import nu.marginalia.wmsa.auth.AuthMain; +import nu.marginalia.wmsa.auth.api.ApiMain; +import nu.marginalia.wmsa.configuration.command.Command; +import nu.marginalia.wmsa.configuration.command.ListCommand; +import nu.marginalia.wmsa.configuration.command.StartCommand; +import nu.marginalia.wmsa.configuration.command.VersionCommand; +import nu.marginalia.wmsa.data_store.DataStoreMain; +import nu.marginalia.wmsa.edge.archive.EdgeArchiveMain; +import nu.marginalia.wmsa.edge.assistant.EdgeAssistantMain; +import nu.marginalia.wmsa.edge.crawler.EdgeCrawlerMain; +import nu.marginalia.wmsa.edge.dating.DatingMain; +import nu.marginalia.wmsa.edge.director.EdgeDirectorMain; +import nu.marginalia.wmsa.edge.index.EdgeIndexMain; +import nu.marginalia.wmsa.edge.search.EdgeSearchMain; +import nu.marginalia.wmsa.memex.MemexMain; +import nu.marginalia.wmsa.podcasts.PodcastScraperMain; +import nu.marginalia.wmsa.renderer.RendererMain; +import nu.marginalia.wmsa.resource_store.ResourceStoreMain; +import nu.marginalia.wmsa.smhi.scraper.SmhiScraperMain; +import org.apache.logging.log4j.core.lookup.MainMapLookup; + +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public enum ServiceDescriptor { + RESOURCE_STORE("resource-store", 5000, ResourceStoreMain.class), + DATA_STORE("data-store", 5001, DataStoreMain.class), + RENDERER("renderer", 5002, RendererMain.class), + AUTH("auth", 5003, AuthMain.class), + API("api", 5004, ApiMain.class), + + SMHI_SCRAPER("smhi-scraper",5012, SmhiScraperMain.class), + PODCST_SCRAPER("podcast-scraper", 5013, PodcastScraperMain.class), + + EDGE_CRAWLER("edge-crawler", 5020, EdgeCrawlerMain.class), + EDGE_INDEX("edge-index", 5021, EdgeIndexMain.class), + EDGE_DIRECTOR("edge-director", 5022, EdgeDirectorMain.class), + EDGE_SEARCH("edge-search", 5023, EdgeSearchMain.class), + EDGE_ARCHIVE("edge-archive", 5024, EdgeArchiveMain.class), + EDGE_ASSISTANT("edge-assistant", 5025, EdgeAssistantMain.class), + + EDGE_MEMEX("memex", 5030, MemexMain.class), + + DATING("dating", 5070, DatingMain.class), + + TEST_1("test-1", 0, null), + TEST_2("test-2", 0, null); + + public static ServiceDescriptor byName(String name) { + for (var v : values()) { + if (v.name.equals(name)) { + return v; + } + } + throw new IllegalArgumentException(name); + } + public final String name; + public final Class mainClass; + public final int port; + + ServiceDescriptor(String name, int port, Class mainClass) { + this.name = name; + this.port = port; + this.mainClass = mainClass; + } + + public String toString() { + return name; + } + + public String describeService() { + return String.format("%s %s", name, mainClass.getName()); + } + + public static void main(String... args) { + + MainMapLookup.setMainArguments(args); + Map functions = Stream.of(new ListCommand(), + new StartCommand(), + new VersionCommand() + ).collect(Collectors.toMap(c -> c.name, c -> c)); + + if(args.length > 0) { + functions.getOrDefault(args[0], new Command("") { + @Override + public void execute(String... args) { + System.err.println("Unknown command"); + System.exit(1); + } + }).execute(args); + } + else { + System.err.println("Usage: " + String.join("|", functions.keySet())); + System.exit(1); + } + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/WmsaHome.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/WmsaHome.java new file mode 100644 index 00000000..f749c9a6 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/WmsaHome.java @@ -0,0 +1,16 @@ +package nu.marginalia.wmsa.configuration; + +import java.nio.file.Files; +import java.nio.file.Path; + +public class WmsaHome { + private static final String DEFAULT = "/var/lib/wmsa"; + + public static Path get() { + var ret = Path.of(System.getProperty("WMSA_HOME", DEFAULT)); + if (!Files.isDirectory(ret)) { + throw new IllegalStateException("Could not find WMSA_HOME, either set environment variable or ensure " + DEFAULT + " exists"); + } + return ret; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/command/Command.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/command/Command.java new file mode 100644 index 00000000..5267045f --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/command/Command.java @@ -0,0 +1,31 @@ +package nu.marginalia.wmsa.configuration.command; + +import nu.marginalia.wmsa.configuration.ServiceDescriptor; + +import java.util.Arrays; +import java.util.Objects; + +public abstract class Command { + public final String name; + + protected Command(String name) { + this.name = name; + } + + public abstract void execute(String... args); + + static ServiceDescriptor getKind(String arg) { + + try { + return Arrays.stream(ServiceDescriptor.values()) + .filter(sd -> Objects.equals(arg, sd.name)) + .findFirst() + .orElseThrow(IllegalArgumentException::new) + ; + } catch (IllegalArgumentException ex) { + System.err.println("Unknown service '" + arg + "'"); + System.exit(1); + } + return null; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/command/ListCommand.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/command/ListCommand.java new file mode 100644 index 00000000..0bd2c3eb --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/command/ListCommand.java @@ -0,0 +1,23 @@ +package nu.marginalia.wmsa.configuration.command; + +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; + +import java.util.Arrays; +import java.util.Objects; + +public class ListCommand extends Command { + public ListCommand() { + super("list"); + } + + @Override + @SneakyThrows + public void execute(String... args) { + Arrays.stream(ServiceDescriptor.values()) + .filter(sd -> Objects.nonNull(sd.mainClass)) + .map(ServiceDescriptor::describeService) + .forEach(System.out::println); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/command/StartCommand.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/command/StartCommand.java new file mode 100644 index 00000000..55d46813 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/command/StartCommand.java @@ -0,0 +1,24 @@ +package nu.marginalia.wmsa.configuration.command; + +import lombok.SneakyThrows; + +import java.util.Arrays; + +public class StartCommand extends Command { + public StartCommand() { + super("start"); + } + + @Override + @SneakyThrows + public void execute(String... args) { + if (args.length < 2) { + System.err.println("Usage: start service-descriptor"); + System.exit(255); + } + + var mainMethod = getKind(args[1]).mainClass.getMethod("main", String[].class); + String[] args2 = Arrays.copyOfRange(args, 2, args.length); + mainMethod.invoke(null, (Object) args2); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/command/VersionCommand.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/command/VersionCommand.java new file mode 100644 index 00000000..179a0300 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/command/VersionCommand.java @@ -0,0 +1,20 @@ +package nu.marginalia.wmsa.configuration.command; + +import lombok.SneakyThrows; + +public class VersionCommand extends Command { + public VersionCommand() { + super("version"); + } + + @Override + @SneakyThrows + public void execute(String... args) { + try (var str = ClassLoader.getSystemResourceAsStream("_version.txt")) { + if (null == str) { + System.err.println("Bad jar, missing _version.txt"); + return; + } + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/ConfigurationModule.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/ConfigurationModule.java new file mode 100644 index 00000000..9f762cb1 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/ConfigurationModule.java @@ -0,0 +1,45 @@ +package nu.marginalia.wmsa.configuration.module; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import lombok.SneakyThrows; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Objects; + +import static com.google.inject.name.Names.named; + +public class ConfigurationModule extends AbstractModule { + private static final String SERVICE_NAME = System.getProperty("service-name"); + public static int MONITOR_PORT = Integer.getInteger("monitor.port", 5000); + public static String MONITOR_HOST = System.getProperty("monitor.host", "127.0.0.1"); + + public void configure() { + bind(Integer.class).annotatedWith(named("monitor-port")).toInstance(MONITOR_PORT); + bind(String.class).annotatedWith(named("monitor-host")).toInstance(MONITOR_HOST); + bind(Integer.class).annotatedWith(named("monitor-boot-timeout")).toInstance(10); + + bind(String.class).annotatedWith(named("service-name")).toInstance(Objects.requireNonNull(SERVICE_NAME)); + bind(String.class).annotatedWith(named("service-host")).toProvider(HostnameProvider.class).in(Singleton.class); + bind(Integer.class).annotatedWith(named("service-port")).toProvider(PortProvider.class).in(Singleton.class); + bind(Integer.class).annotatedWith(named("metrics-server-port")).toProvider(MetricsPortProvider.class).in(Singleton.class); + + } + + @Provides + @Named("build-version") + @SneakyThrows + public String buildVersion() { + try (var str = ClassLoader.getSystemResourceAsStream("_version.txt")) { + if (null == str) { + System.err.println("Missing _version.txt from classpath"); + return LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } + return new String(str.readAllBytes()); + } + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/DatabaseModule.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/DatabaseModule.java new file mode 100644 index 00000000..8ce96c3a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/DatabaseModule.java @@ -0,0 +1,114 @@ +package nu.marginalia.wmsa.configuration.module; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.WmsaHome; +import org.h2.tools.RunScript; +import org.mariadb.jdbc.Driver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; + +public class DatabaseModule extends AbstractModule { + private static final Logger logger = LoggerFactory.getLogger(DatabaseModule.class); + + private static final String DB_USER_KEY="db.user"; + private static final String DB_PASS_KEY ="db.pass"; + private static final String DB_CONN_KEY ="db.conn"; + + private final Properties dbProperties; + + public DatabaseModule() { + new Driver(); + + dbProperties = loadDbProperties(); + } + + private Properties loadDbProperties() { + Path propDir = WmsaHome.get().resolve("db.properties"); + if (!Files.isRegularFile(propDir)) { + throw new IllegalStateException("Database properties file " + propDir + " does not exist"); + } + + try (var is = new FileInputStream(propDir.toFile())) { + var props = new Properties(); + props.load(is); + + if (!props.containsKey(DB_USER_KEY)) throw new IllegalStateException(propDir + " missing required attribute " + DB_USER_KEY); + if (!props.containsKey(DB_PASS_KEY)) throw new IllegalStateException(propDir + " missing required attribute " + DB_PASS_KEY); + if (!props.containsKey(DB_CONN_KEY)) throw new IllegalStateException(propDir + " missing required attribute " + DB_CONN_KEY); + + return props; + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + + } + + @SneakyThrows + @Singleton + @Provides + public HikariDataSource provideConnection() { + if (Boolean.getBoolean("data-store-h2")) { + return getH2(); + } + else { + return getMariaDB(); + } + + } + + @SneakyThrows + private HikariDataSource getMariaDB() { + var connStr = dbProperties.getProperty(DB_CONN_KEY); + + try { + HikariConfig config = new HikariConfig(); + + + config.setJdbcUrl(connStr); + config.setUsername(dbProperties.getProperty(DB_USER_KEY)); + config.setPassword(dbProperties.getProperty(DB_PASS_KEY)); + + config.addDataSourceProperty("cachePrepStmts", "true"); + config.addDataSourceProperty("prepStmtCacheSize", "250"); + config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + config.setMaximumPoolSize(100); + config.setMinimumIdle(10); + return new HikariDataSource(config); + } + finally { + logger.info("Created HikariPool for {}", connStr); + } + } + + + @SneakyThrows + private HikariDataSource getH2() { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl("jdbc:h2:~/wmsa-db"); + config.setUsername("wmsa"); + config.setPassword(""); + + var ds = new HikariDataSource(config); + + try (var stream = ClassLoader.getSystemResourceAsStream("sql/data-store-init.sql")) { + RunScript.execute(ds.getConnection(), new InputStreamReader(stream)); + } + try (var stream = ClassLoader.getSystemResourceAsStream("sql/edge-crawler-cache.sql")) { + RunScript.execute(ds.getConnection(), new InputStreamReader(stream)); + } + return ds; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/HostnameProvider.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/HostnameProvider.java new file mode 100644 index 00000000..86276f91 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/HostnameProvider.java @@ -0,0 +1,36 @@ +package nu.marginalia.wmsa.configuration.module; + +import com.google.inject.name.Named; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Provider; + +public class HostnameProvider implements Provider { + private static final String DEFAULT_HOSTNAME = "127.0.0.1"; + private final int monitorPort; + private final String monitorHost; + private final int timeout; + private Logger logger = LoggerFactory.getLogger(getClass()); + + @Inject + public HostnameProvider(@Named("monitor-port") Integer monitorPort, + @Named("monitor-host") String monitorHost, + @Named("monitor-boot-timeout") Integer timeout + ) { + this.monitorHost = monitorHost; + this.monitorPort = monitorPort; + this.timeout = timeout; + } + + @Override + public String get() { + var override = System.getProperty("service-host"); + if (null != override) { + return override; + } + return DEFAULT_HOSTNAME; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/LoggerConfiguration.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/LoggerConfiguration.java new file mode 100644 index 00000000..9557b433 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/LoggerConfiguration.java @@ -0,0 +1,13 @@ +package nu.marginalia.wmsa.configuration.module; + +import com.google.inject.name.Named; + +import javax.inject.Inject; + +public class LoggerConfiguration { + @Inject + public LoggerConfiguration(@Named("service-name") String serviceName) { + System.setProperty("service-name", serviceName); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/MetricsPortProvider.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/MetricsPortProvider.java new file mode 100644 index 00000000..cca5bbfc --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/MetricsPortProvider.java @@ -0,0 +1,21 @@ +package nu.marginalia.wmsa.configuration.module; + +import com.google.inject.name.Named; + +import javax.inject.Inject; +import javax.inject.Provider; + +public class MetricsPortProvider implements Provider { + private final Integer servicePort; + + @Inject + public MetricsPortProvider(@Named("service-port") Integer servicePort) { + this.servicePort = servicePort; + } + + @Override + public Integer get() { + return servicePort+1000; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/PortProvider.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/PortProvider.java new file mode 100644 index 00000000..090bc46d --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/module/PortProvider.java @@ -0,0 +1,46 @@ +package nu.marginalia.wmsa.configuration.module; + +import com.google.inject.name.Named; +import io.reactivex.rxjava3.core.Flowable; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import org.apache.http.HttpResponse; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Provider; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +public class PortProvider implements Provider { + private static final Integer DEFAULT_PORT = 5000; + private final int monitorPort; + private final String monitorHost; + private Logger logger = LoggerFactory.getLogger(getClass()); + private final int timeout = 10; + @Inject + public PortProvider(@Named("monitor-port") Integer monitorPort, + @Named("monitor-host") String monitorHost, + @Named("monitor-boot-timeout") Integer timeout) { + this.monitorHost = monitorHost; + this.monitorPort = monitorPort; + } + + @Override + public Integer get() { + return ServiceDescriptor.byName(System.getProperty("service-name")).port; + } + + private Publisher repeatDelay(Flowable error) { + return error.delay(1, TimeUnit.SECONDS); + } + + private String accept200(HttpResponse rsp) throws IOException { + if (rsp.getStatusLine().getStatusCode() != 200) { + throw new RuntimeException("Monitor responded unexpected status " + + rsp.getStatusLine().getStatusCode()); + } + return new String(rsp.getEntity().getContent().readAllBytes()); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/Context.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/Context.java new file mode 100644 index 00000000..b98e5d3b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/Context.java @@ -0,0 +1,139 @@ +package nu.marginalia.wmsa.configuration.server; + +import io.reactivex.rxjava3.schedulers.Schedulers; +import org.apache.logging.log4j.ThreadContext; +import spark.Request; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +public class Context { + public static final String CONTEXT_HEADER = "X-Context"; + public static final String SESSION_HEADER = "Cookie"; + public static final String PUBLIC_HEADER = "X-Public"; + private static final Random random; + + private static volatile byte[] seed = new byte[12]; + + private static byte[] generateSalt() { + byte[] oldHash = seed; + + int hash1 = Long.hashCode(random.nextLong()); + int hash2 = Objects.hash(System.currentTimeMillis()); + int hash3 = Arrays.hashCode(oldHash); + + return new byte[]{ + (byte) (hash1 & 0xFF), + (byte) (hash1 >>> 8 & 0xFF), + (byte) (hash1 >>> 16 & 0xFF), + (byte) (hash1 >>> 24 & 0xFF), + (byte) (hash2 & 0xFF), + (byte) (hash2 >>> 8 & 0xFF), + (byte) (hash2 >>> 16 & 0xFF), + (byte) (hash2 >>> 24 & 0xFF), + (byte) (hash3 & 0xFF), + (byte) (hash3 >>> 8 & 0xFF), + (byte) (hash3 >>> 16 & 0xFF), + (byte) (hash3 >>> 24 & 0xFF) + }; + } + + static { + random = new Random(); + for (int i = 0; i < 1_000_000; i++) { + random.nextLong(); + } + random.nextBytes(seed); + + updateSeed(); + } + + private static void updateSeed() { + seed = generateSalt(); + + Schedulers.computation().scheduleDirect(Context::updateSeed, + 60*5000 + (int)(1000*60*10*Math.random()), + TimeUnit.MILLISECONDS); + } + + private String id; + private String session; + private boolean treatAsPublic; + + private Context(String id, String session) { + this.id = id; + this.session = session; + } + + public Context treatAsPublic() { + this.treatAsPublic = true; + return this; + } + + public static Context internal() { + return new Context(UUID.randomUUID().toString(), null); + } + public static Context internal(String hwat) { + return new Context(hwat, null); + } + + public static Context fromRequest(Request request) { + + if (Boolean.getBoolean("unit-test")) { + return Context.internal(); + } + + final var ctxHeader = hashPublicIp(request.headers(CONTEXT_HEADER)); + final var sessHeader = request.headers(SESSION_HEADER); + + ThreadContext.put("context", ctxHeader+"-"+sessHeader); + ThreadContext.put("outbound-request", "none"); + + return new Context(ctxHeader, sessHeader); + } + + private static String hashPublicIp(String header) { + + if (header != null && header.contains("-")) { + + byte[] hashData = Arrays.copyOf(seed, seed.length+4); + int hashi = Objects.hash(header.split("-", 2)[0]); + + for (int i = 0; i < 4; i++) { + hashData[seed.length] = (byte)(hashi & 0xFF); + hashData[seed.length+1] = (byte)(hashi>>>8 & 0xFF); + hashData[seed.length+2] = (byte)(hashi>>>16 & 0xFF); + hashData[seed.length+3] = (byte)(hashi>>>24 & 0xFF); + } + + return String.format("#%x", Arrays.hashCode(hashData)); + } + else { + return header; + } + } + + public okhttp3.Request.Builder paint(okhttp3.Request.Builder requestBuilder) { + requestBuilder.addHeader(CONTEXT_HEADER, id); + + if (session != null) { + requestBuilder.addHeader(SESSION_HEADER, session); + } + + if (treatAsPublic) { + requestBuilder.header(PUBLIC_HEADER, "1"); + } + + return requestBuilder; + } + + public Optional getIpHash() { + + if (id.startsWith("#")) { + return Optional.of(id); + } + + return Optional.empty(); + } + +} \ No newline at end of file diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/Initialization.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/Initialization.java new file mode 100644 index 00000000..6b146672 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/Initialization.java @@ -0,0 +1,47 @@ +package nu.marginalia.wmsa.configuration.server; + +import com.google.inject.Singleton; +import lombok.SneakyThrows; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public class Initialization { + boolean initialized; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + public static Initialization already() { + Initialization init = new Initialization(); + init.setReady(); + return init; + } + + public void setReady() { + synchronized (this) { + logger.info("Initialized"); + initialized = true; + notifyAll(); + } + + if (Boolean.getBoolean("go-no-go")) { + logger.info("Self-test OK"); + System.exit(0); + } + } + + public boolean isReady() { + synchronized (this) { + return initialized; + } + } + + @SneakyThrows + public boolean waitReady() { + synchronized (this) { + while (!initialized) { + wait(); + } + return initialized; + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/MetricsServer.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/MetricsServer.java new file mode 100644 index 00000000..c8da5e97 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/MetricsServer.java @@ -0,0 +1,25 @@ +package nu.marginalia.wmsa.configuration.server; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import io.prometheus.client.exporter.MetricsServlet; +import lombok.SneakyThrows; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; + +public class MetricsServer { + + @SneakyThrows + @Inject + public MetricsServer(@Named("metrics-server-port") int port) { + Server server = new Server(port); + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/"); + server.setHandler(context); + + context.addServlet(new ServletHolder(new MetricsServlet()), "/metrics"); + + server.start(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/RateLimiter.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/RateLimiter.java new file mode 100644 index 00000000..4dc4c8da --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/RateLimiter.java @@ -0,0 +1,71 @@ +package nu.marginalia.wmsa.configuration.server; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Bucket; +import io.github.bucket4j.Bucket4j; +import io.github.bucket4j.Refill; +import io.reactivex.rxjava3.schedulers.Schedulers; + +import java.time.Duration; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +public class RateLimiter { + + private final Map bucketMap = new ConcurrentHashMap<>(); + + private final int capacity; + private final int refillRate; + + public RateLimiter(int capacity, int refillRate) { + this.capacity = capacity; + this.refillRate = refillRate; + + Schedulers.io().schedulePeriodicallyDirect(this::cleanIdleBuckets, 30, 30, TimeUnit.MINUTES); + } + + + public static RateLimiter forExpensiveRequest() { + return new RateLimiter(5, 10); + } + + public static RateLimiter custom(int perMinute) { + return new RateLimiter(perMinute, 60); + } + + public static RateLimiter forSpamBots() { + return new RateLimiter(120, 3600); + } + + + public static RateLimiter forLogin() { + return new RateLimiter(3, 15); + } + + private void cleanIdleBuckets() { + bucketMap.clear(); + } + + public boolean isAllowed(Context ctx) { + final Optional maybeIp = ctx.getIpHash(); + + if (maybeIp.isEmpty()) { // Internal server->server request + return true; + } + + return bucketMap.computeIfAbsent(maybeIp.get(), + (ip) -> createBucket()).tryConsume(1); + } + + public boolean isAllowed() { + return bucketMap.computeIfAbsent("any", + (ip) -> createBucket()).tryConsume(1); + } + private Bucket createBucket() { + var refill = Refill.greedy(1, Duration.ofSeconds(refillRate)); + var bw = Bandwidth.classic(capacity, refill); + return Bucket4j.builder().addLimit(bw).build(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/Service.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/Service.java new file mode 100644 index 00000000..c9f618da --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/configuration/server/Service.java @@ -0,0 +1,146 @@ +package nu.marginalia.wmsa.configuration.server; + +import com.google.common.base.Strings; +import io.prometheus.client.Counter; +import nu.marginalia.wmsa.client.exception.MessagingException; +import org.apache.http.HttpStatus; +import org.apache.logging.log4j.ThreadContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; +import spark.Request; +import spark.Response; +import spark.Spark; + +import java.util.Optional; + +public class Service { + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Marker httpMarker = MarkerFactory.getMarker("HTTP"); + + private final Initialization initialization; + + private final static Counter request_counter = Counter.build("wmsa_service_in_request_counter", "Request Counter") + .labelNames("service") + .register(); + private final static Counter request_counter_good = Counter.build("wmsa_service_good_request_counter", "Good Requests") + .labelNames("service") + .register(); + private final static Counter request_counter_bad = Counter.build("wmsa_service_bad_request_counter", "Bad Requests") + .labelNames("service") + .register(); + private final static Counter request_counter_err = Counter.build("wmsa_service_error_request_counter", "Error Requests") + .labelNames("service") + .register(); + private final String serviceName; + + private static volatile boolean initialized = false; + + public Service(String ip, int port, Initialization initialization, MetricsServer metricsServer) { + this.initialization = initialization; + + serviceName = System.getProperty("service-name"); + + if (!initialization.isReady() && ! initialized ) { + initialized = true; + + Spark.threadPool(32, 4, 60_000); + Spark.ipAddress(ip); + Spark.port(port); + + logger.info("{} Listening to {}:{}", getClass().getSimpleName(), ip == null ? "" : ip, port); + + Spark.staticFiles.expireTime(3600); + Spark.staticFiles.header("Cache-control", "public"); + + Spark.before(this::filterPublicRequests); + Spark.before(this::auditRequestIn); + Spark.after(this::auditRequestOut); + Spark.exception(MessagingException.class, this::handleException); + + Spark.get("/internal/ping", (rq,rp) -> "pong"); + Spark.get("/internal/started", this::isInitialized); + Spark.get("/internal/ready", this::isReady); + Spark.get("/public/who", (rq,rp) -> getClass().getSimpleName()); + } + } + + private void filterPublicRequests(Request request, Response response) { + if (null != request.headers("X-Public")) { + + String context = Optional + .ofNullable(request.headers("X-Context")) + .orElseGet(request::ip); + + if (!request.pathInfo().startsWith("/public/")) { + logger.warn(httpMarker, "External connection to internal API: {} -> {} {}", context, request.requestMethod(), request.pathInfo()); + Spark.halt(HttpStatus.SC_FORBIDDEN); + } + + String url = request.pathInfo(); + if (request.queryString() != null) { + url = url + "?" + request.queryString(); + } + logger.info(httpMarker, "PUBLIC {}: {} {}", Context.fromRequest(request).getIpHash().orElse("?"), request.requestMethod(), url); + } + } + + private Object isInitialized(Request request, Response response) { + if (initialization.isReady()) { + return "ok"; + } + else { + response.status(HttpStatus.SC_FAILED_DEPENDENCY); + return "bad"; + } + } + + public boolean isReady() { + return true; + } + + private String isReady(Request request, Response response) { + if (isReady()) { + return "ok"; + } + else { + response.status(HttpStatus.SC_FAILED_DEPENDENCY); + return "bad"; + } + } + + private void auditRequestIn(Request request, Response response) { + request_counter.labels(serviceName).inc(); + + // Paint context + if (!Strings.isNullOrEmpty(request.headers(Context.CONTEXT_HEADER))) { + Context.fromRequest(request); + } + } + private void auditRequestOut(Request request, Response response) { + ThreadContext.clearMap(); + + if (response.status() < 400) { + request_counter_good.labels(serviceName).inc(); + } + else { + request_counter_bad.labels(serviceName).inc(); + } + + if (null != request.headers("X-Public")) { + logger.info(httpMarker, "RSP {}", response.status()); + } + } + + private void handleException(Exception ex, Request request, Response response) { + request_counter_err.labels(serviceName).inc(); + if (ex instanceof MessagingException) { + logger.error("{} {}", ex.getClass().getSimpleName(), ex.getMessage()); + } + else { + logger.error("Uncaught exception", ex); + } + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/DataStoreMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/DataStoreMain.java new file mode 100644 index 00000000..56c61293 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/DataStoreMain.java @@ -0,0 +1,35 @@ +package nu.marginalia.wmsa.data_store; + +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import nu.marginalia.wmsa.configuration.MainClass; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.module.ConfigurationModule; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.edge.index.EdgeTablesModule; + +import java.io.IOException; + +public class DataStoreMain extends MainClass { + private DataStoreService service; + + @Inject + public DataStoreMain(DataStoreService service) throws IOException { + this.service = service; + } + + public static void main(String... args) { + init(ServiceDescriptor.DATA_STORE, args); + Injector injector = Guice.createInjector( + new DataStoreModule(), + new EdgeTablesModule(), + new DatabaseModule(), + new ConfigurationModule() + ); + injector.getInstance(DataStoreMain.class); + + injector.getInstance(Initialization.class).setReady(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/DataStoreModule.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/DataStoreModule.java new file mode 100644 index 00000000..185b16c5 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/DataStoreModule.java @@ -0,0 +1,14 @@ +package nu.marginalia.wmsa.data_store; + +import com.google.inject.AbstractModule; +import com.google.inject.name.Names; + +public class DataStoreModule extends AbstractModule { + + public void configure() { + bind(String.class).annotatedWith(Names.named("file-storage-dir")).toInstance("/tmp/files"); + bind(String.class).annotatedWith(Names.named("distro-file-name")).toInstance("wmsa.jar"); + bind(String.class).annotatedWith(Names.named("file-tmp-dir")).toInstance("/tmp"); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/DataStoreService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/DataStoreService.java new file mode 100644 index 00000000..3aa8799a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/DataStoreService.java @@ -0,0 +1,152 @@ +package nu.marginalia.wmsa.data_store; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.inject.Inject; +import com.google.inject.name.Named; +import com.zaxxer.hikari.HikariDataSource; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.configuration.server.MetricsServer; +import nu.marginalia.wmsa.configuration.server.Service; +import org.eclipse.jetty.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Request; +import spark.Response; +import spark.Spark; + +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +import static spark.Spark.*; + +public class DataStoreService extends Service { + private final HikariDataSource dataSource; + private final EdgeDataStoreService edgeService; + private Logger logger = LoggerFactory.getLogger(getClass()); + private final Gson gson = new GsonBuilder().create(); + + @Inject + public DataStoreService( + @Named("service-host") String ip, + @Named("service-port") Integer port, + FileRepository fileRepo, + HikariDataSource dataSource, + EdgeDataStoreService edgeService, + Initialization init, + MetricsServer metricsServer + ) { + super(ip, port, init, metricsServer); + + this.dataSource = dataSource; + this.edgeService = edgeService; + + Spark.get("data/:domain/:model/:resource", this::getResource); + Spark.get("data/:domain/:model", this::getResourceIdsForModel, this::convertToJson); + post("data/:domain/:model/:resource", this::storeResource); + + post("release", fileRepo::uploadFile); + Spark.get("release", fileRepo::downloadFile); + Spark.get("release/upload", fileRepo::uploadForm); + Spark.get("release/version", fileRepo::version); + + Spark.path("edge", () -> { + post("/domain-alias/*/*", edgeService::putDomainAlias, this::convertToJson); + post("/link", edgeService::putLink, this::convertToJson); + + post("/url", edgeService::putUrl, this::convertToJson); + post("/url-visited", edgeService::putUrlVisited, this::convertToJson); + get("/url/:id", edgeService::getUrlName, this::convertToJson); + get("/domain-id/*", edgeService::getDomainId, this::convertToJson); + get("/domain/:id", edgeService::getDomainName, this::convertToJson); + get("/meta/:site", edgeService::domainInfo, this::convertToJson); + + }); + + + } + + private String convertToJson(Object o) { + return gson.toJson(o); + } + + @SneakyThrows + private Object getResourceIdsForModel(Request request, Response response) { + try (var connection = dataSource.getConnection()) { + + String model = request.params("model"); + String domain = request.params("domain"); + + String query = String.format("SELECT ID FROM JSON_DATA WHERE MODEL='%s' AND DOM='%s'", + model, domain); + + + List ids = new ArrayList<>(); + try (var stmt = connection.createStatement()) { + var rs = stmt.executeQuery(query); + while (rs.next()) { + ids.add(rs.getString(1)); + } + } + + return ids; + } + } + + + @SneakyThrows + private Object getResource(Request request, Response response) { + try (var connection = dataSource.getConnection()) { + + String resource = request.params("resource"); + String model = request.params("model"); + String domain = request.params("domain"); + + String query = String.format("SELECT DATA FROM JSON_DATA WHERE ID='%s' AND MODEL='%s' AND DOM='%s'", + resource, model, domain); + + try (var stmt = connection.createStatement()) { + var rs = stmt.executeQuery(query); + if (!rs.next()) { + halt(404); + } + + rs.getAsciiStream(1).transferTo(response.raw().getOutputStream()); + + if (rs.next()) { + logger.warn("Duplicate data for {}/{}/{}", domain, model, resource); + } + } + + return ""; + } + } + + @SneakyThrows + private Object storeResource(Request request, Response response) { + try (var connection = dataSource.getConnection()) { + + String resource = request.params("resource"); + String model = request.params("model"); + String domain = request.params("domain"); + + try (var stmt = connection.prepareStatement("INSERT INTO JSON_DATA(dom, model, id, data) VALUES (?,?,?,?)")) { + stmt.setString(1, domain); + stmt.setString(2, model); + stmt.setString(3, resource); + stmt.setCharacterStream(4, new InputStreamReader(request.raw().getInputStream())); + + stmt.executeUpdate(); + + if (stmt.getUpdateCount() != 1) { + logger.warn("Query failed"); + halt(500); + } + } + halt(HttpStatus.ACCEPTED_202); + return null; + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/EdgeDataStoreService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/EdgeDataStoreService.java new file mode 100644 index 00000000..febbe8d3 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/EdgeDataStoreService.java @@ -0,0 +1,201 @@ +package nu.marginalia.wmsa.data_store; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import io.prometheus.client.Histogram; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.data_store.meta.DomainInformation; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDao; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainLink; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlVisit; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.util.UrlEncoded; +import spark.Request; +import spark.Response; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URISyntaxException; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.zip.GZIPInputStream; + +@Singleton +public class EdgeDataStoreService { + + private final EdgeDataStoreDao dataStore; + private Gson gson = new GsonBuilder().create(); + + + static final Histogram request_time_metrics + = Histogram.build("wmsa_edge_data_store_request_time", "DB Request Time") + .labelNames("request") + .register(); + + @Inject + public EdgeDataStoreService(EdgeDataStoreDao dataStore) { + this.dataStore = dataStore; + } + + @SneakyThrows + public Object putLink(Request request, Response response) { + final long start = System.currentTimeMillis(); + + var model = readFromJson(request, EdgeDomainLink[].class); + + dataStore.putLink(false, model); + + request_time_metrics.labels("put_link").observe(System.currentTimeMillis() - start); + + response.status(HttpStatus.CREATED_201); + return ""; + } + + @SneakyThrows + public Object putUrl(Request request, Response response) { + final long start = System.currentTimeMillis(); + + var model = readFromJson(request, EdgeUrl[].class); + + double quality = Double.parseDouble(request.queryParams("quality")); + dataStore.putUrl(quality, model); + + request_time_metrics.labels("put_url").observe(System.currentTimeMillis() - start); + + response.status(HttpStatus.CREATED_201); + return ""; + } + + @SneakyThrows + public Object putUrlVisited(Request request, Response response) { + final long start = System.currentTimeMillis(); + + var model = readFromJson(request, EdgeUrlVisit[].class); + + dataStore.putUrlVisited(model); + + request_time_metrics.labels("put_url_visited").observe(System.currentTimeMillis() - start); + + response.status(HttpStatus.CREATED_201); + return ""; + } + + public Object putDomainAlias(Request request, Response response) { + final long start = System.currentTimeMillis(); + + var src = UrlEncoded.decodeString(request.splat()[0]); + var dst = UrlEncoded.decodeString(request.splat()[1]); + + dataStore.putDomainAlias(new EdgeDomain(src), new EdgeDomain(dst)); + + request_time_metrics.labels("put_domain_alias").observe(System.currentTimeMillis() - start); + + response.status(HttpStatus.ACCEPTED_202); + return ""; + } + + public Object getUrlName(Request request, Response response) { + final long start = System.currentTimeMillis(); + + try { + var id = Integer.parseInt(request.params("id")); + var ret = dataStore.getUrl(new EdgeId<>(id)); + + request_time_metrics.labels("get_url_name").observe(System.currentTimeMillis() - start); + return ret; + } + catch (NoSuchElementException ex) { + response.status(404); + return ""; + } + + } + + public Object getDomainId(Request request, Response response) { + final long start = System.currentTimeMillis(); + + var domain = UrlEncoded.decodeString(request.splat()[0]); + + try { + var ret = dataStore.getDomainId(new EdgeDomain(domain)); + + request_time_metrics.labels("get_domain_id").observe(System.currentTimeMillis() - start); + return ret; + } + catch (NoSuchElementException ex) { + response.status(404); + return ""; + } + + } + + public Object getDomainName(Request request, Response response) { + final long start = System.currentTimeMillis(); + + try { + var id = Integer.parseInt(request.params("id")); + var ret = dataStore.getDomain(new EdgeId<>(id)); + + request_time_metrics.labels("get_domain_name").observe(System.currentTimeMillis() - start); + return ret; + } + catch (NoSuchElementException ex) { + response.status(404); + return ""; + } + + } + + public T readFromJson(Request request, Class clazz) throws IOException { + if ("gzip".equals(request.headers("Content-Encoding"))) { + return gson.fromJson(new InputStreamReader(new GZIPInputStream(request.raw().getInputStream())), clazz); + } + else { + return gson.fromJson(new InputStreamReader(request.raw().getInputStream()), clazz); + } + } + + + public DomainInformation domainInfo(Request request, Response response) throws URISyntaxException { + final String site = request.params("site"); + + EdgeId domainId = getDomainFromPartial(site); + if (domainId == null) { + response.status(404); + return null; + } + EdgeDomain domain = dataStore.getDomain(domainId); + + boolean blacklisted = dataStore.isBlacklisted(domain); + int pagesKnown = dataStore.getPagesKnown(domainId); + int pagesVisited = dataStore.getPagesVisited(domainId); + int pagesIndexed = dataStore.getPagesIndexed(domainId); + int incomingLinks = dataStore.getIncomingLinks(domainId); + int outboundLinks = dataStore.getOutboundLinks(domainId); + double rank = Math.round(10000.0*(1.0-dataStore.getRank(domainId)))/100; + EdgeDomainIndexingState state = dataStore.getDomainState(domainId); + double nominalQuality = Math.round(100*100*Math.exp(dataStore.getDomainQuality(domainId)))/100.; + List linkingDomains = dataStore.getLinkingDomains(domainId); + + return new DomainInformation(domain, blacklisted, pagesKnown, pagesVisited, pagesIndexed, incomingLinks, outboundLinks, nominalQuality, rank, state, linkingDomains); + } + + private EdgeId getDomainFromPartial(String site) throws URISyntaxException { + try { + return dataStore.getDomainId(new EdgeDomain(site)); + } + catch (Exception ex) { + try { + return dataStore.getDomainId(new EdgeDomain(site)); + } + catch (Exception ex2) { + return null; + } + } + + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/FileRepository.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/FileRepository.java new file mode 100644 index 00000000..54c023fd --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/FileRepository.java @@ -0,0 +1,138 @@ +package nu.marginalia.wmsa.data_store; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import lombok.SneakyThrows; +import org.eclipse.jetty.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Request; +import spark.Response; + +import javax.servlet.MultipartConfigElement; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.zip.ZipFile; + + +public class FileRepository { + @Inject @Named("file-storage-dir") + private String fileStoreDir; + + @Inject @Named("file-tmp-dir") + private String fileTempDir; + + @Inject @Named("distro-file-name") + private String distroFileName; + + private String version; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + ReadWriteLock rwl = new ReentrantReadWriteLock(); + + @SneakyThrows + public Object uploadFile(Request request, Response response) { + + request.attribute("org.eclipse.jetty.multipartConfig", + new MultipartConfigElement(fileTempDir, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE) + ); + + final var part = Objects.requireNonNull(request.raw().getPart("uploaded_file"), "Missing part"); + + var lock = rwl.writeLock(); + try (InputStream is = part.getInputStream()) { + lock.lock(); + + var tempPath + = Files.createTempFile(Path.of(fileStoreDir), + "upload-", ".jar"); + + var tempFile = tempPath.toFile(); + + try (var os = new FileOutputStream(tempFile)) { + is.transferTo(os); + } + + var oldVersion = Optional.ofNullable(version) + .orElseGet(() -> readJarVersion(getReleasePath().toFile())); + var newVersion = readJarVersion(tempFile); + + logger.info("Uploading new version {}, replacing {}", newVersion, oldVersion); + + Files.move(tempPath, Path.of(fileStoreDir).resolve(distroFileName), + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + + version = newVersion; + } + finally { + lock.unlock(); + } + + response.status(HttpStatus.ACCEPTED_202); + return ""; + } + + @SneakyThrows + private String readJarVersion(File tempFile) { + try (var zipFile = new ZipFile(tempFile)) { + return new String(zipFile.getInputStream(zipFile.getEntry("_version.txt")).readAllBytes()); + } + } + + @SneakyThrows + public Object downloadFile(Request request, Response response) { + response.type("application/java-archive"); + response.header("Content-Disposition", "attachment; filename=" + distroFileName); + + Lock lock = rwl.readLock(); + try (var is = new FileInputStream(getReleasePath().toFile())) { + lock.lock(); + is.transferTo(response.raw().getOutputStream()); + } + finally { + lock.unlock(); + } + return ""; + }; + + private Path getReleasePath() { + return Path.of(fileStoreDir, distroFileName); + } + + @SneakyThrows + public synchronized Object version(Request request, Response response) { + Lock lock = rwl.readLock(); + try { + lock.lock(); + + if (null != version) { + return version; + } + + return readJarVersion(getReleasePath().toFile()); + } + finally { + lock.unlock(); + } + } + + public Object uploadForm(Request request, Response response) { + return "
    " // note the enctype + + " " + + " " + + "
    "; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/client/DataStoreClient.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/client/DataStoreClient.java new file mode 100644 index 00000000..6fec2ca8 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/client/DataStoreClient.java @@ -0,0 +1,106 @@ +package nu.marginalia.wmsa.data_store.client; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import io.reactivex.rxjava3.core.Observable; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.client.AbstractDynamicClient; +import nu.marginalia.wmsa.client.HttpStatusCode; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.data_store.meta.DomainInformation; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainLink; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlVisit; +import org.eclipse.jetty.util.UrlEncoded; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.CheckReturnValue; +import javax.inject.Inject; +import java.util.List; + +public class DataStoreClient extends AbstractDynamicClient { + private final Gson gson = new GsonBuilder() + .create(); + + private final Logger logger = LoggerFactory.getLogger(getClass()); + @Inject + public DataStoreClient() { + super(ServiceDescriptor.DATA_STORE); + } + + @CheckReturnValue + public Observable getJson(Context ctx, Class type, String domain, String resource) { + var route = "/data/"+domain+"/"+type.getSimpleName()+"/"+resource; + + return super.get(ctx, route, type); + } + + @CheckReturnValue + @SuppressWarnings("unchecked") + public Observable> getJsonIndicies(Context ctx, Class type, String domain) { + var route = "/data/"+domain+"/"+type.getSimpleName(); + + return super.get(ctx, route) + .map(data -> (List) gson.fromJson(data, List.class)) + ; + } + + @CheckReturnValue + @SneakyThrows + public Observable offerJson(Context ctx, Class type, T object, String domain, String resource) { + var route = "/data/"+domain+"/"+type.getSimpleName()+"/"+resource; + return super.post(ctx, route, object) + ; + } + @CheckReturnValue + @Deprecated + public Observable putLink(Context ctx, EdgeDomainLink... data) { + return super.post(ctx, "/edge/link", data); + } + @CheckReturnValue + @Deprecated + public Observable putUrl(Context ctx, double quality, EdgeUrl... data) { + return super.post(ctx, "/edge/url?quality="+quality, data); + } + @CheckReturnValue + @Deprecated + public Observable putUrlVisited(Context ctx, EdgeUrlVisit... data) { + return super.post(ctx, "/edge/url-visited", data); + } + @CheckReturnValue + @Deprecated + public Observable putDomainAlias(Context ctx, EdgeDomain source, EdgeDomain dest) { + var srcEnc = UrlEncoded.encodeString(source.toString()); + var dstEnc = UrlEncoded.encodeString(dest.toString()); + + return super.post(ctx, "/edge/domain-alias/" + srcEnc + "/" + dstEnc, ""); + } + + + + @CheckReturnValue + public Observable getUrl(Context ctx, EdgeId url) { + return super.get(ctx, "/edge/url/"+url.getId(), EdgeUrl.class); + } + + @CheckReturnValue + @SuppressWarnings("unchecked") + public Observable> getDomainId(Context ctx, EdgeDomain domain) { + var dom = UrlEncoded.encodeString(domain.toString()); + + return super.get(ctx, "/edge/domain-id/"+dom, EdgeId.class) + .map(id -> (EdgeId) id); + } + + + @CheckReturnValue + public Observable getDomain(Context ctx, EdgeId url) { + return super.get(ctx, "/edge/domain/"+url.getId(), EdgeDomain.class); + } + + public Observable siteInfo(Context ctx, String site) { + return super.get(ctx,"/edge/meta/" + UrlEncoded.encodeString(site), DomainInformation.class); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/meta/DomainInformation.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/meta/DomainInformation.java new file mode 100644 index 00000000..5bc7c90b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/data_store/meta/DomainInformation.java @@ -0,0 +1,26 @@ +package nu.marginalia.wmsa.data_store.meta; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; + +import java.util.List; + +@Getter @AllArgsConstructor @ToString +public class DomainInformation { + EdgeDomain domain; + + boolean blacklisted; + int pagesKnown; + int pagesFetched; + int pagesIndexed; + int incomingLinks; + int outboundLinks; + double nominalQuality; + double ranking; + + EdgeDomainIndexingState state; + List linkingDomains; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/EdgeArchiveMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/EdgeArchiveMain.java new file mode 100644 index 00000000..70b1e3e1 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/EdgeArchiveMain.java @@ -0,0 +1,33 @@ +package nu.marginalia.wmsa.edge.archive; + +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import nu.marginalia.wmsa.configuration.MainClass; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.module.ConfigurationModule; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.configuration.server.Initialization; + +public class EdgeArchiveMain extends MainClass { + private EdgeArchiveService service; + + @Inject + public EdgeArchiveMain(EdgeArchiveService service) { + this.service = service; + } + + public static void main(String... args) { + init(ServiceDescriptor.EDGE_ARCHIVE, args); + + Injector injector = Guice.createInjector( + new EdgeArchiveModule(), + new ConfigurationModule(), + new DatabaseModule() + ); + + injector.getInstance(EdgeArchiveMain.class); + injector.getInstance(Initialization.class).setReady(); + + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/EdgeArchiveModule.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/EdgeArchiveModule.java new file mode 100644 index 00000000..1d3c8215 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/EdgeArchiveModule.java @@ -0,0 +1,15 @@ +package nu.marginalia.wmsa.edge.archive; + +import com.google.inject.AbstractModule; +import com.google.inject.name.Names; + +import java.nio.file.Path; + +public class EdgeArchiveModule extends AbstractModule { + public void configure() { + bind(Path.class).annotatedWith(Names.named("archive-path")).toInstance(Path.of("/var/lib/wmsa/archive/webpage/")); + bind(Path.class).annotatedWith(Names.named("wiki-path")).toInstance(Path.of("/var/lib/wmsa/archive.fast/wiki/")); + bind(Integer.class).annotatedWith(Names.named("archive-size")).toInstance(10_000); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/EdgeArchiveService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/EdgeArchiveService.java new file mode 100644 index 00000000..bbacb600 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/EdgeArchiveService.java @@ -0,0 +1,182 @@ +package nu.marginalia.wmsa.edge.archive; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.inject.Inject; +import com.google.inject.name.Named; +import io.prometheus.client.Histogram; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.configuration.server.MetricsServer; +import nu.marginalia.wmsa.configuration.server.Service; +import nu.marginalia.wmsa.edge.archive.archiver.ArchivedFile; +import nu.marginalia.wmsa.edge.archive.archiver.Archiver; +import nu.marginalia.wmsa.edge.archive.request.EdgeArchiveSubmissionReq; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Request; +import spark.Response; +import spark.Spark; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Collectors; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +public class EdgeArchiveService extends Service { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Gson gson = new GsonBuilder().create(); + + private static final Histogram wmsa_archive_store_time = Histogram.build().name("wmsa_archive_store_time").help("-").register(); + private static final Histogram wmsa_archive_fetch_time = Histogram.build().name("wmsa_archive_fetch_time").help("-").register(); + + private final Path wikiPath; + private final Archiver archiver; + + @SneakyThrows + @Inject + public EdgeArchiveService(@Named("service-host") String ip, + @Named("service-port") Integer port, + @Named("wiki-path") Path wikiPath, + Archiver archiver, + Initialization initialization, + MetricsServer metricsServer) + { + super(ip, port, initialization, metricsServer); + this.wikiPath = wikiPath; + this.archiver = archiver; + + Spark.staticFiles.expireTime(600); + + Spark.post("/page/submit", this::pathPageSubmit); + + Spark.post("/wiki/submit", this::pathWikiSubmit); + Spark.get("/wiki/has", this::pathWikiHas); + Spark.get("/wiki/get", this::pathWikiGet); + + Spark.awaitInitialization(); + } + + @SneakyThrows + private Object pathPageSubmit(Request request, Response response) { + var timer = wmsa_archive_store_time.startTimer(); + try { + var body = request.body(); + var data = gson.fromJson(body, EdgeArchiveSubmissionReq.class); + + String domainNamePart = data.getUrl().domain.domain.length() > 32 ? data.getUrl().domain.domain.substring(0, 32) : data.getUrl().domain.domain; + String fileName = String.format("%s-%10d", domainNamePart, data.getUrl().hashCode()); + + archiver.writeData(new ArchivedFile(fileName, body.getBytes())); + + return "ok"; + } finally { + timer.observeDuration(); + } + + } + + + @SneakyThrows + private Object pathWikiSubmit(Request request, Response response) { + var timer = wmsa_archive_store_time.startTimer(); + + try { + byte[] data = request.bodyAsBytes(); + + String wikiUrl = request.queryParams("url"); + Path filename = getWikiFilename(wikiPath, wikiUrl); + + Files.createDirectories(filename.getParent()); + + System.out.println(new String(data)); + logger.debug("Writing {} to {}", wikiUrl, filename); + + try (var gos = new GZIPOutputStream(new FileOutputStream(filename.toFile()))) { + gos.write(data); + gos.flush(); + } + + return "ok"; + } finally { + timer.observeDuration(); + } + + } + + + private Path getWikiFilename(Path base, String url) { + Path p = base; + + int urlHash = url.hashCode(); + + p = p.resolve(Integer.toString(urlHash & 0xFF)); + p = p.resolve(Integer.toString((urlHash>>>8) & 0xFF)); + p = p.resolve(Integer.toString((urlHash>>>16) & 0xFF)); + p = p.resolve(Integer.toString((urlHash>>>24) & 0xFF)); + + String fileName = url.chars() + .mapToObj(this::encodeUrlChar) + .collect(Collectors.joining()); + + if (fileName.length() > 128) { + fileName = fileName.substring(0, 128) + (((long)urlHash)&0xFFFFFFFFL); + } + + return p.resolve(fileName + ".gz"); + } + + + private String encodeUrlChar(int i) { + if (i >= 'a' && i <= 'z') { + return Character.toString(i); + } + if (i >= 'A' && i <= 'Z') { + return Character.toString(i); + } + if (i >= '0' && i <= '9') { + return Character.toString(i); + } + if (i == '.') { + return Character.toString(i); + } + else { + return String.format("%%%2X", i); + } + } + + @SneakyThrows + private Object pathWikiHas(Request request, Response response) { + return Files.exists(getWikiFilename(wikiPath, request.queryParams("url"))); + } + + + @SneakyThrows + private String pathWikiGet(Request request, Response response) { + var timer = wmsa_archive_fetch_time.startTimer(); + + try { + String url = request.queryParams("url"); + + var filename = getWikiFilename(wikiPath, url); + + if (Files.exists(filename)) { + try (var stream = new GZIPInputStream(new FileInputStream(filename.toFile()))) { + return new String(stream.readAllBytes()); + } + } else { + Spark.halt(404); + return null; + } + } + finally { + timer.observeDuration(); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/archiver/ArchiveExtractor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/archiver/ArchiveExtractor.java new file mode 100644 index 00000000..e8d920ef --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/archiver/ArchiveExtractor.java @@ -0,0 +1,65 @@ +package nu.marginalia.wmsa.edge.archive.archiver; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import nu.marginalia.wmsa.edge.archive.request.EdgeArchiveSubmissionReq; +import nu.marginalia.wmsa.edge.model.crawl.EdgeRawPageContents; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Consumer; + +public class ArchiveExtractor { + private final Path archivePath; + private final String arhivePattern = "archive-%04d.tar.gz"; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Gson gson = new GsonBuilder().create(); + + public ArchiveExtractor(Path archivePath) { + this.archivePath = archivePath; + + } + + public void forEach(Consumer contents) { + for (int i = 0; ; ++i) { + var fn = getArchiveFile(i); + logger.info("{}", fn); + if (!Files.exists(fn)) { + break; + } + try (var stream = new TarArchiveInputStream(new GzipCompressorInputStream(new BufferedInputStream(new FileInputStream(fn.toFile()))))) { + TarArchiveEntry entry; + while ((entry = stream.getNextTarEntry()) != null) { + if (entry.isFile()) { + try { + var obj = gson.fromJson(new InputStreamReader(stream), EdgeArchiveSubmissionReq.class); + if (obj != null) { + contents.accept(obj.getData()); + } + } + catch (Exception ex) { + logger.error("Could not unpack {} - {} {}", entry.getName(), ex.getClass().getSimpleName(), ex.getMessage()); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private Path getArchiveFile(int number) { + final String fileName = String.format(arhivePattern, number); + return archivePath.resolve(fileName); + } +} + diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/archiver/ArchivedFile.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/archiver/ArchivedFile.java new file mode 100644 index 00000000..5a477495 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/archiver/ArchivedFile.java @@ -0,0 +1,7 @@ +package nu.marginalia.wmsa.edge.archive.archiver; + +import lombok.Data; + + +public record ArchivedFile(String filename,byte[] data ) { +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/archiver/Archiver.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/archiver/Archiver.java new file mode 100644 index 00000000..63a45b3c --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/archiver/Archiver.java @@ -0,0 +1,116 @@ +package nu.marginalia.wmsa.edge.archive.archiver; + +import com.google.inject.name.Named; +import lombok.SneakyThrows; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; + +@Singleton +public class Archiver implements AutoCloseable { + private final Path archivePath; + private final int filesPerArchive; + private final String arhivePattern = "archive-%04d.tar.gz"; + + private final LinkedBlockingDeque writeQueue = new LinkedBlockingDeque<>(10); + private final Thread writeThread; + + private volatile int archiveNumber; + private volatile boolean running; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Inject + public Archiver(@Named("archive-path") Path archivePath, @Named("archive-size") Integer filesPerArchive) { + this.archivePath = archivePath; + this.filesPerArchive = filesPerArchive; + + if (!Files.exists(archivePath)) { + throw new IllegalArgumentException("Archive path does not exist"); + } + for (int i = 0;; ++i) { + if (!Files.exists(getArchiveFile(i))) { + archiveNumber = i; + break; + } + } + + running = true; + writeThread = new Thread(this::writeThreadMain, "ArchiveWriteThread"); + writeThread.start(); + } + + private Path getArchiveFile(int number) { + final String fileName = String.format(arhivePattern, number); + return archivePath.resolve(fileName); + } + + public void writeData(ArchivedFile file) throws InterruptedException { + if (!running) throw new IllegalStateException("Archiver is closing or closed"); + writeQueue.put(file); + } + + private void writeThreadMain() { + try { + while (running || !writeQueue.isEmpty()) { + writeToFile(archiveNumber); + archiveNumber++; + } + running = false; + } + catch (Exception ex) { + logger.error("Uncaught exception in writer thread!!"); + } + } + + private void writeToFile(int archiveNumber) { + var archiveFile = getArchiveFile(archiveNumber); + + logger.info("Switching to file {}", archiveFile); + + try (TarArchiveOutputStream taos = new TarArchiveOutputStream(new GzipCompressorOutputStream(new FileOutputStream(archiveFile.toFile())))) { + for (int i = 0; i < filesPerArchive; i++) { + + ArchivedFile writeJob = null; + while (writeJob == null) { + writeJob = writeQueue.poll(1, TimeUnit.SECONDS); + if (!running) return; + } + + var entry = new TarArchiveEntry(String.format("%06d-%s", i, writeJob.filename())); + entry.setSize(writeJob.data().length); + taos.putArchiveEntry(entry); + logger.debug("Writing {} to {}", writeJob.filename(), archiveFile); + try (var bais = new ByteArrayInputStream(writeJob.data())) { + IOUtils.copy(bais, taos); + } + taos.closeArchiveEntry(); + } + taos.finish(); + logger.debug("Finishing {}", archiveFile); + } catch (Exception e) { + logger.error("Error", e); + } + + } + + @Override + public void close() throws Exception { + running = false; + writeThread.join(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/client/ArchiveClient.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/client/ArchiveClient.java new file mode 100644 index 00000000..0e56ac53 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/client/ArchiveClient.java @@ -0,0 +1,56 @@ +package nu.marginalia.wmsa.edge.archive.client; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import io.reactivex.rxjava3.core.Observable; +import nu.marginalia.wmsa.client.AbstractDynamicClient; +import nu.marginalia.wmsa.client.HttpStatusCode; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.edge.archive.request.EdgeArchiveSubmissionReq; +import nu.marginalia.wmsa.edge.model.crawl.EdgeRawPageContents; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import okhttp3.MediaType; +import org.eclipse.jetty.util.UrlEncoded; + +import javax.annotation.CheckReturnValue; +import java.util.concurrent.Semaphore; + +@Singleton +public class ArchiveClient extends AbstractDynamicClient { + + private final Semaphore submitPageSem = new Semaphore(3, true); + + @Inject + public ArchiveClient() { + super(ServiceDescriptor.EDGE_ARCHIVE); + } + + @CheckReturnValue + public void submitPage(Context ctx, EdgeUrl url, EdgeRawPageContents data) throws InterruptedException { + try { + submitPageSem.acquire(); + super.post(ctx, "/page/submit", new EdgeArchiveSubmissionReq(url, data)).blockingSubscribe(); + } + finally { + submitPageSem.release(); + } + + } + + @CheckReturnValue + public Observable submitWiki(Context ctx, String url, String data) { + return super.post(ctx, "/wiki/submit?url="+UrlEncoded.encodeString(url), data, MediaType.parse("text/plain; charset=UTF-8")); + } + + @CheckReturnValue + public Observable hasWiki(Context ctx, String url) { + return super.get(ctx, "/wiki/has?url="+UrlEncoded.encodeString(url), Boolean.class); + } + + @CheckReturnValue + public Observable getWiki(Context ctx, String url) { + return super.get(ctx, "/wiki/get?url="+UrlEncoded.encodeString(url)); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/request/EdgeArchiveSubmissionReq.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/request/EdgeArchiveSubmissionReq.java new file mode 100644 index 00000000..fbd97452 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/archive/request/EdgeArchiveSubmissionReq.java @@ -0,0 +1,13 @@ +package nu.marginalia.wmsa.edge.archive.request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.edge.model.crawl.EdgeRawPageContents; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +@AllArgsConstructor @Getter @ToString +public class EdgeArchiveSubmissionReq { + EdgeUrl url; + EdgeRawPageContents data; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/EdgeAssistantMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/EdgeAssistantMain.java new file mode 100644 index 00000000..44041723 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/EdgeAssistantMain.java @@ -0,0 +1,33 @@ +package nu.marginalia.wmsa.edge.assistant; + +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import nu.marginalia.wmsa.configuration.MainClass; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.module.ConfigurationModule; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.configuration.server.Initialization; + +public class EdgeAssistantMain extends MainClass { + private EdgeAssistantService service; + + @Inject + public EdgeAssistantMain(EdgeAssistantService service) { + this.service = service; + } + + public static void main(String... args) { + init(ServiceDescriptor.EDGE_ASSISTANT, args); + + Injector injector = Guice.createInjector( + new EdgeAssistantModule(), + new ConfigurationModule(), + new DatabaseModule() + ); + + injector.getInstance(EdgeAssistantMain.class); + injector.getInstance(Initialization.class).setReady(); + + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/EdgeAssistantModule.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/EdgeAssistantModule.java new file mode 100644 index 00000000..1632bbf2 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/EdgeAssistantModule.java @@ -0,0 +1,22 @@ +package nu.marginalia.wmsa.edge.assistant; + +import com.google.inject.AbstractModule; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; + +import java.nio.file.Path; + +import static com.google.inject.name.Names.named; + +public class EdgeAssistantModule extends AbstractModule { + public void configure() { + bind(Path.class).annotatedWith(named("suggestions-file")).toInstance(Path.of("/var/lib/wmsa/suggestions.txt")); + bind(LanguageModels.class).toInstance(new LanguageModels( + Path.of("/var/lib/wmsa/model/ngrams-generous-emstr.bin"), + Path.of("/var/lib/wmsa/model/tfreq-new-algo3.bin"), + Path.of("/var/lib/wmsa/model/opennlp-sentence.bin"), + Path.of("/var/lib/wmsa/model/English.RDR"), + Path.of("/var/lib/wmsa/model/English.DICT"), + Path.of("/var/lib/wmsa/model/opennlp-tok.bin") + )); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/EdgeAssistantService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/EdgeAssistantService.java new file mode 100644 index 00000000..3e595d47 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/EdgeAssistantService.java @@ -0,0 +1,192 @@ +package nu.marginalia.wmsa.edge.assistant; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.inject.Inject; +import com.google.inject.name.Named; +import io.reactivex.rxjava3.core.Observable; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.client.HttpStatusCode; +import nu.marginalia.wmsa.configuration.server.*; +import nu.marginalia.wmsa.edge.archive.client.ArchiveClient; +import nu.marginalia.wmsa.edge.assistant.dict.DictionaryService; +import nu.marginalia.wmsa.edge.assistant.eval.MathParser; +import nu.marginalia.wmsa.edge.assistant.eval.Units; +import nu.marginalia.wmsa.edge.assistant.screenshot.ScreenshotService; +import nu.marginalia.wmsa.edge.assistant.suggest.Suggestions; +import nu.marginalia.wmsa.renderer.mustache.MustacheRenderer; +import nu.marginalia.wmsa.renderer.mustache.RendererFactory; +import org.eclipse.jetty.websocket.api.StatusCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Request; +import spark.Response; +import spark.Spark; + +import java.util.Map; + +public class EdgeAssistantService extends Service { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Gson gson = new GsonBuilder().create(); + private final Units units; + private final DictionaryService dictionaryService; + private final MathParser mathParser; + private final ArchiveClient archiveClient; + private final ScreenshotService screenshotService; + private final MustacheRenderer wikiErrorPageRenderer; + private final MustacheRenderer wikiSearchResultRenderer; + private final Suggestions suggestions; + + @SneakyThrows + @Inject + public EdgeAssistantService(@Named("service-host") String ip, + @Named("service-port") Integer port, + Initialization initialization, + MetricsServer metricsServer, + DictionaryService dictionaryService, + MathParser mathParser, + Units units, + ArchiveClient archiveClient, + RendererFactory rendererFactory, + ScreenshotService screenshotService, + Suggestions suggestions + ) + { + super(ip, port, initialization, metricsServer); + this.dictionaryService = dictionaryService; + this.mathParser = mathParser; + this.units = units; + this.archiveClient = archiveClient; + this.screenshotService = screenshotService; + this.suggestions = suggestions; + + Spark.staticFiles.expireTime(600); + + if (rendererFactory != null) { + wikiErrorPageRenderer = rendererFactory.renderer("encyclopedia/wiki-error"); + wikiSearchResultRenderer = rendererFactory.renderer("encyclopedia/wiki-search"); + } + else { + wikiErrorPageRenderer = null; + wikiSearchResultRenderer = null; + } + + Spark.get("/public/wiki/*", this::getWikiPage); + Spark.get("/public/wiki-search", this::searchWikiPage); + + Spark.get("/public/screenshot/:id", screenshotService::serveScreenshotRequest); + Spark.get("/screenshot/:id", screenshotService::serveScreenshotRequest); + + Spark.get("/dictionary/:word", (req, rsp) -> dictionaryService.define(req.params("word")), this::convertToJson); + Spark.get("/spell-check/:term", (req, rsp) -> dictionaryService.spellCheck(req.params("term").toLowerCase()), this::convertToJson); + Spark.get("/encyclopedia/:term", (req, rsp) -> dictionaryService.encyclopedia(req.params("term")), this::convertToJson); + Spark.get("/unit-conversion", (req, rsp) -> unitConversion( + rsp, + req.queryParams("value"), + req.queryParams("from"), + req.queryParams("to") + + )); + Spark.get("/eval-expression", (req, rsp) -> evalExpression( + rsp, + req.queryParams("value") + )); + + Spark.get("/public/suggest/", this::getSuggestions, this::convertToJson); + + Spark.awaitInitialization(); + } + + private Object getSuggestions(Request request, Response response) { + response.type("application/json"); + var param = request.queryParams("partial"); + if (param == null) { + logger.warn("Bad parameter, partial is null"); + Spark.halt(500); + } + return suggestions.getSuggestions(10, param); + } + + @SneakyThrows + private Object getWikiPage(Request req, Response rsp) { + final var ctx = Context.fromRequest(req); + + final String[] splats = req.splat(); + if (splats.length == 0) + rsp.redirect("https://encyclopedia.marginalia.nu/wiki-start.html"); + + + final String s = splats[0]; + + String pageName = dictionaryService.resolveEncylopediaRedirect(s).orElse(s); + logger.info("Resolved {} -> {}", s, pageName); + return archiveClient.getWiki(ctx, pageName) + .onErrorResumeWith(resolveWikiPageNameWrongCase(ctx, s)) + .blockingFirst(); + } + + private Observable resolveWikiPageNameWrongCase(Context ctx, String s) { + var rsp = dictionaryService.findEncyclopediaPageDirect(s); + if (rsp.isEmpty()) { + return renderSearchPage(s); + } + return archiveClient.getWiki(ctx, rsp.get().getInternalName()) + .onErrorResumeWith(renderSearchPage(s)); + } + + private Observable renderSearchPage(String s) { + return Observable.fromCallable(() -> wikiSearchResultRenderer.render( + Map.of("query", s, + "error", "true", + "results", dictionaryService.findEncyclopediaPages(s)))); + } + + @SneakyThrows + private Object searchWikiPage(Request req, Response rsp) { + final var ctx = Context.fromRequest(req); + + String term = req.queryParams("query"); + if (null == term) { + rsp.redirect("https://encyclopedia.marginalia.nu/wiki-start.html"); + return ""; + } + + return wikiSearchResultRenderer.render( + Map.of("query", term, + "results", + dictionaryService.findEncyclopediaPages(term)) + ); + } + + private Object evalExpression(Response rsp, String value) { + try { + var val = mathParser.evalFormatted(value); + if (val.isBlank()) { + Spark.halt(400); + return null; + } + return val; + } + catch (Exception ex) { + Spark.halt(400); + return null; + } + } + + private Object unitConversion(Response rsp, String value, String fromUnit, String toUnit) { + var result = units.convert(value, fromUnit, toUnit); + if (result.isPresent()) { + return result.get(); + } + { + Spark.halt(400); + return null; + } + } + + private String convertToJson(Object o) { + return gson.toJson(o); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/client/AssistantClient.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/client/AssistantClient.java new file mode 100644 index 00000000..891a2cc0 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/client/AssistantClient.java @@ -0,0 +1,42 @@ +package nu.marginalia.wmsa.edge.assistant.client; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import io.reactivex.rxjava3.core.Observable; +import nu.marginalia.wmsa.client.AbstractDynamicClient; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.edge.assistant.dict.DictionaryResponse; +import nu.marginalia.wmsa.edge.assistant.dict.WikiArticles; +import org.eclipse.jetty.util.UrlEncoded; + +import java.util.List; + +@Singleton +public class AssistantClient extends AbstractDynamicClient { + + @Inject + public AssistantClient() { + super(ServiceDescriptor.EDGE_ASSISTANT); + } + + public Observable dictionaryLookup(Context ctx, String word) { + return super.get(ctx,"/dictionary/" + UrlEncoded.encodeString(word), DictionaryResponse.class); + } + + public Observable encyclopediaLookup(Context ctx, String word) { + return super.get(ctx,"/encyclopedia/" + UrlEncoded.encodeString(word), WikiArticles.class); + } + + @SuppressWarnings("unchecked") + public Observable> spellCheck(Context ctx, String word) { + return (Observable>) (Object) super.get(ctx,"/spell-check/" + UrlEncoded.encodeString(word), List.class); + } + public Observable unitConversion(Context ctx, String value, String from, String to) { + return super.get(ctx,"/unit-conversion?value="+value + "&from="+from+"&to="+to); + } + + public Observable evalMath(Context ctx, String expression) { + return super.get(ctx,"/eval-expression?value="+UrlEncoded.encodeString(expression)); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/DictionaryEntry.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/DictionaryEntry.java new file mode 100644 index 00000000..88c48986 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/DictionaryEntry.java @@ -0,0 +1,14 @@ +package nu.marginalia.wmsa.edge.assistant.dict; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@AllArgsConstructor +@Getter +@ToString +public class DictionaryEntry { + public final String type; + public final String word; + public final String definition; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/DictionaryResponse.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/DictionaryResponse.java new file mode 100644 index 00000000..624782a6 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/DictionaryResponse.java @@ -0,0 +1,14 @@ +package nu.marginalia.wmsa.edge.assistant.dict; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.List; + +@ToString @Getter @AllArgsConstructor @NoArgsConstructor +public class DictionaryResponse { + public String word; + public List entries; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/DictionaryService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/DictionaryService.java new file mode 100644 index 00000000..2147c297 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/DictionaryService.java @@ -0,0 +1,185 @@ +package nu.marginalia.wmsa.edge.assistant.dict; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.zaxxer.hikari.HikariDataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; + +@Singleton +public class DictionaryService { + + private final HikariDataSource dataSource; + private final SpellChecker spellChecker; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Inject + public DictionaryService(HikariDataSource dataSource, SpellChecker spellChecker) + { + this.spellChecker = spellChecker; + this.dataSource = dataSource; + } + + public DictionaryResponse define(String word) { + DictionaryResponse response = new DictionaryResponse(); + response.entries = new ArrayList<>(); + + try (var connection = dataSource.getConnection()) { + var stmt = connection.prepareStatement("SELECT TYPE,WORD,DEFINITION FROM REF_DICTIONARY WHERE WORD=?"); + stmt.setString(1, word); + + var rsp = stmt.executeQuery(); + while (rsp.next()) { + response.entries.add(new DictionaryEntry(rsp.getString(1), rsp.getString(2), rsp.getString(3))); + } + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + + return response; + } + + public WikiArticles encyclopedia(String term) { + WikiArticles response = new WikiArticles(); + response.entries = new ArrayList<>(); + + try (var connection = dataSource.getConnection()) { + var stmt = connection.prepareStatement("SELECT DISTINCT(NAME_LOWER) FROM REF_WIKI_TITLE WHERE NAME_LOWER=?"); + stmt.setString(1, term); + + var rsp = stmt.executeQuery(); + while (rsp.next()) { + response.entries.add(capitalizeWikiString(rsp.getString(1))); + } + } + catch (Exception ex) { + logger.error("Failed to fetch articles", ex); + return new WikiArticles(); + } + + return response; + } + + public Optional resolveEncylopediaRedirect(String term) { + final List matches = new ArrayList<>(); + + try (var connection = dataSource.getConnection()) { + try (var stmt = connection.prepareStatement("SELECT NAME, REF_NAME FROM REF_WIKI_TITLE WHERE NAME_LOWER=LOWER(?)")) { + stmt.setString(1, term); + + var rsp = stmt.executeQuery(); + while (rsp.next()) { + if (term.equals(rsp.getString(1)) + || rsp.getString(2) == null) { + return Optional.ofNullable(rsp.getString(2)); + } else { + matches.add(rsp.getString(2)); + } + } + } + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + + if (!matches.isEmpty()) { + return Optional.of(matches.get(0)); + } + return Optional.empty(); + } + + + public Optional findEncyclopediaPageDirect(String term) { + + try (var connection = dataSource.getConnection()) { + + try (var stmt = connection.prepareStatement("SELECT NAME, REF_NAME FROM REF_WIKI_TITLE WHERE NAME_LOWER=LOWER(?)")) { + stmt.setString(1, term.replace(' ', '_')); + + var rsp = stmt.executeQuery(); + while (rsp.next()) { + String name = rsp.getString(1); + String refName = rsp.getString(2); + + if (refName == null) { + return Optional.of(new WikiSearchResult(name, null)); + } + } + } + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + + return Optional.empty(); + } + + public List findEncyclopediaPages(String term) { + final List directMatches = new ArrayList<>(); + final Set directSearchMatches = new HashSet<>(); + final Set indirectMatches = new HashSet<>(); + + try (var connection = dataSource.getConnection()) { + + try (var stmt = connection.prepareStatement("SELECT NAME, REF_NAME FROM REF_WIKI_TITLE WHERE NAME_LOWER=LOWER(?)")) { + stmt.setString(1, term.replace(' ', '_')); + + var rsp = stmt.executeQuery(); + while (rsp.next()) { + String name = rsp.getString(1); + String refName = rsp.getString(2); + + if (refName == null) { + directMatches.add(new WikiSearchResult(name, null)); + } else { + indirectMatches.add(new WikiSearchResult(name, refName)); + } + } + } + + try (var stmt = connection.prepareStatement("SELECT NAME, REF_NAME FROM REF_WIKI_TITLE WHERE NAME_LOWER LIKE ? LIMIT 10")) { + stmt.setString(1, term.replace(' ', '_').replaceAll("%", "\\%").toLowerCase() + "%"); + + var rsp = stmt.executeQuery(); + while (rsp.next()) { + String name = rsp.getString(1); + String refName = rsp.getString(2); + + if (refName == null) { + directSearchMatches.add(new WikiSearchResult(name, null)); + } else { + indirectMatches.add(new WikiSearchResult(name, refName)); + } + } + } + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + + directMatches.forEach(indirectMatches::remove); + indirectMatches.removeAll(directSearchMatches); + directMatches.forEach(directSearchMatches::remove); + directMatches.addAll(indirectMatches); + directMatches.addAll(directSearchMatches); + return directMatches; + } + + private String capitalizeWikiString(String string) { + if (string.contains("_")) { + return Arrays.stream(string.split("_")).map(this::capitalizeWikiString).collect(Collectors.joining("_")); + } + if (string.length() < 2) { + return string.toUpperCase(); + } + return Character.toUpperCase(string.charAt(0)) + string.substring(1).toLowerCase(); + } + + public List spellCheck(String word) { + return spellChecker.correct(word); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/NGramDict.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/NGramDict.java new file mode 100644 index 00000000..256b0b9a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/NGramDict.java @@ -0,0 +1,140 @@ +package nu.marginalia.wmsa.edge.assistant.dict; + +import ca.rmen.porterstemmer.PorterStemmer; +import gnu.trove.map.hash.TLongIntHashMap; +import gnu.trove.map.hash.TLongLongHashMap; +import gnu.trove.set.hash.TLongHashSet; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import javax.inject.Inject; +import javax.inject.Singleton; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Singleton +public class NGramDict { + + private final TLongIntHashMap wordRates = new TLongIntHashMap(1_000_000, 0.5f, 0, 0); + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private static final Pattern separator = Pattern.compile("[_ ]+"); + private static PorterStemmer ps = new PorterStemmer(); + + private static long fileSize(Path p) throws IOException { + return Files.size(p); + } + + @Inject + public NGramDict(@Nullable LanguageModels models) { + if (models == null) { + return; + } + + if (models.ngramFrequency != null) { + + try (var frequencyData = new DataInputStream(new BufferedInputStream(new FileInputStream(models.ngramFrequency.toFile())))) { + + wordRates.ensureCapacity((int)(fileSize(models.ngramFrequency)/16)); + + for (;;) { + wordRates.put(frequencyData.readLong(), (int) frequencyData.readLong()); + } + } catch (EOFException eof) { + // ok + } catch (IOException e) { + logger.error("IO Exception reading " + models.ngramFrequency, e); + } + } + + logger.info("Read {} N-grams frequencies", wordRates.size()); + } + + + public static void main(String... args) { + if (args.length != 2) { + System.err.println("Expected arguments: in-file out-file"); + } + String inFile = args[0]; + String outFile = args[1]; + + var wordPattern = Pattern.compile("\\w+(_\\w+)*").asMatchPredicate(); + try (var linesStr = Files.lines(Path.of(inFile)); + var dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(outFile))) + ) { + linesStr + .filter(wordPattern) + .mapToLong(NGramDict::getStringHash).forEach(l -> + { + try { + dos.writeLong(l); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static long getStringHash(String s) { + String[] strings = separator.split(s); + if (s.length() > 1) { + byte[][] parts = new byte[strings.length][]; + for (int i = 0; i < parts.length; i++) { + parts[i] = ps.stemWord(strings[i]).getBytes(); + } + return longHash(parts); + } + else { + return longHash(s.getBytes()); + } + } + public long getTermFreqHash(long hash) { + return wordRates.get(hash); + } + public long getTermFreq(String s) { + return wordRates.get(getStringHash(s)); + } + public long getTermFreqStemmed(String s) { + return wordRates.get(longHash(s.getBytes())); + } + + public static String getStemmedString(String s) { + String[] strings = separator.split(s); + if (s.length() > 1) { + return Arrays.stream(strings).map(ps::stemWord).collect(Collectors.joining("_")); + } + else { + return s; + } + + } + + public static long longHash(byte[]... bytesSets) { + if (bytesSets == null || bytesSets.length == 0) + return 0; + + // https://cp-algorithms.com/string/string-hashing.html + int p = 127; + long m = (1L<<61)-1; + long p_power = 1; + long hash_val = 0; + + for (byte[] bytes: bytesSets) { + for (byte element : bytes) { + hash_val = (hash_val + (element + 1) * p_power) % m; + p_power = (p_power * p) % m; + } + } + return hash_val; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/SpellChecker.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/SpellChecker.java new file mode 100644 index 00000000..1e878096 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/SpellChecker.java @@ -0,0 +1,22 @@ +package nu.marginalia.wmsa.edge.assistant.dict; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +@Singleton +public class SpellChecker { + + private final SymSpell symSpell = new SymSpell(); + + public SpellChecker() { + + } + + public List correct(String word) { + return symSpell.Correct(word).stream().sorted(Comparator.comparing(term -> term.distance)).map(term->term.term).collect(Collectors.toList()); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/SymSpell.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/SymSpell.java new file mode 100644 index 00000000..ec143fba --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/SymSpell.java @@ -0,0 +1,442 @@ +package nu.marginalia.wmsa.edge.assistant.dict; + + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +// SymSpell: 1 million times faster through Symmetric Delete spelling correction algorithm +// +// The Symmetric Delete spelling correction algorithm reduces the complexity of edit candidate generation and dictionary lookup +// for a given Damerau-Levenshtein distance. It is six orders of magnitude faster and language independent. +// Opposite to other algorithms only deletes are required, no transposes + replaces + inserts. +// Transposes + replaces + inserts of the input term are transformed into deletes of the dictionary term. +// Replaces and inserts are expensive and language dependent: e.g. Chinese has 70,000 Unicode Han characters! +// +// Copyright (C) 2015 Wolf Garbe +// Version: 3.0 +// Author: Wolf Garbe +// Maintainer: Wolf Garbe +// URL: http://blog.faroo.com/2012/06/07/improved-edit-distance-based-spelling-correction/ +// Description: http://blog.faroo.com/2012/06/07/improved-edit-distance-based-spelling-correction/ +// +// License: +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License, +// version 3.0 (LGPL-3.0) as published by the Free Software Foundation. +// http://www.opensource.org/licenses/LGPL-3.0 +// +// Usage: single word + Enter: Display spelling suggestions +// Enter without input: Terminate the program + +public class SymSpell +{ + private int editDistanceMax=2; + private int verbose = 1; + //0: top suggestion + //1: all suggestions of smallest edit distance + //2: all suggestions <= editDistanceMax (slower, no early termination) + + public static class dictionaryItem + { + public List suggestions = new ArrayList(); + public int count = 0; + } + + public static class SuggestionItem + { + public String term = ""; + public int distance = 0; + public int count = 0; + + @Override + public boolean equals(Object obj) + { + return term.equals(((SuggestionItem)obj).term); + } + + @Override + public int hashCode() + { + return term.hashCode(); + } + } + + //Dictionary that contains both the original words and the deletes derived from them. A term might be both word and delete from another word at the same time. + //For space reduction a item might be either of type dictionaryItem or Int. + //A dictionaryItem is used for word, word/delete, and delete with multiple suggestions. Int is used for deletes with a single suggestion (the majority of entries). + private HashMap dictionary = new HashMap(); //initialisierung + + //List of unique words. By using the suggestions (Int) as index for this list they are translated into the original String. + private List wordlist = new ArrayList(); + + //create a non-unique wordlist from sample text + //language independent (e.g. works with Chinese characters) + private Iterable parseWords(String text) + { + // \w Alphanumeric characters (including non-latin characters, umlaut characters and digits) plus "_" + // \d Digits + // Provides identical results to Norvigs regex "[a-z]+" for latin characters, while additionally providing compatibility with non-latin characters + List allMatches = new ArrayList(); + Matcher m = Pattern.compile("[\\w-[\\d_]]+").matcher(text.toLowerCase()); + while (m.find()) { + allMatches.add(m.group()); + } + return allMatches; + } + + public int maxlength = 0;//maximum dictionary term length + + //for every word there all deletes with an edit distance of 1..editDistanceMax created and added to the dictionary + //every delete entry has a suggestions list, which points to the original term(s) it was created from + //The dictionary may be dynamically updated (word frequency and new words) at any time by calling createDictionaryEntry + private boolean CreateDictionaryEntry(String key, String language) + { + boolean result = false; + dictionaryItem value=null; + Object valueo; + valueo = dictionary.get(language+key); + if (valueo!=null) + { + //int or dictionaryItem? delete existed before word! + if (valueo instanceof Integer) { + int tmp = (int)valueo; + value = new dictionaryItem(); + value.suggestions.add(tmp); + dictionary.put(language + key,value); + } + + //already exists: + //1. word appears several times + //2. word1==deletes(word2) + else + { + value = (dictionaryItem)valueo; + } + + //prevent overflow + if (value.count < Integer.MAX_VALUE) value.count++; + } + else if (wordlist.size() < Integer.MAX_VALUE) + { + value = new dictionaryItem(); + value.count++; + dictionary.put(language + key, value); + + if (key.length() > maxlength) maxlength = key.length(); + } + + //edits/suggestions are created only once, no matter how often word occurs + //edits/suggestions are created only as soon as the word occurs in the corpus, + //even if the same term existed before in the dictionary as an edit from another word + //a treshold might be specifid, when a term occurs so frequently in the corpus that it is considered a valid word for spelling correction + if(value.count == 1) + { + //word2index + wordlist.add(key); + int keyint = (int)(wordlist.size() - 1); + + result = true; + + //create deletes + for (String delete : Edits(key, 0, new HashSet())) + { + Object value2; + value2 = dictionary.get(language+delete); + if (value2!=null) + { + //already exists: + //1. word1==deletes(word2) + //2. deletes(word1)==deletes(word2) + //int or dictionaryItem? single delete existed before! + if (value2 instanceof Integer) + { + //transformes int to dictionaryItem + int tmp = (int)value2; + dictionaryItem di = new dictionaryItem(); + di.suggestions.add(tmp); + dictionary.put(language + delete,di); + if (!di.suggestions.contains(keyint)) AddLowestDistance(di, key, keyint, delete); + } + else if (!((dictionaryItem)value2).suggestions.contains(keyint)) AddLowestDistance((dictionaryItem) value2, key, keyint, delete); + } + else + { + dictionary.put(language + delete, keyint); + } + + } + } + return result; + } + + //create a frequency dictionary from a corpus + private void CreateDictionary(String path, String language) + { + + try (var resource = Objects.requireNonNull(ClassLoader.getSystemResourceAsStream(path), + "Could not load dictionary"); + BufferedReader br = new BufferedReader(new InputStreamReader(resource))){ + String line; + while ((line = br.readLine()) != null) + { + for (String key : parseWords(line)) + { + CreateDictionaryEntry(key.toLowerCase(), language); + } + } + } + catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + //save some time and space + private void AddLowestDistance(dictionaryItem item, String suggestion, int suggestionint, String delete) + { + //remove all existing suggestions of higher distance, if verbose<2 + //index2word + //TODO check + if ((verbose < 2) && (item.suggestions.size() > 0) && (wordlist.get(item.suggestions.get(0)).length()-delete.length() > suggestion.length() - delete.length())) item.suggestions.clear(); + //do not add suggestion of higher distance than existing, if verbose<2 + if ((verbose == 2) || (item.suggestions.size() == 0) || (wordlist.get(item.suggestions.get(0)).length()-delete.length() >= suggestion.length() - delete.length())) item.suggestions.add(suggestionint); + } + + //inexpensive and language independent: only deletes, no transposes + replaces + inserts + //replaces and inserts are expensive and language dependent (Chinese has 70,000 Unicode Han characters) + private HashSet Edits(String word, int editDistance, HashSet deletes) + { + editDistance++; + if (word.length() > 1) + { + for (int i = 0; i < word.length(); i++) + { + //delete ith character + String delete = word.substring(0,i)+word.substring(i+1); + if (deletes.add(delete)) + { + //recursion, if maximum edit distance not yet reached + if (editDistance < editDistanceMax) Edits(delete, editDistance, deletes); + } + } + } + return deletes; + } + + private List Lookup(String input, String language, int editDistanceMax) + { + //save some time + if (input.length() - editDistanceMax > maxlength) + return new ArrayList(); + + List candidates = new ArrayList(); + HashSet hashset1 = new HashSet(); + + List suggestions = new ArrayList(); + HashSet hashset2 = new HashSet(); + + Object valueo; + + //add original term + candidates.add(input); + + while (candidates.size()>0) + { + String candidate = candidates.get(0); + candidates.remove(0); + + //save some time + //early termination + //suggestion distance=candidate.distance... candidate.distance+editDistanceMax + //if canddate distance is already higher than suggestion distance, than there are no better suggestions to be expected + + //label for c# goto replacement + nosort:{ + + if ((verbose < 2) && (suggestions.size() > 0) && (input.length()-candidate.length() > suggestions.get(0).distance)) + break nosort; + + //read candidate entry from dictionary + valueo = dictionary.get(language + candidate); + if (valueo != null) + { + dictionaryItem value= new dictionaryItem(); + if (valueo instanceof Integer) + value.suggestions.add((int)valueo); + else value = (dictionaryItem)valueo; + + //if count>0 then candidate entry is correct dictionary term, not only delete item + if ((value.count > 0) && hashset2.add(candidate)) + { + //add correct dictionary term term to suggestion list + SuggestionItem si = new SuggestionItem(); + si.term = candidate; + si.count = value.count; + si.distance = input.length() - candidate.length(); + suggestions.add(si); + //early termination + if ((verbose < 2) && (input.length() - candidate.length() == 0)) + break nosort; + } + + //iterate through suggestions (to other correct dictionary items) of delete item and add them to suggestion list + Object value2; + for (int suggestionint : value.suggestions) + { + //save some time + //skipping double items early: different deletes of the input term can lead to the same suggestion + //index2word + //TODO + String suggestion = wordlist.get(suggestionint); + if (hashset2.add(suggestion)) + { + //True Damerau-Levenshtein Edit Distance: adjust distance, if both distances>0 + //We allow simultaneous edits (deletes) of editDistanceMax on on both the dictionary and the input term. + //For replaces and adjacent transposes the resulting edit distance stays <= editDistanceMax. + //For inserts and deletes the resulting edit distance might exceed editDistanceMax. + //To prevent suggestions of a higher edit distance, we need to calculate the resulting edit distance, if there are simultaneous edits on both sides. + //Example: (bank==bnak and bank==bink, but bank!=kanb and bank!=xban and bank!=baxn for editDistanceMaxe=1) + //Two deletes on each side of a pair makes them all equal, but the first two pairs have edit distance=1, the others edit distance=2. + int distance = 0; + if (suggestion != input) + { + if (suggestion.length() == candidate.length()) distance = input.length() - candidate.length(); + else if (input.length() == candidate.length()) distance = suggestion.length() - candidate.length(); + else + { + //common prefixes and suffixes are ignored, because this speeds up the Damerau-levenshtein-Distance calculation without changing it. + int ii = 0; + int jj = 0; + while ((ii < suggestion.length()) && (ii < input.length()) && (suggestion.charAt(ii) == input.charAt(ii))) ii++; + while ((jj < suggestion.length() - ii) && (jj < input.length() - ii) && (suggestion.charAt(suggestion.length() - jj - 1) == input.charAt(input.length() - jj - 1))) jj++; + if ((ii > 0) || (jj > 0)) { + distance = DamerauLevenshteinDistance(suggestion.substring(ii, suggestion.length() - jj), input.substring(ii, input.length() - jj)); + } + else distance = DamerauLevenshteinDistance(suggestion, input); + } + } + + //save some time. + //remove all existing suggestions of higher distance, if verbose<2 + if ((verbose < 2) && (suggestions.size() > 0) && (suggestions.get(0).distance > distance)) suggestions.clear(); + //do not process higher distances than those already found, if verbose<2 + if ((verbose < 2) && (suggestions.size() > 0) && (distance > suggestions.get(0).distance)) continue; + + if (distance <= editDistanceMax) + { + value2 = dictionary.get(language + suggestion); + if (value2!=null) + { + SuggestionItem si = new SuggestionItem(); + si.term = suggestion; + si.count = ((dictionaryItem)value2).count; + si.distance = distance; + suggestions.add(si); + } + } + } + }//end foreach + }//end if + + //add edits + //derive edits (deletes) from candidate (input) and add them to candidates list + //this is a recursive process until the maximum edit distance has been reached + if (input.length() - candidate.length() < editDistanceMax) + { + //save some time + //do not create edits with edit distance smaller than suggestions already found + if ((verbose < 2) && (suggestions.size() > 0) && (input.length() - candidate.length() >= suggestions.get(0).distance)) continue; + + for (int i = 0; i < candidate.length(); i++) + { + String delete = candidate.substring(0, i)+candidate.substring(i+1); + if (hashset1.add(delete)) candidates.add(delete); + } + } + } //end lable nosort + } //end while + + //sort by ascending edit distance, then by descending word frequency + if (verbose < 2) + //suggestions.Sort((x, y) => -x.count.CompareTo(y.count)); + Collections.sort(suggestions, new Comparator() + { + public int compare(SuggestionItem f1, SuggestionItem f2) + { + return -(f1.count-f2.count); + } + }); + else + //suggestions.Sort((x, y) => 2*x.distance.CompareTo(y.distance) - x.count.CompareTo(y.count)); + Collections.sort(suggestions, new Comparator() + { + public int compare(SuggestionItem x, SuggestionItem y) + { + return ((2*x.distance-y.distance)>0?1:0) - ((x.count - y.count)>0?1:0); + } + }); + if ((verbose == 0)&&(suggestions.size()>1)) + return suggestions.subList(0, 1); + else return suggestions; + } + + public List Correct(String input) + { + return Lookup(input, "", editDistanceMax); + } + + public SymSpell() { + CreateDictionary("dictionary/en-words", ""); + } + + // Damerau–Levenshtein distance algorithm and code + // from http://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance (as retrieved in June 2012) + public int DamerauLevenshteinDistance(String a, String b) { + final int inf = a.length() + b.length() + 1; + int[][] H = new int[a.length() + 2][b.length() + 2]; + for (int i = 0; i <= a.length(); i++) { + H[i + 1][1] = i; + H[i + 1][0] = inf; + } + for (int j = 0; j <= b.length(); j++) { + H[1][j + 1] = j; + H[0][j + 1] = inf; + } + HashMap DA = new HashMap(); + for (int d = 0; d < a.length(); d++) + if (!DA.containsKey(a.charAt(d))) + DA.put(a.charAt(d), 0); + + + for (int d = 0; d < b.length(); d++) + if (!DA.containsKey(b.charAt(d))) + DA.put(b.charAt(d), 0); + + for (int i = 1; i <= a.length(); i++) { + int DB = 0; + for (int j = 1; j <= b.length(); j++) { + final int i1 = DA.get(b.charAt(j - 1)); + final int j1 = DB; + int d = 1; + if (a.charAt(i - 1) == b.charAt(j - 1)) { + d = 0; + DB = j; + } + H[i + 1][j + 1] = min( + H[i][j] + d, + H[i + 1][j] + 1, + H[i][j + 1] + 1, + H[i1][j1] + ((i - i1 - 1)) + + 1 + ((j - j1 - 1))); + } + DA.put(a.charAt(i - 1), i); + } + return H[a.length() + 1][b.length() + 1]; + } + public int min(int a, int b, int c, int d) { + return Math.min(a, Math.min(b, Math.min(c, d))); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/WikiArticles.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/WikiArticles.java new file mode 100644 index 00000000..29cf187e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/WikiArticles.java @@ -0,0 +1,23 @@ +package nu.marginalia.wmsa.edge.assistant.dict; + +import lombok.Getter; +import lombok.ToString; + +import java.util.List; + +@ToString @Getter +public class WikiArticles { + public List entries; + + public WikiArticles(String... args) { + entries = List.of(args); + } + public String getPage() { + if (entries.isEmpty()) { + return null; + } + else { + return entries.get(0); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/WikiCleaner.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/WikiCleaner.java new file mode 100644 index 00000000..80560be1 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/WikiCleaner.java @@ -0,0 +1,393 @@ +package nu.marginalia.wmsa.edge.assistant.dict; + +import lombok.SneakyThrows; +import net.sourceforge.jeuclid.MathMLParserSupport; +import net.sourceforge.jeuclid.context.Display; +import net.sourceforge.jeuclid.context.LayoutContextImpl; +import net.sourceforge.jeuclid.context.Parameter; +import net.sourceforge.jeuclid.font.FontFactory; +import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.NotNull; +import org.jsoup.Jsoup; +import org.jsoup.nodes.*; +import org.jsoup.select.Elements; +import org.jsoup.select.NodeFilter; + +import java.awt.*; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.*; +import java.util.stream.Collectors; + + +public class WikiCleaner { + + static { + try (var font = ClassLoader.getSystemResourceAsStream("fonts/LM-regular.ttf")) { + FontFactory.getInstance().registerFont(Font.TRUETYPE_FONT, font); + } catch (IOException | FontFormatException e) { + e.printStackTrace(); + } + try (var font = ClassLoader.getSystemResourceAsStream("fonts/STIXTwoMath-Regular.ttf")) { + FontFactory.getInstance().registerFont(Font.TRUETYPE_FONT, font); + } catch (IOException | FontFormatException e) { + e.printStackTrace(); + } + } + public String cleanWikiJunk(String url, String html) { + return cleanWikiJunk(url, Jsoup.parse(html)); + } + + public List extractLinkWords(String data) { + var doc = Jsoup.parse(data); + return getWikiPageLinkText(doc); + } + + public String cleanWikiJunk(String url, Document doc) { + + if (doc.getElementById("content") == null) { + return null; + } + List> disambig = getDisambiguationLinks(doc); + List> topLinks = getWikiPageLinks(doc); + + removeTag(doc, "script", "object", "embed", "audio", "style", "noscript", "link", "meta", "img"); + doc.getElementsByClass("mwe-math-element").forEach(this::convertMathTag); + removeByClass(doc, "infobox", "collapsible", "navbar", "printfooter", + "mw-editsection", "thumb", "sidebar", "navbox", "mw-jump-link", + "vertical-navbox"); + removeByClass(doc, "mw-indicators", "noprint", "sistersitebox"); + removeIds(doc, "coordinates", "mw-page-base", "mw-head-base", "site-notice", "contentSub", "contentSub2"); + + doc.getElementsByAttributeValue("role", "presentation").remove(); + + doc.getElementsByTag("a").forEach(atag -> { + var href = atag.attr("href"); + var parent = atag.parent(); + + if ("li".equals(parent.tagName())) { + atag.removeAttr("title"); + if (href.startsWith("http://")) { + atag.addClass("extern-link"); + atag.attr("rel", "nofollow"); + return; + } + } + else { + atag.replaceWith(new TextNode(atag.text())); + } + }); + + Optional.ofNullable(doc.getElementsByTag("cite")).ifPresent(cite -> cite.forEach(c -> { + c.tagName("span"); + })); + + + removeIds(doc, "toc", "catlinks", "Notes", "mw-navigation", "mw-data-after-content", "jump-to-nav"); + removeByClass(doc, "mw-references-wrap", "references", "reference", "siteSub", "refbegin"); + + // doc.getElementById("mw-content-text").insertChildren(0, doc.getElementById("firstHeading")); + doc.getElementById("content").tagName("article"); + doc.getAllElements().forEach(elem -> { + if (elem.parent() != null + && "summary".equals(elem.parent().tagName())) + { + elem.parent().replaceWith(elem); + } + }); + + doc.getElementsByTag("span").forEach(elem -> { + if ("pre".equals(elem.parent().tagName())) { + if (elem.hasClass("linenos")) { + elem.replaceWith(new TextNode(String.format("%-4s", elem.text()))); + } + else { + elem.replaceWith(new TextNode(elem.text())); + } + } + else { + elem.replaceWith(new TextNode(" " + elem.text() + " ")); + } + }); + + doc.getElementsByTag("details").forEach(deets -> { + if (deets.children().size() == 1) { + deets.replaceWith(deets.children().first()); + } + else { + deets.tagName("div"); + } + }); + + removeEmptyTags(doc, "li"); + removeEmptyTags(doc, "ul"); + removeEmptyTags(doc, "div"); + + doc.getElementsByTag("p").forEach(elem -> { + if ("blockquote".equals(elem.parent().tagName())) { + elem.replaceWith(new TextNode(elem.text())); + } + }); + + removeEmptyTags(doc, "p"); + + doc.getElementsByTag("h4").forEach(elem -> { + var next = elem.nextElementSibling(); + if (next == null) { + elem.remove(); + return; + } + String nextTagName = next.tagName(); + if ("h4".equals(nextTagName) || "h3".equals(nextTagName) || "h2".equals(nextTagName)) { + elem.remove(); + } + }); + + + doc.getElementsByTag("h3").forEach(elem -> { + var next = elem.nextElementSibling(); + if (next == null) { + elem.remove(); + return; + } + String nextTagName = next.tagName(); + if ("h3".equals(nextTagName) || "h2".equals(nextTagName)) { + elem.remove(); + } + }); + + doc.getElementsByTag("h2").forEach(elem -> { + var next = elem.nextElementSibling(); + if (next == null) { + elem.remove(); + return; + } + if ("h2".equals(next.tagName())) { + elem.remove(); + } + }); + doc.getElementsByTag("footer").remove(); + doc.getElementsByTag("table").forEach(table -> { + table.attr("border", "1"); + }); + doc.getElementsByTag("table").forEach(table -> { + if ("right".equals(table.attr("align"))) { + table.remove(); + } + }); + + doc.getElementsByTag("head").append(""); + doc.getElementsByTag("head").append(""); + doc.getElementsByTag("head").append(""); + doc.getElementsByTag("head").append(""); + doc.getElementsByTag("head").append(""); + doc.getElementsByTag("head").append(""); + + if (!topLinks.isEmpty()) { + doc.getElementsByTag("article").append("

    Index of References

    "); + } + + if (!disambig.isEmpty()) { + doc.getElementsByTag("h1").first().nextElementSibling().prepend("
    See Also" + + disambig.stream().map(href -> ""+href.getValue()+"").collect(Collectors.joining("
    ")) + + ""); + } + + doc.getElementsByTag("article").first().parent().prepend("
    "); + doc.getElementsByTag("article").first().parent().append(""); + + doc.getElementsByTag("div").forEach(tag -> { + if (tag.text().startsWith("This article is issued from Wikipedia")) { + tag.remove(); // we have our own + } + }); + doc.getAllElements().forEach(elem -> { + var classes = elem.classNames().stream().filter(this::isWikiClass).collect(Collectors.toList()); + classes.forEach(elem::removeClass); + elem.removeAttr("lang"); + elem.removeAttr("dir"); + elem.removeAttr("id"); + elem.removeAttr("role"); + elem.removeAttr("style"); + elem.removeAttr("tabindex"); + elem.removeAttr("aria-haspopup"); + elem.removeAttr("data-section-id"); + elem.removeAttr("aria-expanded"); + elem.removeAttr("aria-pressed"); + elem.removeAttr("open"); + elem.removeAttr("data-level"); + }); + + marginifyHeaders(doc); + + + doc.filter(new NodeFilter() { + @Override + public FilterResult head(Node node, int depth) { + if (node instanceof Comment) { + return FilterResult.REMOVE; + } + return FilterResult.CONTINUE; + } + + @Override + public FilterResult tail(Node node, int depth) { + if (node instanceof Comment) { + return FilterResult.REMOVE; + } + return FilterResult.CONTINUE; + } + }); + return doc.html(); + } + + @SneakyThrows + private void convertMathTag(Element math) { + + try { + var formula = math.getElementsByTag("math"); + var converter = net.sourceforge.jeuclid.converter.Converter.getInstance(); + var sos = new ByteArrayOutputStream(); + var alt = Optional.ofNullable(formula.attr("alttext")) + .or(() -> Optional.ofNullable(math.getElementsByTag("annotation").text())) + .orElse(""); + + var layoutContext = new LayoutContextImpl(LayoutContextImpl.getDefaultLayoutContext()); + + String parentTag = math.parent().tag().getName(); + boolean topLevel = "dd".equals(parentTag) || "div".equals(parentTag) + || (math.nextElementSibling() == null && math.previousElementSibling() == null); + + int mathSize = 16; + if (topLevel) + mathSize = 24; + if ("h1".equals(parentTag)) { + mathSize = 28; + } + if ("h2".equals(parentTag)) { + mathSize = 24; + } + if ("h3".equals(parentTag)) { + mathSize = 22; + } + layoutContext.setParameter(Parameter.MATHSIZE, mathSize); + + layoutContext.setParameter(Parameter.ANTIALIAS, true); + layoutContext.setParameter(Parameter.SCRIPTMINSIZE, 8); + layoutContext.setParameter(Parameter.FONTS_SERIF, "STIX Two Math"); + layoutContext.setParameter(Parameter.FONTS_SCRIPT, "STIX Two Math"); + layoutContext.setParameter(Parameter.DISPLAY, topLevel ? Display.BLOCK : Display.INLINE); + + converter.convert(MathMLParserSupport.parseString( + formula.html().replace(" ", " ")), sos, + "image/png", + layoutContext).toString(); + + math.tagName("img") + .text("") + .attr("src", "data:image/png;base64," + Base64.getEncoder().encodeToString(sos.toByteArray())) + .attr("alt", alt); + + } + catch (Exception ex) { + ex.printStackTrace(); + } + } + + private void removeEmptyTags(Document doc, String tag) { + doc.getElementsByTag(tag).forEach(elem -> { + if (elem.text().isBlank() && elem.getElementsByTag("img").isEmpty()) { + elem.replaceWith(new TextNode(" ")); + } + + }); + } + + @NotNull + private List> getWikiPageLinks(Document doc) { + List> topLinks = new ArrayList<>(); + Optional.ofNullable(doc.select("p a")).ifPresent(links -> links.forEach(atag -> { + String href = atag.attr("href"); + + if (href != null && !href.isBlank() + && !href.contains(":") + && !href.startsWith("#") + ) { + topLinks.add(Pair.of(href, atag.attr("title"))); + } + })); + return topLinks; + } + + + @NotNull + private List getWikiPageLinkText(Document doc) { + List topLinks = new ArrayList<>(); + + doc.select("p a,h1,h2,h3,h4,i,em,strong,b").forEach(e -> topLinks.add(e.text())); + + return topLinks; + } + + @NotNull + private List> getDisambiguationLinks(Document doc) { + List> disambig = new ArrayList<>(); + + + Optional.ofNullable(doc.getElementsByClass("hatnote")).ifPresent(hatnotes -> { + hatnotes.forEach(note -> { + Optional.ofNullable(note.getElementsByTag("a")) + .ifPresent(links -> links.forEach(atag -> { + String href = atag.attr("href"); + if (atag.hasClass("mw-disambig") && href != null) { + disambig.add(Pair.of(href, atag.attr("title"))); + } + })); + }); + }); + Optional.ofNullable(doc.getElementsByClass("hatnote")).ifPresent(Elements::remove); + return disambig; + } + + private void removeTag(Document doc, String... tags) { + for (String tag : tags) { + doc.getElementsByTag(tag).remove(); + } + } + private void removeByClass(Document doc, String... classes) { + for (String clas: classes) { + doc.getElementsByClass(clas).remove(); + } + } + private void removeIds(Document doc, String... ids) { + Arrays.stream(ids) + .map(doc::getElementById) + .filter(Objects::nonNull) + .forEach(Element::remove); + } + + private void marginifyHeaders(Document doc) { + Elements headers = doc.getElementsByTag("h4"); + if (headers.size() == 0) { + headers = doc.getElementsByTag("h3"); + } + headers.addClass("margin-note"); + } + + boolean isWikiClass(String clazz) { + if ("verb".equals(clazz)) { + return false; + } + if ("extern-link".equals(clazz)) { + return false; + } + if ("margin-note".equals(clazz)) { + return false; + } + return true; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/WikiSearchResult.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/WikiSearchResult.java new file mode 100644 index 00000000..f3e0f7ac --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/dict/WikiSearchResult.java @@ -0,0 +1,55 @@ +package nu.marginalia.wmsa.edge.assistant.dict; + +import lombok.AllArgsConstructor; + +import javax.annotation.Nullable; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +@AllArgsConstructor +public class WikiSearchResult { + private final String name; + @Nullable + private final String refName; + + public String getName() { + return name.replace('_', ' '); + } + @Nullable + public String getRefName() { + if (refName == null) + return null; + + return refName.replace('_', ' '); + } + + public String getUrl() { + return "https://encyclopedia.marginalia.nu/wiki/" + URLEncoder.encode(getRealName(), StandardCharsets.UTF_8); + } + + public String getRealName() { + return Optional.ofNullable(refName).orElse(name); + } + + public String getInternalName() { + return name; + } + + @Override + public int hashCode() { + return getRealName().hashCode(); + } + + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (other instanceof WikiSearchResult) { + WikiSearchResult r = (WikiSearchResult) other; + return r.getRealName().equals(getRealName()); + } + return false; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/eval/MathParser.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/eval/MathParser.java new file mode 100644 index 00000000..6bb316c1 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/eval/MathParser.java @@ -0,0 +1,394 @@ +package nu.marginalia.wmsa.edge.assistant.eval; + +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import lombok.ToString; + +import javax.inject.Singleton; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +@Singleton +public class MathParser { + private final NumberFormat df; + static final Map constants = Map.of("e", Math.E, "pi", Math.PI, "2pi", 2*Math.PI); + + Predicate isTrivial = Pattern.compile("([0-9]+\\.[0-9]*|\\.[0-9]+)").asMatchPredicate(); + + public MathParser() { + df = DecimalFormat.getInstance(Locale.US); + df.setRoundingMode(RoundingMode.HALF_UP); + df.setMaximumFractionDigits(6); + } + + public String evalFormatted(String inputExpression) throws ParseException { + if (isTrivial.test(inputExpression)) { + return ""; + } + + return df.format(eval(inputExpression)); + } + + @SneakyThrows + public double eval(String inputExpression) { + if (isTrivial.test(inputExpression)) { + return Double.parseDouble(inputExpression); + } + + List tokens = tokenize(inputExpression); + + tokens = parenthesize(tokens); + tokens = negate(tokens); + tokens = functions(tokens); + tokens = binaryExpression(tokens, "^"); + tokens = binaryExpression(tokens, "*/"); + tokens = binaryExpression(tokens, "+-"); + + return new GroupExpression(' ', tokens).evaluate(); + } + + List negate(List tokens) { + if (tokens.isEmpty()) { + return tokens; + } + for (int i = 0; i < tokens.size(); i++) { + var t = tokens.get(i); + t.transform(this::negate); + } + + + for (int i = 0; i < tokens.size()-1;) { + var t = tokens.get(i); + + if (t.tokenType != '-') { + i++; + continue; + } + + if (i == 0) { + tokens.set(0, new UniExpression('~', tokens.get(1))); + tokens.remove(1); + continue; + } + + var t2 = tokens.get(i-1); + if ("+-%*/A".indexOf(t2.tokenType) >= 0) { + tokens.set(i, new UniExpression('~', tokens.get(i+1))); + tokens.remove(i+1); + continue; + } + + i++; + } + return tokens; + } + + List functions(List tokens) { + if (tokens.isEmpty()) { + return tokens; + } + + for (int i = 0; i < tokens.size(); i++) { + var t = tokens.get(i); + t.transform(this::functions); + } + + + for (int i = 0; i < tokens.size()-1;) { + var t = tokens.get(i); + + if (t.tokenType != 'A') { + i++; + continue; + } + + tokens.set(i, new BiExpression('F', tokens.get(i), tokens.get(i+1))); + tokens.remove(i+1); + } + return tokens; + } + + + List binaryExpression(List tokens, String operators) { + for (int i = 0; i < tokens.size(); i++) { + var t = tokens.get(i); + + t.transform(toks-> binaryExpression(toks, operators)); + } + + for (int i = 1; i < tokens.size()-1; i++) { + var t = tokens.get(i); + + if (operators.indexOf(t.tokenType) >= 0) { + Token newToken = new BiExpression(t.tokenType, tokens.get(i-1), tokens.get(i+1)); + tokens.set(i, newToken); + tokens.remove(i+1); + tokens.remove(i-1); + i = i-1; + } + + } + return tokens; + } + + List parenthesize(List tokens) { + int depth = 0; + for (int i = 0; i < tokens.size(); i++) { + var t = tokens.get(i); + if (t.tokenType == ')') { + throw new IllegalArgumentException("Unbalanced parentheses"); + } + if (t.tokenType == '(') { + int j; + for (j = i+1; j < tokens.size(); j++) { + var t2 = tokens.get(j); + if (t2.tokenType == '(') { + depth++; + } + else if (t2.tokenType == ')') { + if (depth == 0) { + break; + } + else { + depth--; + } + } + } + if (j == tokens.size()) { + throw new IllegalArgumentException("Unbalanced parentheses, depth = " + depth); + } + else { + var newToken = new GroupExpression(' ', parenthesize(new ArrayList<>(tokens.subList(i+1, j)))); + tokens.set(i, newToken); + tokens.subList(i+1, j+1).clear(); + } + } + } + return tokens; + } + + List tokenize(String inputExpression) throws ParseException { + List tokens = new ArrayList<>(); + + for (int i = 0; i < inputExpression.length(); i++) { + char c = inputExpression.charAt(i); + if ("()+-/*^".indexOf(c) >= 0) { + tokens.add(new Token(c)); + } + else if (Character.isDigit(c)) { + int j; + boolean hasPeriod = false; + for (j = i+1; j < inputExpression.length(); j++) { + char c2 = inputExpression.charAt(j); + if (Character.isDigit(c2)) { + continue; + } + if (c2 == '.') { + if (!hasPeriod) { + hasPeriod = true; + continue; + } + else { + throw new ParseException("Malformatted number in " + inputExpression, j); + } + } + break; + } + tokens.add(new StringToken('0', inputExpression.substring(i, j))); + i = j-1; + } + else if (Character.isAlphabetic(c)) { + int j; + for (j = i+1; j < inputExpression.length(); j++) { + char c2 = inputExpression.charAt(j); + if (Character.isAlphabetic(c2)) { + continue; + } + break; + } + var str = inputExpression.substring(i, j); + if (constants.containsKey(str)) { + tokens.add(new StringToken('C', str)); + } + else { + tokens.add(new StringToken('A', str)); + } + i = j-1; + } + else if(Character.isSpaceChar(c)) { + // + } + else { + throw new ParseException(inputExpression, i); + } + } + return tokens; + } +} + +@AllArgsConstructor @ToString +class Token { + public final char tokenType; + + public double evaluate() { + throw new IllegalArgumentException("Can't evaluate" + this); + } + + public void transform(Function, List> mapper) { + + } +} + +@ToString +class StringToken extends Token { + public final String value; + + public StringToken(char tokenType, String value) { + super(tokenType); + + this.value = value; + } + + public double evaluate() { + var cv = MathParser.constants.get(value); + if (cv != null) { + return cv; + } + + return Double.parseDouble(value); + } +} + +class UniExpression extends Token { + public final Token argument; + + public UniExpression(char tokenType, Token argument) { + super(tokenType); + + this.argument = argument; + } + + public String toString() { + return String.format("(%s %s)", tokenType, argument); + } + + @Override + public double evaluate() { + if (tokenType == '~') { + return -argument.evaluate(); + } + throw new IllegalArgumentException("Can't evaluate" + this); + } + + public void transform(Function, List> mapper) { + argument.transform(mapper); + } +} + +@ToString +class GroupExpression extends Token { + public List argument; + + public GroupExpression(char tokenType, List argument) { + super(tokenType); + + this.argument = argument; + } + + @Override + public double evaluate() { + if (argument.size() == 1) { + return argument.get(0).evaluate(); + } + throw new IllegalArgumentException("Can't evaluate" + this); + } + + public void transform(Function, List> mapper) { + argument = mapper.apply(argument); + } +} + + +class BiExpression extends Token { + public final Token left; + public final Token right; + + BiExpression(char tokenType, Token left, Token right) { + super(tokenType); + + this.left = left; + this.right = right; + } + + public String toString() { + return String.format("(%s %s %s)", tokenType, left, right); + } + + public void transform(Function, List> mapper) { + left.transform(mapper); + right.transform(mapper); + } + + @Override + public double evaluate() { + double rightVal = right.evaluate(); + switch (tokenType) { + case '+': + return left.evaluate() + rightVal; + case '-': + return left.evaluate() - rightVal; + case '*': + return left.evaluate() * rightVal; + case '/': { + if (rightVal == 0) { + return Double.NaN; + } + return left.evaluate() / rightVal; + } + case '%': + { + if (rightVal == 0) { + return Double.NaN; + } + return left.evaluate() % rightVal; + } + case '^': + return Math.pow(left.evaluate(), rightVal); + case 'F': + return evalFunction(rightVal); + default: + throw new IllegalArgumentException("Can't evaluate" + this); + } + } + + private double evalFunction(double rightVal) { + StringToken left2 = (StringToken) left; + switch (left2.value.toLowerCase()) { + case "sqrt": + return Math.sqrt(rightVal); + case "log": + return Math.log(rightVal); + case "log10": + return Math.log10(rightVal); + case "log2": + return Math.log(rightVal)/Math.log(2); + case "cos": + return Math.cos(rightVal); + case "sin": + return Math.sin(rightVal); + case "tan": + return Math.tan(rightVal); + default: + throw new IllegalArgumentException("Can't evaluate" + this); + } + } +} \ No newline at end of file diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/eval/Unit.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/eval/Unit.java new file mode 100644 index 00000000..e8da905e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/eval/Unit.java @@ -0,0 +1,15 @@ +package nu.marginalia.wmsa.edge.assistant.eval; + +public class Unit { + + public final String name; + public final String type; + public final double baseValue; + + public Unit(String type, double value, String name) { + this.type = type; + this.name = name; + this.baseValue = value; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/eval/Units.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/eval/Units.java new file mode 100644 index 00000000..6a0d4be8 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/eval/Units.java @@ -0,0 +1,122 @@ +package nu.marginalia.wmsa.edge.assistant.eval; + +import com.opencsv.CSVReader; +import lombok.SneakyThrows; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.text.DecimalFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@Singleton +public class Units { + + private final Map unitsByName = new HashMap<>(); + private final MathParser mathParser; + + @SneakyThrows + @Inject + public Units(MathParser mathParser) { + this.mathParser = mathParser; + + var resource = Objects.requireNonNull(ClassLoader.getSystemResourceAsStream("units.csv"), + "Could not load IP location db"); + + try (var reader = new CSVReader(new InputStreamReader(resource, StandardCharsets.UTF_8))) { + for (;;) { + String[] vals = reader.readNext(); + if (vals == null) { + break; + } + + var unit = new Unit(vals[1], Double.parseDouble(vals[0]), vals[2]); + + for (int i = 2; i < vals.length; i++) { + unitsByName.put(vals[i].toLowerCase(), unit); + } + } + } + + } + + public Optional convert(String value, String fromUnitName, String toUnitName) { + var fromUnit = unitsByName.get(fromUnitName.toLowerCase()); + var toUnit = unitsByName.get(toUnitName.toLowerCase()); + + if (Objects.equals(fromUnit, toUnit)) { + return Optional.of(value + " " + fromUnit.name); + } + if (null == fromUnit || null == toUnit) { + return Optional.empty(); + } + + if (!Objects.equals(toUnit.type, fromUnit.type)) { + return Optional.empty(); + } + + double valNum; + try { + valNum = mathParser.eval(value); + } + catch (Exception ex) { + return Optional.empty(); + } + double convertedValue; + if ("TEMPERATURE".equals(fromUnit.type)) { + convertedValue = convertTemperature(valNum, fromUnit, toUnit); + } + else { + convertedValue = fromUnit.baseValue * valNum / toUnit.baseValue; + } + + boolean negative = convertedValue < 0; + if (negative) { + convertedValue = -convertedValue; + } + + long intFraction = (int) Math.log10(convertedValue); + + int sigFigs = countSigFigs(value); + var nf = new DecimalFormat(); + nf.setMaximumIntegerDigits(1 + (int) intFraction); + nf.setMaximumFractionDigits(1 + sigFigs - (int)intFraction); + return Optional.of((negative ? "-":"") + nf.format(convertedValue) + " " + toUnit.name); + } + + private double convertTemperature(double valNum, Unit fromUnit, Unit toUnit) { + if ("C".equals(fromUnit.name)) { + if ("K".equals(toUnit.name)) { + return valNum + 273.15; + } + else if ("F".equals(toUnit.name)) { + return 32. + 9*valNum/5; + } + } + else if ("F".equals(fromUnit.name)) { + if ("C".equals(toUnit.name)) { + return 5*(valNum - 32.)/9; + } + if ("K".equals(toUnit.name)) { + return 5*(valNum - 32.)/9 + 273.15; + } + } + else if ("K".equals(fromUnit.name)) { + if ("C".equals(toUnit.name)) { + return valNum - 273.15; + } + else if ("F".equals(toUnit.name)) { + return 32. + 9*(valNum-273.15)/5; + } + } + return 0; + } + + private int countSigFigs(String value) { + return (int) value.chars().filter(Character::isDigit).count(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/screenshot/ScreenshotService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/screenshot/ScreenshotService.java new file mode 100644 index 00000000..59a719b2 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/screenshot/ScreenshotService.java @@ -0,0 +1,132 @@ +package nu.marginalia.wmsa.edge.assistant.screenshot; + +import com.google.common.base.Strings; +import com.google.inject.Inject; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDao; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeId; +import spark.Request; +import spark.Response; +import spark.utils.IOUtils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.NoSuchElementException; + +import static java.lang.Integer.parseInt; + +public class ScreenshotService { + + private final Path screenshotsRoot = Path.of("/var/lib/wmsa/archive/screenshots/screenshots/"); + private final Path screenshotsRootWebp = Path.of("/var/lib/wmsa/archive.fast/screenshots/"); + private final EdgeDataStoreDao edgeDataStoreDao; + private final long MIN_FILE_SIZE = 4096; + + @Inject + public ScreenshotService(EdgeDataStoreDao edgeDataStoreDao) { + this.edgeDataStoreDao = edgeDataStoreDao; + } + + public boolean hasScreenshot(EdgeId domainId) { + EdgeDomain domain = edgeDataStoreDao.getDomain(domainId); + + Path p = getScreenshotPath(screenshotsRootWebp, domain, ".webp"); + if (p == null) { + p = getScreenshotPath(screenshotsRoot, domain, ".png"); + } + + try { + return p != null && Files.size(p) >= MIN_FILE_SIZE; + } catch (IOException e) { + return false; + } + } + + @SneakyThrows + public Object serveScreenshotRequest(Request request, Response response) { + if (Strings.isNullOrEmpty(request.params("id"))) { + response.redirect("https://search.marginalia.nu/"); + return null; + } + + int id = parseInt(request.params("id")); + + Path p = null; + if (id == 0) { + p = screenshotsRootWebp.resolve("dummy-snapshot.webp"); + } else { + EdgeDomain domain; + try { + domain = edgeDataStoreDao.getDomain(new EdgeId<>(id)); + p = getScreenshotPath(screenshotsRootWebp, domain, ".webp"); + if (p == null) { + p = getScreenshotPath(screenshotsRoot, domain, ".png"); + } + + if (p != null && Files.size(p) <= MIN_FILE_SIZE) { + p = null; + } + } catch (NoSuchElementException ex) { + domain = new EdgeDomain("error.example.com"); + } + + if (p == null) { + response.type("image/svg+xml"); + + return String.format("\n" + + "\n" + + " \n" + + " \n" + + " Placeholder\n" + + " %s\n" + + " \n" + + "\n", domain); + } + } + response.status(200); + response.header("Cache-control", "public,max-age=3600"); + if (p.toString().endsWith("webp")) { + response.type("image/webp"); + } else { + response.type("image/png"); + } + IOUtils.copy(new ByteArrayInputStream(Files.readAllBytes(p)), response.raw().getOutputStream()); + return ""; + } + + private Path getScreenshotPath(Path root, EdgeDomain domain, String ending) { + + var p = root.resolve(domain.toString() + ending); + if (!p.normalize().startsWith(root)) { + return null; + } + + if (!Files.exists(p)) { + return null; + } + + return p; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/suggest/Suggestions.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/suggest/Suggestions.java new file mode 100644 index 00000000..69b3f7f1 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/assistant/suggest/Suggestions.java @@ -0,0 +1,145 @@ +package nu.marginalia.wmsa.edge.assistant.suggest; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.inject.Inject; +import com.google.inject.name.Named; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.assistant.dict.SpellChecker; +import org.apache.commons.collections4.trie.PatriciaTrie; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class Suggestions { + private final PatriciaTrie suggestionsTrie; + private final NGramDict nGramDict; + private final SpellChecker spellChecker; + + private static final Pattern suggestionPattern = Pattern.compile("^[a-zA-Z0-9]+( [a-zA-Z0-9]+)*$"); + private static final Logger logger = LoggerFactory.getLogger(Suggestions.class); + + private static final int MIN_SUGGEST_LENGTH = 3; + @Inject + public Suggestions(@Named("suggestions-file") Path suggestionsFile, + SpellChecker spellChecker, + NGramDict dict + ) { + this.spellChecker = spellChecker; + + suggestionsTrie = loadSuggestions(suggestionsFile); + nGramDict = dict; + + logger.info("Loaded {} suggestions", suggestionsTrie.size()); + } + + private static PatriciaTrie loadSuggestions(Path file) { + try (var lines = Files.lines(file)) { + var ret = new PatriciaTrie(); + + lines.filter(suggestionPattern.asPredicate()) + .filter(line -> line.length()<32) + .map(String::toLowerCase) + .forEach(w -> ret.put(w, w)); + + return ret; + } + catch (IOException ex) { + logger.error("Failed to load suggestions file", ex); + return new PatriciaTrie(); + } + } + + private record SuggestionStream(String prefix, Stream suggestionStream) { + public Stream stream() { + return suggestionStream.map(s -> prefix + s); + } + + } + + public List getSuggestions(int count, String searchWord) { + if (searchWord.length() < MIN_SUGGEST_LENGTH) { + return Collections.emptyList(); + } + + searchWord = trimLeading(searchWord.toLowerCase()); + + List streams = new ArrayList<>(4); + streams.add(new SuggestionStream("", getSuggestionsForKeyword(count, searchWord))); + + int sp = searchWord.lastIndexOf(' '); + if (sp >= 0) { + String prefixString = searchWord.substring(0, sp+1); + String suggestString = searchWord.substring(sp+1); + + if (suggestString.length() >= MIN_SUGGEST_LENGTH) { + streams.add(new SuggestionStream(prefixString, getSuggestionsForKeyword(count, suggestString))); + } + + } + streams.add(spellCheckStream(searchWord)); + + return streams.stream().flatMap(SuggestionStream::stream).limit(count).collect(Collectors.toList()); + } + + private SuggestionStream spellCheckStream(String word) { + int start = word.lastIndexOf(' '); + String prefix; + String corrWord; + + if (start < 0) { + corrWord = word; + prefix = ""; + } + else { + prefix = word.substring(0, start + 1); + corrWord = word.substring(start + 1); + } + + if (corrWord.length() >= MIN_SUGGEST_LENGTH) { + Supplier> suggestionsLazyEval = () -> spellChecker.correct(corrWord).stream(); + return new SuggestionStream(prefix, Stream.of(suggestionsLazyEval).flatMap(Supplier::get)); + } + else { + return new SuggestionStream("", Stream.empty()); + } + } + + private String trimLeading(String word) { + + for (int i = 0; i < word.length(); i++) { + if (!Character.isWhitespace(word.charAt(i))) + return word.substring(i); + } + + return ""; + } + + public Stream getSuggestionsForKeyword(int count, String prefix) { + var start = suggestionsTrie.select(prefix); + + if (!start.getKey().startsWith(prefix)) { + return Stream.empty(); + } + + Map scach = new HashMap<>(512); + Function valr = s -> -nGramDict.getTermFreqHash(scach.computeIfAbsent(s, NGramDict::getStringHash)); + + return Stream.iterate(start.getKey(), Objects::nonNull, suggestionsTrie::nextKey) + .takeWhile(s -> s.startsWith(prefix)) + .limit(256) + .sorted(Comparator.comparing(valr).thenComparing(String::length).thenComparing(Comparator.naturalOrder())) + .limit(count); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/ConvertedDomainReader.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/ConvertedDomainReader.java new file mode 100644 index 00000000..7c3621cb --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/ConvertedDomainReader.java @@ -0,0 +1,62 @@ +package nu.marginalia.wmsa.edge.converting; + +import com.github.luben.zstd.ZstdInputStream; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSyntaxException; +import crawlercommons.utils.Strings; +import nu.marginalia.wmsa.edge.converting.interpreter.Instruction; +import nu.marginalia.wmsa.edge.converting.interpreter.InstructionTag; +import nu.marginalia.wmsa.edge.crawling.model.CrawledDomain; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.io.*; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +public class ConvertedDomainReader { + private static final Logger logger = LoggerFactory.getLogger(ConvertedDomainReader.class); + private final Gson gson; + + @Inject + public ConvertedDomainReader(Gson gson) { + this.gson = gson; + } + + public List read(Path path, int cntHint) throws IOException { + List ret = new ArrayList<>(cntHint); + + try (var br = new BufferedReader(new InputStreamReader(new ZstdInputStream(new BufferedInputStream(new FileInputStream(path.toFile())))))) { + String line; + for (;;) { + line = br.readLine(); + + if (line == null) { + break; + } + if (Strings.isBlank(line)) { + continue; + } + var parts= line.split(" ", 2); + var type = InstructionTag.valueOf(parts[0]).clazz; + + try { + ret.add(gson.fromJson(parts[1], type)); + } + catch (JsonParseException ex) { + logger.warn("Failed to deserialize {} {}", type.getSimpleName(), StringUtils.abbreviate(parts[1], 255)); + logger.warn("Json error", ex); + } + } + } + + return ret; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/ConverterMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/ConverterMain.java new file mode 100644 index 00000000..84a5d2f0 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/ConverterMain.java @@ -0,0 +1,137 @@ +package nu.marginalia.wmsa.edge.converting; + +import com.google.gson.*; +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import nu.marginalia.wmsa.edge.converting.interpreter.Instruction; +import nu.marginalia.wmsa.edge.converting.processor.DomainProcessor; +import nu.marginalia.wmsa.edge.converting.processor.InstructionsCompiler; +import nu.marginalia.wmsa.edge.crawling.CrawlPlanLoader; +import nu.marginalia.wmsa.edge.crawling.CrawledDomainReader; +import nu.marginalia.wmsa.edge.crawling.WorkLog; +import nu.marginalia.wmsa.edge.crawling.CrawlerSpecificationLoader; +import nu.marginalia.wmsa.edge.crawling.model.CrawledDomain; +import nu.marginalia.util.ParallelPipe; +import nu.marginalia.wmsa.edge.model.EdgeCrawlPlan; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ConverterMain { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final DomainProcessor processor; + private final InstructionsCompiler compiler; + private final WorkLog processLog; + private final CrawledInstructionWriter instructionWriter; + + private Gson gson; + private final CrawledDomainReader reader = new CrawledDomainReader(); + + private final Map domainToId = new HashMap<>(); + private final Map idToFileName = new HashMap<>(); + + public static void main(String... args) throws IOException { + + if (args.length != 1) { + System.err.println("Arguments: crawl-plan.yaml"); + System.exit(0); + } + var plan = new CrawlPlanLoader().load(Path.of(args[0])); + + Injector injector = Guice.createInjector( + new ConverterModule(plan) + ); + + injector.getInstance(ConverterMain.class); + } + + private static void requireArgs(String[] args, String... help) { + if (args.length != help.length) { + System.out.println("Usage: " + String.join(", ", help)); + System.exit(255); + } + } + + @Inject + public ConverterMain( + EdgeCrawlPlan plan, + DomainProcessor processor, + InstructionsCompiler compiler, + Gson gson + ) throws Exception { + this.processor = processor; + this.compiler = compiler; + this.gson = gson; + + instructionWriter = new CrawledInstructionWriter(plan.process.getDir(), gson); + + logger.info("Loading input spec"); + CrawlerSpecificationLoader.readInputSpec(plan.getJobSpec(), + spec -> domainToId.put(spec.domain, spec.id)); + + logger.info("Replaying crawl log"); + WorkLog.readLog(plan.crawl.getLogFile(), + entry -> idToFileName.put(entry.id(), entry.path())); + + logger.info("Starting pipe"); + processLog = new WorkLog(plan.process.getLogFile()); + + + var pipe = new ParallelPipe("Crawler", 48, 4, 2) { + @Override + protected ProcessingInstructions onProcess(CrawledDomain domainData) throws Exception { + var processed = processor.process(domainData); + return new ProcessingInstructions(domainData.id, compiler.compile(processed)); + } + + @Override + protected void onReceive(ProcessingInstructions processedInstructions) throws IOException { + var instructions = processedInstructions.instructions; + instructions.removeIf(Instruction::isNoOp); + + String where = instructionWriter.accept(processedInstructions.id, instructions); + processLog.setJobToFinished(processedInstructions.id, where, instructions.size()); + } + }; + + domainToId.forEach((domain, id) -> { + String fileName = idToFileName.get(id); + Path dest = getFilePath(plan.crawl.getDir(), fileName); + logger.info("{} - {} - {}", domain, id, dest); + + if (!processLog.isJobFinished(id)) { + try { + var cd = reader.read(dest); + pipe.accept(cd); + + } catch (IOException e) { + logger.error("Failed to read {}", dest); + } + } + }); + + pipe.join(); + + processLog.close(); + + logger.info("Finished"); + + System.exit(0); + } + + record ProcessingInstructions(String id, List instructions) {} + + private Path getFilePath(Path dir, String fileName) { + String sp1 = fileName.substring(0, 2); + String sp2 = fileName.substring(2, 4); + return dir.resolve(sp1).resolve(sp2).resolve(fileName); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/ConverterModule.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/ConverterModule.java new file mode 100644 index 00000000..bd003030 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/ConverterModule.java @@ -0,0 +1,60 @@ +package nu.marginalia.wmsa.edge.converting; + +import com.google.gson.*; +import com.google.inject.AbstractModule; +import com.google.inject.name.Names; +import marcono1234.gson.recordadapter.RecordTypeAdapterFactory; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.model.EdgeCrawlPlan; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +import java.net.URISyntaxException; +import java.nio.file.Path; + +public class ConverterModule extends AbstractModule { + + private final EdgeCrawlPlan plan; + + public ConverterModule(EdgeCrawlPlan plan) { + this.plan = plan; + } + + public void configure() { + bind(EdgeCrawlPlan.class).toInstance(plan); + + bind(Gson.class).toInstance(createGson()); + + bind(Double.class).annotatedWith(Names.named("min-document-quality")).toInstance(-15.); + bind(Integer.class).annotatedWith(Names.named("min-document-length")).toInstance(100); + bind(Integer.class).annotatedWith(Names.named("max-title-length")).toInstance(128); + bind(Integer.class).annotatedWith(Names.named("max-summary-length")).toInstance(255); + + bind(LanguageModels.class).toInstance(new LanguageModels( + Path.of("/var/lib/wmsa/model/ngrams-generous-emstr.bin"), + Path.of("/var/lib/wmsa/model/tfreq-new-algo3.bin"), + Path.of("/var/lib/wmsa/model/opennlp-sentence.bin"), + Path.of("/var/lib/wmsa/model/English.RDR"), + Path.of("/var/lib/wmsa/model/English.DICT"), + Path.of("/var/lib/wmsa/model/opennlp-tok.bin") + )); + } + + private Gson createGson() { + + return new GsonBuilder() + .registerTypeAdapter(EdgeUrl.class, (JsonSerializer) (src, typeOfSrc, context) -> new JsonPrimitive(src.toString())) + .registerTypeAdapter(EdgeDomain.class, (JsonSerializer) (src, typeOfSrc, context) -> new JsonPrimitive(src.toString())) + .registerTypeAdapter(EdgeUrl.class, (JsonDeserializer) (json, typeOfT, context) -> { + try { + return new EdgeUrl(json.getAsString()); + } catch (URISyntaxException e) { + throw new JsonParseException("URL Parse Exception", e); + } + }) + .registerTypeAdapter(EdgeDomain.class, (JsonDeserializer) (json, typeOfT, context) -> new EdgeDomain(json.getAsString())) + .registerTypeAdapterFactory(RecordTypeAdapterFactory.builder().allowMissingComponentValues().create()) + .create(); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/CrawledInstructionWriter.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/CrawledInstructionWriter.java new file mode 100644 index 00000000..e269528a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/CrawledInstructionWriter.java @@ -0,0 +1,64 @@ +package nu.marginalia.wmsa.edge.converting; + +import com.github.luben.zstd.ZstdOutputStream; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import nu.marginalia.wmsa.edge.converting.interpreter.Instruction; +import nu.marginalia.wmsa.edge.crawling.model.CrawledDomain; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +public class CrawledInstructionWriter { + private final Path outputDir; + private Gson gson; + private static final Logger logger = LoggerFactory.getLogger(CrawledInstructionWriter.class); + + public CrawledInstructionWriter(Path outputDir, Gson gson) { + this.outputDir = outputDir; + this.gson = gson; + + if (!Files.isDirectory(outputDir)) { + throw new IllegalArgumentException("Output dir " + outputDir + " does not exist"); + } + } + + public String accept(String id, List instructionList) throws IOException { + Path outputFile = getOutputFile(id); + + if (Files.exists(outputFile)) { + Files.delete(outputFile); + } + + try (var outputStream = new OutputStreamWriter(new ZstdOutputStream(new BufferedOutputStream(new FileOutputStream(outputFile.toFile()))))) { + logger.info("Writing {} - {}", id, instructionList.size()); + + for (var instr : instructionList) { + outputStream.append(instr.tag().name()); + outputStream.append(' '); + gson.toJson(instr, outputStream); + outputStream.append('\n'); + } + } + + return outputFile.getFileName().toString(); + } + + private Path getOutputFile(String id) throws IOException { + String first = id.substring(0, 2); + String second = id.substring(2, 4); + + Path destDir = outputDir.resolve(first).resolve(second); + if (!Files.exists(destDir)) { + Files.createDirectories(destDir); + } + return destDir.resolve(id + ".pzstd"); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/LoaderMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/LoaderMain.java new file mode 100644 index 00000000..95a9fbb4 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/LoaderMain.java @@ -0,0 +1,140 @@ +package nu.marginalia.wmsa.edge.converting; + +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.zaxxer.hikari.HikariDataSource; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.edge.converting.interpreter.Instruction; +import nu.marginalia.wmsa.edge.converting.loader.Loader; +import nu.marginalia.wmsa.edge.converting.loader.LoaderFactory; +import nu.marginalia.wmsa.edge.crawling.CrawlPlanLoader; +import nu.marginalia.wmsa.edge.crawling.WorkLog; +import nu.marginalia.wmsa.edge.index.client.EdgeIndexClient; +import nu.marginalia.wmsa.edge.model.EdgeCrawlPlan; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class LoaderMain { + + private final Path processDir; + private EdgeCrawlPlan plan; + private final ConvertedDomainReader instructionsReader; + private final HikariDataSource dataSource; + + private static final Logger logger = LoggerFactory.getLogger(LoaderMain.class); + private final LoaderFactory loaderFactory; + private EdgeIndexClient indexClient; + private volatile boolean running = true; + + Thread processorThread = new Thread(this::processor, "Processor Thread"); + + public static void main(String... args) throws IOException { + if (args.length != 1) { + System.err.println("Arguments: crawl-plan.yaml"); + System.exit(0); + } + var plan = new CrawlPlanLoader().load(Path.of(args[0])); + + Injector injector = Guice.createInjector( + new ConverterModule(plan), + new DatabaseModule() + ); + + var instance = injector.getInstance(LoaderMain.class); + instance.run(); + } + + @Inject + public LoaderMain(EdgeCrawlPlan plan, + ConvertedDomainReader instructionsReader, + HikariDataSource dataSource, + LoaderFactory loaderFactory, + EdgeIndexClient indexClient) { + + this.processDir = plan.process.getDir(); + this.plan = plan; + this.instructionsReader = instructionsReader; + this.dataSource = dataSource; + this.loaderFactory = loaderFactory; + this.indexClient = indexClient; + + processorThread.start(); + } + + @SneakyThrows + public void run() { + var logFile = plan.process.getLogFile(); + + AtomicInteger loadTotal = new AtomicInteger(); + WorkLog.readLog(logFile, entry -> { loadTotal.incrementAndGet(); }); + LoaderMain.loadTotal = loadTotal.get(); + + WorkLog.readLog(logFile, entry -> { + load(entry.path(), entry.cnt()); + }); + + processorThread.join(); + indexClient.close(); + } + + private volatile static int loadTotal; + private volatile static int loaded = 0; + + private void load(String path, int cnt) { + String first = path.substring(0, 2); + String second = path.substring(2, 4); + Path destDir = processDir.resolve(first).resolve(second).resolve(path); + + + + try { + var loader = loaderFactory.create(cnt); + var instructions = instructionsReader.read(destDir, cnt); + processQueue.put(new LoadJob(path, loader, instructions)); + } catch (Exception e) { + logger.error("Failed to load " + destDir, e); + } + } + + static TaskStats taskStats = new TaskStats(100); + + private record LoadJob(String path, Loader loader, List instructionList) { + public void run() { + long startTime = System.currentTimeMillis(); + for (var i : instructionList) { + i.apply(loader); + } + + loader.finish(); + long loadTime = System.currentTimeMillis() - startTime; + taskStats.observe(loadTime); + logger.info("Loaded {}/{} : {} ({}) {}ms {} l/s", taskStats.getCount(), loadTotal, path, loader.data.sizeHint, loadTime, taskStats.avgTime()); + } + + }; + private static final LinkedBlockingQueue processQueue = new LinkedBlockingQueue<>(2); + + private void processor() { + try { + while (running || !processQueue.isEmpty()) { + LoadJob job = processQueue.poll(1, TimeUnit.SECONDS); + + if (job != null) { + job.run(); + } + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/TaskStats.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/TaskStats.java new file mode 100644 index 00000000..7c0384bb --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/TaskStats.java @@ -0,0 +1,37 @@ +package nu.marginalia.wmsa.edge.converting; + +public class TaskStats { + private final long[] taskTimes; + private int count = 0; + private long total = 0; + + public TaskStats(int windowSize) { + taskTimes = new long[windowSize]; + } + + public synchronized void observe(long time) { + taskTimes[count++%taskTimes.length] = time; + total += time; + } + + public double avgTime() { + long tts = 0; + long tot; + + if (count < taskTimes.length) tot = count; + else tot = taskTimes.length; + + for (int i = 0; i < tot; i++) tts += taskTimes[i]; + + return (tot * 10_000L / tts)/10.; + } + + public double totalTime() { + return total; + } + + public int getCount() { + return count; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/Instruction.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/Instruction.java new file mode 100644 index 00000000..7f40edf6 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/Instruction.java @@ -0,0 +1,8 @@ +package nu.marginalia.wmsa.edge.converting.interpreter; + +public interface Instruction { + void apply(Interpreter interpreter); + boolean isNoOp(); + + InstructionTag tag(); +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/InstructionTag.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/InstructionTag.java new file mode 100644 index 00000000..398ad430 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/InstructionTag.java @@ -0,0 +1,23 @@ +package nu.marginalia.wmsa.edge.converting.interpreter; + +import nu.marginalia.wmsa.edge.converting.interpreter.instruction.*; + +public enum InstructionTag { + + DOMAIN(LoadDomain.class), + URL(LoadUrl.class), + LINK(LoadDomainLink.class), + REDIRECT(LoadDomainRedirect.class), + WORDS(LoadKeywords.class), + PROC_DOCUMENT(LoadProcessedDocument.class), + PROC_DOCUMENT_ERR(LoadProcessedDocumentWithError.class), + PROC_DOMAIN(LoadProcessedDomain.class), + RSS(LoadRssFeed.class); + + public final Class clazz; + + InstructionTag(Class clazz) { + this.clazz = clazz; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/Interpreter.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/Interpreter.java new file mode 100644 index 00000000..1d9d13a8 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/Interpreter.java @@ -0,0 +1,28 @@ +package nu.marginalia.wmsa.edge.converting.interpreter; + +import nu.marginalia.wmsa.edge.converting.interpreter.instruction.DocumentKeywords; +import nu.marginalia.wmsa.edge.converting.interpreter.instruction.DomainLink; +import nu.marginalia.wmsa.edge.converting.interpreter.instruction.LoadProcessedDocument; +import nu.marginalia.wmsa.edge.converting.interpreter.instruction.LoadProcessedDocumentWithError; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.crawl.EdgeHtmlStandard; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageWordSet; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlState; + +public interface Interpreter { + void loadUrl(EdgeUrl[] url); + void loadDomain(EdgeDomain[] domain); + void loadRssFeed(EdgeUrl[] rssFeed); + void loadDomainLink(DomainLink[] links); + + void loadProcessedDomain(EdgeDomain domain, EdgeDomainIndexingState state, double quality); + void loadProcessedDocument(LoadProcessedDocument loadProcessedDocument); + void loadProcessedDocumentWithError(LoadProcessedDocumentWithError loadProcessedDocumentWithError); + + void loadKeywords(EdgeUrl url, DocumentKeywords[] words); + + void loadDomainRedirect(DomainLink link); +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/DocumentKeywords.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/DocumentKeywords.java new file mode 100644 index 00000000..e9d2471f --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/DocumentKeywords.java @@ -0,0 +1,17 @@ +package nu.marginalia.wmsa.edge.converting.interpreter.instruction; + +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageWords; + +import java.util.Arrays; + +public record DocumentKeywords(IndexBlock block, String... keywords) { + public DocumentKeywords(EdgePageWords words) { + this(words.block, words.words.toArray(String[]::new)); + } + + @Override + public String toString() { + return getClass().getSimpleName()+"["+block +", "+Arrays.toString(keywords)+"]"; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/DomainLink.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/DomainLink.java new file mode 100644 index 00000000..338e345c --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/DomainLink.java @@ -0,0 +1,6 @@ +package nu.marginalia.wmsa.edge.converting.interpreter.instruction; + +import nu.marginalia.wmsa.edge.model.EdgeDomain; + +public record DomainLink(EdgeDomain from, EdgeDomain to) { +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadDomain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadDomain.java new file mode 100644 index 00000000..7cf88b06 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadDomain.java @@ -0,0 +1,31 @@ +package nu.marginalia.wmsa.edge.converting.interpreter.instruction; + +import nu.marginalia.wmsa.edge.converting.interpreter.Instruction; +import nu.marginalia.wmsa.edge.converting.interpreter.InstructionTag; +import nu.marginalia.wmsa.edge.converting.interpreter.Interpreter; +import nu.marginalia.wmsa.edge.model.EdgeDomain; + +import java.util.Arrays; + +public record LoadDomain(EdgeDomain... domain) implements Instruction { + + @Override + public void apply(Interpreter interpreter) { + interpreter.loadDomain(domain); + } + + @Override + public boolean isNoOp() { + return domain.length == 0; + } + + @Override + public InstructionTag tag() { + return InstructionTag.DOMAIN; + } + + @Override + public String toString() { + return getClass().getSimpleName()+"["+Arrays.toString(domain)+"]"; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadDomainLink.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadDomainLink.java new file mode 100644 index 00000000..2d302ddf --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadDomainLink.java @@ -0,0 +1,31 @@ +package nu.marginalia.wmsa.edge.converting.interpreter.instruction; + +import nu.marginalia.wmsa.edge.converting.interpreter.Instruction; +import nu.marginalia.wmsa.edge.converting.interpreter.InstructionTag; +import nu.marginalia.wmsa.edge.converting.interpreter.Interpreter; + +import java.util.Arrays; + +public record LoadDomainLink(DomainLink... links) implements Instruction { + + @Override + public void apply(Interpreter interpreter) { + interpreter.loadDomainLink(links); + } + + @Override + public String toString() { + return getClass().getSimpleName()+"["+ Arrays.toString(links)+"]"; + } + + @Override + public InstructionTag tag() { + return InstructionTag.LINK; + } + + @Override + public boolean isNoOp() { + return links.length == 0; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadDomainRedirect.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadDomainRedirect.java new file mode 100644 index 00000000..742ad5cb --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadDomainRedirect.java @@ -0,0 +1,31 @@ +package nu.marginalia.wmsa.edge.converting.interpreter.instruction; + +import nu.marginalia.wmsa.edge.converting.interpreter.Instruction; +import nu.marginalia.wmsa.edge.converting.interpreter.InstructionTag; +import nu.marginalia.wmsa.edge.converting.interpreter.Interpreter; + +import java.util.Arrays; + +public record LoadDomainRedirect(DomainLink links) implements Instruction { + + @Override + public void apply(Interpreter interpreter) { + interpreter.loadDomainRedirect(links); + } + + @Override + public String toString() { + return getClass().getSimpleName()+"["+ links+"]"; + } + + @Override + public InstructionTag tag() { + return InstructionTag.REDIRECT; + } + + @Override + public boolean isNoOp() { + return false; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadKeywords.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadKeywords.java new file mode 100644 index 00000000..7f12bf67 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadKeywords.java @@ -0,0 +1,32 @@ +package nu.marginalia.wmsa.edge.converting.interpreter.instruction; + +import nu.marginalia.wmsa.edge.converting.interpreter.Instruction; +import nu.marginalia.wmsa.edge.converting.interpreter.InstructionTag; +import nu.marginalia.wmsa.edge.converting.interpreter.Interpreter; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +import java.util.Arrays; + +public record LoadKeywords(EdgeUrl url, DocumentKeywords... words) implements Instruction { + + @Override + public void apply(Interpreter interpreter) { + interpreter.loadKeywords(url, words); + } + + @Override + public boolean isNoOp() { + return words.length == 0; + } + + @Override + public InstructionTag tag() { + return InstructionTag.WORDS; + } + + @Override + public String toString() { + return getClass().getSimpleName()+"["+ Arrays.toString(words)+"]"; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadProcessedDocument.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadProcessedDocument.java new file mode 100644 index 00000000..9a35c58b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadProcessedDocument.java @@ -0,0 +1,35 @@ +package nu.marginalia.wmsa.edge.converting.interpreter.instruction; + +import nu.marginalia.wmsa.edge.converting.interpreter.Instruction; +import nu.marginalia.wmsa.edge.converting.interpreter.InstructionTag; +import nu.marginalia.wmsa.edge.converting.interpreter.Interpreter; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import nu.marginalia.wmsa.edge.model.crawl.EdgeHtmlStandard; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlState; + + +public record LoadProcessedDocument(EdgeUrl url, + EdgeUrlState state, + String title, + String description, + int htmlFeatures, + EdgeHtmlStandard standard, + int length, + long hash, + double quality) implements Instruction +{ + @Override + public void apply(Interpreter interpreter) { + interpreter.loadProcessedDocument(this); + } + + @Override + public InstructionTag tag() { + return InstructionTag.PROC_DOCUMENT; + } + + @Override + public boolean isNoOp() { + return false; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadProcessedDocumentWithError.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadProcessedDocumentWithError.java new file mode 100644 index 00000000..28d96989 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadProcessedDocumentWithError.java @@ -0,0 +1,28 @@ +package nu.marginalia.wmsa.edge.converting.interpreter.instruction; + +import nu.marginalia.wmsa.edge.converting.interpreter.Instruction; +import nu.marginalia.wmsa.edge.converting.interpreter.InstructionTag; +import nu.marginalia.wmsa.edge.converting.interpreter.Interpreter; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlState; + + +public record LoadProcessedDocumentWithError(EdgeUrl url, + EdgeUrlState state) implements Instruction +{ + @Override + public void apply(Interpreter interpreter) { + interpreter.loadProcessedDocumentWithError(this); + } + + @Override + public InstructionTag tag() { + return InstructionTag.PROC_DOCUMENT_ERR; + } + + @Override + public boolean isNoOp() { + return false; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadProcessedDomain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadProcessedDomain.java new file mode 100644 index 00000000..065d6211 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadProcessedDomain.java @@ -0,0 +1,26 @@ +package nu.marginalia.wmsa.edge.converting.interpreter.instruction; + +import nu.marginalia.wmsa.edge.converting.interpreter.Instruction; +import nu.marginalia.wmsa.edge.converting.interpreter.InstructionTag; +import nu.marginalia.wmsa.edge.converting.interpreter.Interpreter; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; + +public record LoadProcessedDomain(EdgeDomain domain, EdgeDomainIndexingState state, double quality) implements Instruction { + + @Override + public void apply(Interpreter interpreter) { + interpreter.loadProcessedDomain(domain, state, quality); + } + + @Override + public InstructionTag tag() { + return InstructionTag.PROC_DOMAIN; + } + + @Override + public boolean isNoOp() { + return false; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadRssFeed.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadRssFeed.java new file mode 100644 index 00000000..d4dbe0eb --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadRssFeed.java @@ -0,0 +1,32 @@ +package nu.marginalia.wmsa.edge.converting.interpreter.instruction; + +import nu.marginalia.wmsa.edge.converting.interpreter.Instruction; +import nu.marginalia.wmsa.edge.converting.interpreter.InstructionTag; +import nu.marginalia.wmsa.edge.converting.interpreter.Interpreter; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +import java.util.Arrays; + +public record LoadRssFeed(EdgeUrl... feeds) implements Instruction { + + @Override + public void apply(Interpreter interpreter) { + interpreter.loadRssFeed(feeds); + } + + @Override + public String toString() { + return getClass().getSimpleName()+"["+ Arrays.toString(feeds)+"]"; + } + + @Override + public InstructionTag tag() { + return InstructionTag.RSS; + } + + @Override + public boolean isNoOp() { + return feeds.length == 0; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadUrl.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadUrl.java new file mode 100644 index 00000000..50c2b34c --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/interpreter/instruction/LoadUrl.java @@ -0,0 +1,31 @@ +package nu.marginalia.wmsa.edge.converting.interpreter.instruction; + +import nu.marginalia.wmsa.edge.converting.interpreter.Instruction; +import nu.marginalia.wmsa.edge.converting.interpreter.InstructionTag; +import nu.marginalia.wmsa.edge.converting.interpreter.Interpreter; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +import java.util.Arrays; + +public record LoadUrl(EdgeUrl... url) implements Instruction { + + @Override + public void apply(Interpreter interpreter) { + interpreter.loadUrl(url); + } + + @Override + public String toString() { + return getClass().getSimpleName()+"["+ Arrays.toString(url)+"]"; + } + + @Override + public InstructionTag tag() { + return InstructionTag.URL; + } + + @Override + public boolean isNoOp() { + return url.length == 0; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/IndexLoadKeywords.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/IndexLoadKeywords.java new file mode 100644 index 00000000..491af2de --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/IndexLoadKeywords.java @@ -0,0 +1,64 @@ +package nu.marginalia.wmsa.edge.converting.loader; + +import com.google.inject.Inject; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.edge.converting.interpreter.instruction.DocumentKeywords; +import nu.marginalia.wmsa.edge.index.client.EdgeIndexClient; +import nu.marginalia.wmsa.edge.model.EdgeId; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageWordSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +public class IndexLoadKeywords implements Runnable { + private EdgeIndexClient client; + private static final Logger logger = LoggerFactory.getLogger(IndexLoadKeywords.class); + private final LinkedBlockingQueue insertQueue = new LinkedBlockingQueue<>(32); + + private record InsertTask(int urlId, int domainId, EdgePageWordSet wordSet) {}; + private final Thread runThread; + private volatile boolean canceled = false; + + @Inject + public IndexLoadKeywords(EdgeIndexClient client) { + this.client = client; + runThread = new Thread(this, getClass().getSimpleName()); + runThread.start(); + } + + @SneakyThrows + public void run() { + while (!canceled) { + var data = insertQueue.poll(1, TimeUnit.SECONDS); + if (data != null) { + client.putWords(Context.internal(), new EdgeId<>(data.domainId), new EdgeId<>(data.urlId), -5., data.wordSet, 1).blockingSubscribe(); + } + } + } + + public void close() throws InterruptedException { + canceled = true; + runThread.join(); + } + + public void load(LoaderData loaderData, EdgeUrl url, DocumentKeywords[] words) throws InterruptedException { + int domainId = loaderData.getDomainId(url.domain); + int urlId = loaderData.getUrlId(url); + + if (urlId < 0 || domainId < 0) { + logger.warn("Failed to get IDs for {} -- d={},u={}", url, domainId, urlId); + } + + var ws = new EdgePageWordSet(); + for (var doc : words) { + ws.append(doc.block(), Arrays.asList(doc.keywords())); + } + + insertQueue.put(new InsertTask(urlId, domainId, ws)); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/Loader.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/Loader.java new file mode 100644 index 00000000..bdd0a5b3 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/Loader.java @@ -0,0 +1,115 @@ +package nu.marginalia.wmsa.edge.converting.loader; + +import io.reactivex.rxjava3.core.Observable; +import nu.marginalia.wmsa.edge.converting.interpreter.Interpreter; +import nu.marginalia.wmsa.edge.converting.interpreter.instruction.DocumentKeywords; +import nu.marginalia.wmsa.edge.converting.interpreter.instruction.DomainLink; +import nu.marginalia.wmsa.edge.converting.interpreter.instruction.LoadProcessedDocument; +import nu.marginalia.wmsa.edge.converting.interpreter.instruction.LoadProcessedDocumentWithError; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class Loader implements Interpreter { + private final SqlLoadUrls sqlLoadUrls; + private final SqlLoadDomains sqlLoadDomains; + private final SqlLoadDomainLinks sqlLoadDomainLinks; + private final SqlLoadProcessedDomain sqlLoadProcessedDomain; + private final SqlLoadProcessedDocument sqlLoadProcessedDocument; + private final IndexLoadKeywords indexLoadKeywords; + + private static final Logger logger = LoggerFactory.getLogger(Loader.class); + + private final List processedDocumentList; + private final List processedDocumentWithErrorList; + + public final LoaderData data; + + public Loader(int sizeHint, + SqlLoadUrls sqlLoadUrls, + SqlLoadDomains sqlLoadDomains, + SqlLoadDomainLinks sqlLoadDomainLinks, + SqlLoadProcessedDomain sqlLoadProcessedDomain, + SqlLoadProcessedDocument sqlLoadProcessedDocument, + IndexLoadKeywords indexLoadKeywords) + { + data = new LoaderData(sizeHint); + + this.sqlLoadUrls = sqlLoadUrls; + this.sqlLoadDomains = sqlLoadDomains; + this.sqlLoadDomainLinks = sqlLoadDomainLinks; + this.sqlLoadProcessedDomain = sqlLoadProcessedDomain; + this.sqlLoadProcessedDocument = sqlLoadProcessedDocument; + this.indexLoadKeywords = indexLoadKeywords; + + processedDocumentList = new ArrayList<>(sizeHint); + processedDocumentWithErrorList = new ArrayList<>(sizeHint); + } + + + @Override + public void loadUrl(EdgeUrl[] urls) { + logger.debug("loadUrl({})", urls, null); + + sqlLoadUrls.load(data, urls); + } + + @Override + public void loadDomain(EdgeDomain[] domains) { + logger.debug("loadDomain({})", domains, null); + sqlLoadDomains.load(data, domains); + } + + @Override + public void loadRssFeed(EdgeUrl[] rssFeed) { + logger.debug("loadRssFeed({})", rssFeed, null); + } + + @Override + public void loadDomainLink(DomainLink[] links) { + logger.debug("loadDomainLink({})", links, null); + sqlLoadDomainLinks.load(links); + } + + @Override + public void loadProcessedDomain(EdgeDomain domain, EdgeDomainIndexingState state, double quality) { + logger.debug("loadProcessedDomain({}, {}, {})", domain, state, quality); + sqlLoadProcessedDomain.load(data, domain, state, quality); + } + + @Override + public void loadProcessedDocument(LoadProcessedDocument document) { + processedDocumentList.add(document); + } + + @Override + public void loadProcessedDocumentWithError(LoadProcessedDocumentWithError document) { + processedDocumentWithErrorList.add(document); + } + + @Override + public void loadKeywords(EdgeUrl url, DocumentKeywords[] words) { + logger.debug("loadKeywords(#{})", words.length); + try { + indexLoadKeywords.load(data, url, words); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Override + public void loadDomainRedirect(DomainLink link) { + logger.debug("loadDomainRedirect({})", link); + sqlLoadProcessedDomain.loadAlias(data, link); + } + + public void finish() { + sqlLoadProcessedDocument.load(data, processedDocumentList); + sqlLoadProcessedDocument.loadWithError(data, processedDocumentWithErrorList); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/LoaderData.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/LoaderData.java new file mode 100644 index 00000000..5c9dc4a1 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/LoaderData.java @@ -0,0 +1,43 @@ +package nu.marginalia.wmsa.edge.converting.loader; + +import gnu.trove.map.hash.TObjectIntHashMap; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +public class LoaderData { + + private final TObjectIntHashMap urlIds; + private final TObjectIntHashMap domainIds; + private EdgeDomain targetDomain; + public final int sizeHint; + + public LoaderData(int sizeHint) { + urlIds = new TObjectIntHashMap<>(sizeHint+1); + domainIds = new TObjectIntHashMap<>(10); + this.sizeHint = sizeHint; + } + + public void setTargetDomain(EdgeDomain domain) { + this.targetDomain = domain; + } + public EdgeDomain getTargetDomain() { + return targetDomain; + } + + + public void addDomain(EdgeDomain domain, int id) { + domainIds.put(domain, id); + } + + public void addUrl(EdgeUrl url, int id) { + urlIds.put(url, id); + } + + public int getUrlId(EdgeUrl url) { + return urlIds.get(url); + } + + public int getDomainId(EdgeDomain domain) { + return domainIds.get(domain); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/LoaderFactory.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/LoaderFactory.java new file mode 100644 index 00000000..f92319aa --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/LoaderFactory.java @@ -0,0 +1,32 @@ +package nu.marginalia.wmsa.edge.converting.loader; + +import com.google.inject.Inject; + +public class LoaderFactory { + private final SqlLoadUrls sqlLoadUrls; + private final SqlLoadDomains sqlLoadDomains; + private final SqlLoadDomainLinks sqlLoadDomainLinks; + private final SqlLoadProcessedDomain sqlLoadProcessedDomain; + private final SqlLoadProcessedDocument sqlLoadProcessedDocument; + private final IndexLoadKeywords indexLoadKeywords; + + @Inject + public LoaderFactory(SqlLoadUrls sqlLoadUrls, + SqlLoadDomains sqlLoadDomains, + SqlLoadDomainLinks sqlLoadDomainLinks, + SqlLoadProcessedDomain sqlLoadProcessedDomain, + SqlLoadProcessedDocument sqlLoadProcessedDocument, + IndexLoadKeywords indexLoadKeywords) { + + this.sqlLoadUrls = sqlLoadUrls; + this.sqlLoadDomains = sqlLoadDomains; + this.sqlLoadDomainLinks = sqlLoadDomainLinks; + this.sqlLoadProcessedDomain = sqlLoadProcessedDomain; + this.sqlLoadProcessedDocument = sqlLoadProcessedDocument; + this.indexLoadKeywords = indexLoadKeywords; + } + + public Loader create(int sizeHint) { + return new Loader(sizeHint, sqlLoadUrls, sqlLoadDomains, sqlLoadDomainLinks, sqlLoadProcessedDomain, sqlLoadProcessedDocument, indexLoadKeywords); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadDomainLinks.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadDomainLinks.java new file mode 100644 index 00000000..e0978828 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadDomainLinks.java @@ -0,0 +1,69 @@ +package nu.marginalia.wmsa.edge.converting.loader; + +import com.google.inject.Inject; +import com.zaxxer.hikari.HikariDataSource; +import nu.marginalia.wmsa.edge.converting.interpreter.instruction.DomainLink; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.SQLException; + +import static java.sql.Statement.SUCCESS_NO_INFO; + +public class SqlLoadDomainLinks { + + private final HikariDataSource dataSource; + private static final Logger logger = LoggerFactory.getLogger(SqlLoadDomainLinks.class); + + @Inject + public SqlLoadDomainLinks(HikariDataSource dataSource) { + this.dataSource = dataSource; + try (var conn = dataSource.getConnection()) { + try (var stmt = conn.createStatement()) { + stmt.execute("DROP PROCEDURE IF EXISTS INSERT_LINK"); + stmt.execute(""" + CREATE PROCEDURE INSERT_LINK ( + IN FROM_DOMAIN VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + IN TO_DOMAIN VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + ) + BEGIN + INSERT IGNORE INTO EC_DOMAIN_LINK (SOURCE_DOMAIN_ID, DEST_DOMAIN_ID) + SELECT SOURCE.ID,DEST.ID + FROM EC_DOMAIN SOURCE INNER JOIN EC_DOMAIN DEST + ON SOURCE.URL_PART=FROM_DOMAIN AND DEST.URL_PART=TO_DOMAIN; + END + """); + } + } + catch (SQLException ex) { + throw new RuntimeException("Failed to set up loader", ex); + } + } + + public void load(DomainLink[] links) { + + try (var connection = dataSource.getConnection(); + var stmt = + connection.prepareCall("CALL INSERT_LINK(?,?)")) + { + + for (DomainLink link : links) { + stmt.setString(1, link.from().toString()); + stmt.setString(2, link.to().toString()); + + stmt.addBatch(); + } + + var ret = stmt.executeBatch(); + for (int rv = 0; rv < links.length; rv++) { + if (ret[rv] != 1 && ret[rv] != SUCCESS_NO_INFO) { + logger.warn("load({}) -- bad row count {}", links[rv], ret[rv]); + } + } + } + catch (SQLException sql) { + sql.printStackTrace(); + } + + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadDomains.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadDomains.java new file mode 100644 index 00000000..18cc40bd --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadDomains.java @@ -0,0 +1,124 @@ +package nu.marginalia.wmsa.edge.converting.loader; + +import com.google.inject.Inject; +import com.zaxxer.hikari.HikariDataSource; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.SQLException; + +import static java.sql.Statement.SUCCESS_NO_INFO; + +public class SqlLoadDomains { + + private final HikariDataSource dataSource; + private static final Logger logger = LoggerFactory.getLogger(SqlLoadDomains.class); + + @Inject + public SqlLoadDomains(HikariDataSource dataSource) { + this.dataSource = dataSource; + try (var conn = dataSource.getConnection()) { + try (var stmt = conn.createStatement()) { + stmt.execute("DROP PROCEDURE IF EXISTS INSERT_DOMAIN"); + stmt.execute(""" + CREATE PROCEDURE INSERT_DOMAIN ( + IN DOMAIN_NAME VARCHAR(255), + IN SUB_DOMAIN VARCHAR(255), + IN TOP_DOMAIN VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci) + BEGIN + INSERT IGNORE INTO EC_TOP_DOMAIN (URL_PART) VALUES (TOP_DOMAIN); + + INSERT IGNORE INTO EC_DOMAIN(URL_PART, URL_SUBDOMAIN, URL_TOP_DOMAIN_ID) + SELECT DOMAIN_NAME,SUB_DOMAIN,ID + FROM EC_TOP_DOMAIN + WHERE EC_TOP_DOMAIN.URL_PART=TOP_DOMAIN; + END + """); + } + } + catch (SQLException ex) { + throw new RuntimeException("Failed to set up loader", ex); + } + } + + public void load(LoaderData data, EdgeDomain domain) { + + try (var connection = dataSource.getConnection()) { + try (var insertCall = connection.prepareCall("CALL INSERT_DOMAIN(?,?,?)")) { + insertCall.setString(1, domain.toString()); + insertCall.setString(2, domain.subDomain); + insertCall.setString(3, domain.domain); + insertCall.addBatch(); + + var ret = insertCall.executeUpdate(); + if (ret < 0) { + logger.warn("load({}) -- bad row count {}", domain, ret); + } + + connection.commit(); + findIdForTargetDomain(connection, data); + } + } + catch (SQLException ex) { + ex.printStackTrace(); + } + + + } + + public void load(LoaderData data, EdgeDomain[] domains) { + + try (var connection = dataSource.getConnection()) { + connection.setAutoCommit(false); + + try (var insertCall = connection.prepareCall("CALL INSERT_DOMAIN(?,?,?)")) { + + for (var domain : domains) { + insertCall.setString(1, domain.toString()); + insertCall.setString(2, domain.subDomain); + insertCall.setString(3, domain.domain); + insertCall.addBatch(); + } + var ret = insertCall.executeBatch(); + + for (int rv = 0; rv < domains.length; rv++) { + if (ret[rv] < 0 && ret[rv] != SUCCESS_NO_INFO) { + logger.warn("load({}) -- bad row count {}", domains[rv], ret[rv]); + } + } + + } + connection.commit(); + connection.setAutoCommit(true); + findIdForTargetDomain(connection, data); + } + catch (SQLException ex) { + ex.printStackTrace(); + } + } + + void findIdForTargetDomain(Connection connection, LoaderData data) { + if (data.getTargetDomain() == null || data.getDomainId(data.getTargetDomain()) > 0) { + return; + } + + try (var query = connection.prepareStatement("SELECT ID FROM EC_DOMAIN WHERE URL_PART=?")) + { + + var targetDomain = data.getTargetDomain(); + query.setString(1, targetDomain.toString()); + var rsp = query.executeQuery(); + if (rsp.next()) { + data.addDomain(targetDomain, rsp.getInt(1)); + } + else { + logger.warn("load() -- could not find ID for target domain {}", targetDomain); + } + } + catch (SQLException ex) { + ex.printStackTrace(); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadProcessedDocument.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadProcessedDocument.java new file mode 100644 index 00000000..5bc48caa --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadProcessedDocument.java @@ -0,0 +1,126 @@ +package nu.marginalia.wmsa.edge.converting.loader; + +import com.google.inject.Inject; +import com.zaxxer.hikari.HikariDataSource; +import nu.marginalia.wmsa.edge.converting.interpreter.instruction.LoadProcessedDocument; +import nu.marginalia.wmsa.edge.converting.interpreter.instruction.LoadProcessedDocumentWithError; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; + +import static java.sql.Statement.SUCCESS_NO_INFO; + +public class SqlLoadProcessedDocument { + private final HikariDataSource dataSource; + private static final Logger logger = LoggerFactory.getLogger(SqlLoadProcessedDocument.class); + + @Inject + public SqlLoadProcessedDocument(HikariDataSource dataSource) { + this.dataSource = dataSource; + + try (var conn = dataSource.getConnection()) { + try (var stmt = conn.createStatement()) { + stmt.execute("DROP PROCEDURE IF EXISTS INSERT_PAGE_VISIT"); + stmt.execute("DROP PROCEDURE IF EXISTS INSERT_PAGE_VISIT_BAD"); + stmt.execute(""" + CREATE PROCEDURE INSERT_PAGE_VISIT ( + IN URL_ID INT, + IN STATE VARCHAR(32), + IN TITLE VARCHAR(255), + IN DESCRIPTION VARCHAR(255), + IN LENGTH INT, + IN QUALITY_MEASURE DOUBLE, + IN FEATURES INT, + IN STANDARD VARCHAR(32), + IN HASH INT) + BEGIN + SET FOREIGN_KEY_CHECKS=0; + REPLACE INTO EC_PAGE_DATA(ID, TITLE, DESCRIPTION, WORDS_TOTAL, FORMAT, FEATURES) VALUES (URL_ID, TITLE, DESCRIPTION, LENGTH, STANDARD, FEATURES); + UPDATE EC_URL SET VISITED=1, STATE=STATE, QUALITY_MEASURE=QUALITY_MEASURE, DATA_HASH=HASH WHERE ID=URL_ID; + SET FOREIGN_KEY_CHECKS=1; + END + """); + stmt.execute(""" + CREATE PROCEDURE INSERT_PAGE_VISIT_BAD ( + IN URL_ID INT, + IN STATE VARCHAR(32)) + BEGIN + UPDATE EC_URL SET VISITED=1, STATE=STATE, QUALITY_MEASURE=-100, DATA_HASH=NULL WHERE ID=URL_ID; + END + """); + + } + } + catch (SQLException ex) { + throw new RuntimeException("Failed to set up loader", ex); + } + } + + public void load(LoaderData data, List documents) { + try (var conn = dataSource.getConnection(); + var stmt = conn.prepareCall("CALL INSERT_PAGE_VISIT(?, ?, ?, ?, ?, ?, ?, ?, ?)")) { + + for (var doc : documents) { + int urlId = data.getUrlId(doc.url()); + if (urlId < 0) { + logger.warn("Failed to resolve ID for URL {}", doc.url()); + return; + } + + stmt.setInt(1, urlId); + stmt.setString(2, doc.state().name()); + stmt.setString(3, doc.title()); + stmt.setString(4, doc.description()); + stmt.setInt(5, doc.length()); + stmt.setDouble(6, doc.quality()); + stmt.setInt(7, doc.htmlFeatures()); + stmt.setString(8, doc.standard().name()); + stmt.setInt(9, (int) doc.hash()); + stmt.addBatch(); + } + var ret = stmt.executeBatch(); + + for (int rv = 0; rv < documents.size(); rv++) { + if (ret[rv] < 1 && ret[rv] != SUCCESS_NO_INFO) { + logger.warn("load({}) -- bad row count {}", documents.get(rv), ret[rv]); + } + } + + conn.commit(); + } catch (SQLException e) { + e.printStackTrace(); + } + + + } + + public void loadWithError(LoaderData data, List documents) { + try (var conn = dataSource.getConnection(); + var stmt = conn.prepareCall("CALL INSERT_PAGE_VISIT_BAD(?, ?)")) { + + for (var doc : documents) { + int urlId = data.getUrlId(doc.url()); + if (urlId < 0) { + logger.warn("Failed to resolve ID for URL {}", doc.url()); + return; + } + + stmt.setInt(1, urlId); + stmt.setString(2, doc.state().name()); + stmt.addBatch(); + } + var ret = stmt.executeBatch(); + for (int rv = 0; rv < documents.size(); rv++) { + if (ret[rv] < 0 && ret[rv] != SUCCESS_NO_INFO) { + logger.warn("load({}) -- bad row count {}", documents.get(rv), ret[rv]); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadProcessedDomain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadProcessedDomain.java new file mode 100644 index 00000000..64607b3a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadProcessedDomain.java @@ -0,0 +1,87 @@ +package nu.marginalia.wmsa.edge.converting.loader; + +import com.google.inject.Inject; +import com.zaxxer.hikari.HikariDataSource; +import nu.marginalia.wmsa.edge.converting.interpreter.instruction.DomainLink; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.SQLException; + +public class SqlLoadProcessedDomain { + private final HikariDataSource dataSource; + private final SqlLoadDomains loadDomains; + private static final Logger logger = LoggerFactory.getLogger(SqlLoadProcessedDomain.class); + @Inject + public SqlLoadProcessedDomain(HikariDataSource dataSource, SqlLoadDomains loadDomains) { + this.dataSource = dataSource; + this.loadDomains = loadDomains; + + + try (var conn = dataSource.getConnection()) { + try (var stmt = conn.createStatement()) { + stmt.execute("DROP PROCEDURE IF EXISTS INITIALIZE_DOMAIN"); + stmt.execute(""" + CREATE PROCEDURE INITIALIZE_DOMAIN ( + IN ST INT, + IN IDX INT, + IN QUAL DOUBLE, + IN DID INT) + BEGIN + UPDATE EC_DOMAIN SET INDEX_DATE=NOW(), STATE=ST, DOMAIN_ALIAS=NULL, INDEXED=GREATEST(INDEXED,IDX), QUALITY=QUAL, QUALITY_RAW=QUAL, QUALITY_ORIGINAL=QUAL WHERE ID=DID; + DELETE FROM EC_DOMAIN_LINK WHERE SOURCE_DOMAIN_ID=DID; + END + """); + } + } + catch (SQLException ex) { + throw new RuntimeException("Failed to set up loader", ex); + } + } + + public void load(LoaderData data, EdgeDomain domain, EdgeDomainIndexingState state, double quality) { + data.setTargetDomain(domain); + + loadDomains.load(data, domain); + + try (var conn = dataSource.getConnection(); + var initCall = conn.prepareCall("CALL INITIALIZE_DOMAIN(?,?,?,?)")) + { + initCall.setInt(1, state.code); + initCall.setInt(2, 1 + data.sizeHint / 100); + initCall.setDouble(3, quality); + initCall.setInt(4, data.getDomainId(domain)); + int rc = initCall.executeUpdate(); + if (rc < 1) { + logger.warn("load({},{},{}) -- bad rowcount {}", domain, state, quality, rc); + } + conn.commit(); + } + catch (SQLException ex) { + ex.printStackTrace(); + } + + } + + public void loadAlias(LoaderData data, DomainLink link) { + try (var conn = dataSource.getConnection(); + var stmt = conn.prepareStatement(""" + UPDATE EC_DOMAIN TARGET + INNER JOIN EC_DOMAIN ALIAS ON ALIAS.URL_PART=? + SET TARGET.DOMAIN_ALIAS=ALIAS.ID + WHERE TARGET.URL_PART=? + """)) { + stmt.setString(1, link.to().toString()); + stmt.setString(2, link.from().toString()); + int rc = stmt.executeUpdate(); + if (rc != 1) { + logger.warn("loadAlias({}) - unexpected row count {}", link, rc); + } + } + catch (SQLException ex) { + ex.printStackTrace(); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadUrls.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadUrls.java new file mode 100644 index 00000000..7d8851ca --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/loader/SqlLoadUrls.java @@ -0,0 +1,92 @@ +package nu.marginalia.wmsa.edge.converting.loader; + +import com.google.inject.Inject; +import com.zaxxer.hikari.HikariDataSource; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.SQLException; +import java.sql.Types; + +import static java.sql.Statement.SUCCESS_NO_INFO; + +public class SqlLoadUrls { + + private final HikariDataSource dataSource; + private static final Logger logger = LoggerFactory.getLogger(SqlLoadUrls.class); + + @Inject + public SqlLoadUrls(HikariDataSource dataSource) { + this.dataSource = dataSource; + try (var conn = dataSource.getConnection()) { + try (var stmt = conn.createStatement()) { + stmt.execute("DROP PROCEDURE IF EXISTS INSERT_URL"); + stmt.execute(""" + CREATE PROCEDURE INSERT_URL ( + IN PROTO VARCHAR(255), + IN DOMAIN_NAME VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + IN PORT INT, + IN URL VARCHAR(255) + ) + BEGIN + INSERT IGNORE INTO EC_URL (PROTO,DOMAIN_ID,PORT,URL) SELECT PROTO,ID,PORT,URL FROM EC_DOMAIN WHERE URL_PART=DOMAIN_NAME; + END + """); + } + } + catch (SQLException ex) { + throw new RuntimeException("Failed to set up loader", ex); + } + } + + public void load(LoaderData data, EdgeUrl[] urls) { + try (var conn = dataSource.getConnection(); + var insertCall = conn.prepareCall("CALL INSERT_URL(?,?,?,?)"); + var queryCall = conn.prepareStatement("SELECT ID, PROTO, URL FROM EC_URL WHERE DOMAIN_ID=?") + ) + { + conn.setAutoCommit(false); + for (var url : urls) { + + insertCall.setString(1, url.proto); + insertCall.setString(2, url.domain.toString()); + if (url.port != null) { + insertCall.setInt(3, url.port); + } + else { + insertCall.setNull(3, Types.INTEGER); + } + insertCall.setString(4, url.path); + insertCall.addBatch(); + } + var ret = insertCall.executeBatch(); + for (int rv = 0; rv < urls.length; rv++) { + if (ret[rv] < 0 && ret[rv] != SUCCESS_NO_INFO) { + logger.warn("load({}) -- bad row count {}", urls[rv], ret[rv]); + } + } + + conn.commit(); + conn.setAutoCommit(true); + + + var targetDomain = data.getTargetDomain(); + queryCall.setInt(1, data.getDomainId(targetDomain)); + + var rsp = queryCall.executeQuery(); + + while (rsp.next()) { + int urlId = rsp.getInt(1); + String proto = rsp.getString(2); + String path = rsp.getString(3); + + data.addUrl(new EdgeUrl(proto, targetDomain, null, path), urlId); + } + + } + catch (SQLException ex) { + ex.printStackTrace(); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/model/DisqualifiedException.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/model/DisqualifiedException.java new file mode 100644 index 00000000..1c785371 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/model/DisqualifiedException.java @@ -0,0 +1,17 @@ +package nu.marginalia.wmsa.edge.converting.model; + +public class DisqualifiedException extends Exception { + public final DisqualificationReason reason; + + public DisqualifiedException(DisqualificationReason reason) { + this.reason = reason; + } + @Override + public Throwable fillInStackTrace() { + return this; + } + + public enum DisqualificationReason { + LENGTH, CONTENT_TYPE, LANGUAGE, STATUS, QUALITY + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/model/ProcessedDocument.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/model/ProcessedDocument.java new file mode 100644 index 00000000..e73b6a8f --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/model/ProcessedDocument.java @@ -0,0 +1,25 @@ +package nu.marginalia.wmsa.edge.converting.model; + +import lombok.ToString; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageWordSet; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlState; + +import java.util.OptionalDouble; + +@ToString +public class ProcessedDocument { + public EdgeUrl url; + + public ProcessedDocumentDetails details; + public EdgePageWordSet words; + + public EdgeUrlState state; + + public OptionalDouble quality() { + if (details != null) { + return OptionalDouble.of(details.quality); + } + return OptionalDouble.empty(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/model/ProcessedDocumentDetails.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/model/ProcessedDocumentDetails.java new file mode 100644 index 00000000..f21e86ae --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/model/ProcessedDocumentDetails.java @@ -0,0 +1,26 @@ +package nu.marginalia.wmsa.edge.converting.model; + +import lombok.ToString; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlFeature; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import nu.marginalia.wmsa.edge.model.crawl.EdgeHtmlStandard; + +import java.util.List; +import java.util.Set; + +@ToString +public class ProcessedDocumentDetails { + public String title; + public String description; + + public int length; + public double quality; + public long hashCode; + + public Set features; + public EdgeHtmlStandard standard; + + public List linksInternal; + public List linksExternal; + public List feedLinks; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/model/ProcessedDomain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/model/ProcessedDomain.java new file mode 100644 index 00000000..101d1fb8 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/model/ProcessedDomain.java @@ -0,0 +1,34 @@ +package nu.marginalia.wmsa.edge.converting.model; + +import lombok.ToString; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; + +import java.util.List; +import java.util.Optional; +import java.util.OptionalDouble; + +@ToString +public class ProcessedDomain { + public EdgeDomain domain; + + public List documents; + public EdgeDomainIndexingState state; + public EdgeDomain redirect; + public String ip; + + public OptionalDouble averageQuality() { + if (documents == null) { + return OptionalDouble.empty(); + } + return documents.stream() + .map(ProcessedDocument::quality) + .filter(OptionalDouble::isPresent) + .mapToDouble(OptionalDouble::getAsDouble) + .average(); + } + + public int size() { + return Optional.ofNullable(documents).map(List::size).orElse(1); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/DocumentProcessor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/DocumentProcessor.java new file mode 100644 index 00000000..2d7cda5e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/DocumentProcessor.java @@ -0,0 +1,245 @@ +package nu.marginalia.wmsa.edge.converting.processor; + +import com.google.common.hash.HashCode; +import com.google.inject.Inject; +import com.google.inject.name.Named; +import nu.marginalia.wmsa.edge.converting.model.DisqualifiedException; +import nu.marginalia.wmsa.edge.converting.model.DisqualifiedException.DisqualificationReason; +import nu.marginalia.wmsa.edge.converting.model.ProcessedDocument; +import nu.marginalia.wmsa.edge.converting.model.ProcessedDocumentDetails; +import nu.marginalia.wmsa.edge.converting.processor.logic.*; +import nu.marginalia.wmsa.edge.crawler.domain.FeedExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.LinkParser; +import nu.marginalia.wmsa.edge.crawler.domain.language.LanguageFilter; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.DocumentKeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentLanguageData; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlFeature; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlStandardExtractor; +import nu.marginalia.wmsa.edge.crawling.model.CrawledDocument; +import nu.marginalia.wmsa.edge.crawling.model.CrawledDomain; +import nu.marginalia.wmsa.edge.crawling.model.CrawlerDocumentStatus; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import nu.marginalia.wmsa.edge.model.crawl.EdgeHtmlStandard; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageWordSet; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlState; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URISyntaxException; +import java.util.*; + +import static nu.marginalia.wmsa.edge.model.crawl.EdgeHtmlStandard.UNKNOWN; + +public class DocumentProcessor { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final int minDocumentLength; + private final double minDocumentQuality; + + private static final Set acceptedContentTypes = Set.of("application/xhtml+xml", "application/xhtml", "text/html"); + + private final SentenceExtractor sentenceExtractor; + private final FeatureExtractor featureExtractor; + private final TitleExtractor titleExtractor; + private final DocumentKeywordExtractor keywordExtractor; + private final SummaryExtractor summaryExtractor; + + private static final DocumentValuator documentValuator = new DocumentValuator(); + private static final LanguageFilter languageFilter = new LanguageFilter(); + private static final LinkParser linkParser = new LinkParser(); + private static final FeedExtractor feedExtractor = new FeedExtractor(linkParser); + + @Inject + public DocumentProcessor(@Named("min-document-length") Integer minDocumentLength, + @Named("min-document-quality") Double minDocumentQuality, + SentenceExtractor sentenceExtractor, + FeatureExtractor featureExtractor, + TitleExtractor titleExtractor, + DocumentKeywordExtractor keywordExtractor, + SummaryExtractor summaryExtractor) + { + this.minDocumentLength = minDocumentLength; + this.minDocumentQuality = minDocumentQuality; + this.sentenceExtractor = sentenceExtractor; + this.featureExtractor = featureExtractor; + this.titleExtractor = titleExtractor; + this.keywordExtractor = keywordExtractor; + this.summaryExtractor = summaryExtractor; + } + + public ProcessedDocument process(CrawledDocument crawledDocument, CrawledDomain crawledDomain) { + ProcessedDocument ret = new ProcessedDocument(); + + try { + ret.url = new EdgeUrl(crawledDocument.url); + ret.state = crawlerStatusToUrlState(crawledDocument.crawlerStatus, crawledDocument.httpStatus); + + if (ret.state == EdgeUrlState.OK && isAcceptedContentType(crawledDocument)) { + var detailsWords = createDetails(crawledDomain, crawledDocument); + + if (detailsWords.details().quality < minDocumentQuality) { + throw new DisqualifiedException(DisqualificationReason.QUALITY); + } + + ret.details = detailsWords.details(); + ret.words = detailsWords.words(); + } + else { + throw new DisqualifiedException(DisqualificationReason.STATUS); + } + } + catch (DisqualifiedException ex) { + ret.state = EdgeUrlState.DISQUALIFIED; + logger.debug("Disqualified {}: {}", ret.url, ex.reason); + } + catch (Exception ex) { + ret.state = EdgeUrlState.DISQUALIFIED; + logger.info("Failed to convert " + ret.url, ex); + ex.printStackTrace(); + } + + return ret; + } + + private boolean isAcceptedContentType(CrawledDocument crawledDocument) { + return crawledDocument.contentType != null && acceptedContentTypes.contains(crawledDocument.contentType.toLowerCase()); + } + + private EdgeUrlState crawlerStatusToUrlState(String crawlerStatus, int httpStatus) { + return switch (CrawlerDocumentStatus.valueOf(crawlerStatus)) { + case OK -> httpStatus < 300 ? EdgeUrlState.OK : EdgeUrlState.DEAD; + case REDIRECT -> EdgeUrlState.REDIRECT; + default -> EdgeUrlState.DEAD; + }; + } + + private DetailsWithWords createDetails(CrawledDomain crawledDomain, CrawledDocument crawledDocument) + throws DisqualifiedException, URISyntaxException { + + var doc = Jsoup.parse(crawledDocument.documentBody); + var dld = sentenceExtractor.extractSentences(doc.clone()); + + checkDocumentLanguage(dld); + + var ret = new ProcessedDocumentDetails(); + + ret.description = getDescription(doc); + ret.length = getLength(doc); + ret.standard = getHtmlStandard(doc); + ret.title = titleExtractor.getTitleAbbreviated(doc, dld, crawledDocument.url); + ret.features = featureExtractor.getFeatures(crawledDomain, doc); + ret.quality = documentValuator.getQuality(ret.standard, doc, dld); + ret.hashCode = HashCode.fromString(crawledDocument.documentBodyHash).asLong(); + + var words = getWords(dld); + + var url = new EdgeUrl(crawledDocument.url); + addMetaWords(ret, url, crawledDomain, words); + + getLinks(url, ret, doc, words); + + return new DetailsWithWords(ret, words); + } + + private void addMetaWords(ProcessedDocumentDetails ret, EdgeUrl url, CrawledDomain domain, EdgePageWordSet words) { + List tagWords = new ArrayList<>(); + + var edgeDomain = url.domain; + tagWords.add("format:"+ret.standard.toString().toLowerCase()); + + + tagWords.add("site:" + edgeDomain.toString().toLowerCase()); + if (!Objects.equals(edgeDomain.toString(), edgeDomain.domain)) { + tagWords.add("site:" + edgeDomain.domain.toLowerCase()); + } + + tagWords.add("proto:"+url.proto.toLowerCase()); + tagWords.add("js:" + Boolean.toString(ret.features.contains(HtmlFeature.JS)).toLowerCase()); + + if (ret.features.contains(HtmlFeature.MEDIA)) { + tagWords.add("special:media"); + } + if (ret.features.contains(HtmlFeature.TRACKING)) { + tagWords.add("special:tracking"); + } + if (ret.features.contains(HtmlFeature.AFFILIATE_LINK)) { + tagWords.add("special:affiliate"); + } + if (ret.features.contains(HtmlFeature.COOKIES)) { + tagWords.add("special:cookies"); + } + + words.append(IndexBlock.Meta, tagWords); + words.append(IndexBlock.Words, tagWords); + } + + private void getLinks(EdgeUrl baseUrl, ProcessedDocumentDetails ret, Document doc, EdgePageWordSet words) { + var links = doc.getElementsByTag("a"); + var frames = doc.getElementsByTag("frame"); + var feeds = doc.select("link[rel=alternate]"); + + LinkProcessor lp = new LinkProcessor(ret, baseUrl); + + for (var atag : links) { + linkParser.parseLink(baseUrl, atag).ifPresent(lp::accept); + } + for (var frame : frames) { + linkParser.parseFrame(baseUrl, frame).ifPresent(lp::accept); + } + + for (var link : feeds) { + feedExtractor + .getFeedFromAlternateTag(baseUrl, link) + .ifPresent(lp::acceptFeed); + } + + Set linkTerms = new HashSet<>(); + + for (var domain : lp.getForeignDomains()) { + linkTerms.add("links:"+domain.toString().toLowerCase()); + linkTerms.add("links:"+domain.getDomain().toLowerCase()); + } + + words.append(IndexBlock.Meta, linkTerms); + + } + + private void checkDocumentLanguage(DocumentLanguageData dld) throws DisqualifiedException { + if (dld.totalNumWords() < minDocumentLength) { + throw new DisqualifiedException(DisqualificationReason.LENGTH); + } + + double languageAgreement = languageFilter.dictionaryAgreement(dld); + if (languageAgreement < 0.1) { + throw new DisqualifiedException(DisqualificationReason.LANGUAGE); + } + } + + private EdgeHtmlStandard getHtmlStandard(Document doc) { + EdgeHtmlStandard htmlStandard = HtmlStandardExtractor.parseDocType(doc.documentType()); + + if (UNKNOWN.equals(htmlStandard)) { + return HtmlStandardExtractor.sniffHtmlStandard(doc); + } + return htmlStandard; + } + + private EdgePageWordSet getWords(DocumentLanguageData dld) { + return keywordExtractor.extractKeywords(dld); + } + + private String getDescription(Document doc) { + return summaryExtractor.extractSummary(doc).orElse(""); + } + + private int getLength(Document doc) { + return doc.text().length(); + } + + private record DetailsWithWords(ProcessedDocumentDetails details, EdgePageWordSet words) {}; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/DomainProcessor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/DomainProcessor.java new file mode 100644 index 00000000..4343b0c3 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/DomainProcessor.java @@ -0,0 +1,59 @@ +package nu.marginalia.wmsa.edge.converting.processor; + +import com.google.inject.Inject; +import nu.marginalia.wmsa.edge.converting.model.ProcessedDomain; +import nu.marginalia.wmsa.edge.crawling.model.CrawledDomain; +import nu.marginalia.wmsa.edge.crawling.model.CrawlerDomainStatus; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; + +import java.util.ArrayList; +import java.util.Collections; + +public class DomainProcessor { + private final DocumentProcessor documentProcessor; + + @Inject + public DomainProcessor(DocumentProcessor documentProcessor) { + this.documentProcessor = documentProcessor; + } + + public ProcessedDomain process(CrawledDomain crawledDomain) { + var ret = new ProcessedDomain(); + + ret.domain = new EdgeDomain(crawledDomain.domain); + ret.ip = crawledDomain.ip; + + if (crawledDomain.redirectDomain != null) { + ret.redirect = new EdgeDomain(crawledDomain.redirectDomain); + } + + if (crawledDomain.doc != null) { + ret.documents = new ArrayList<>(crawledDomain.doc.size()); + + for (var doc : crawledDomain.doc) { + var processedDoc = documentProcessor.process(doc, crawledDomain); + if (processedDoc.url != null) { + ret.documents.add(processedDoc); + } + } + + } + else { + ret.documents = Collections.emptyList(); + } + + ret.state = getState(crawledDomain.crawlerStatus); + + return ret; + } + + private EdgeDomainIndexingState getState(String crawlerStatus) { + return switch (CrawlerDomainStatus.valueOf(crawlerStatus)) { + case OK -> EdgeDomainIndexingState.ACTIVE; + case REDIRECT -> EdgeDomainIndexingState.REDIR; + case BLOCKED -> EdgeDomainIndexingState.BLOCKED; + default -> EdgeDomainIndexingState.ERROR; + }; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/InstructionsCompiler.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/InstructionsCompiler.java new file mode 100644 index 00000000..13be9939 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/InstructionsCompiler.java @@ -0,0 +1,116 @@ +package nu.marginalia.wmsa.edge.converting.processor; + +import nu.marginalia.wmsa.edge.converting.interpreter.Instruction; +import nu.marginalia.wmsa.edge.converting.interpreter.instruction.*; +import nu.marginalia.wmsa.edge.converting.model.ProcessedDocument; +import nu.marginalia.wmsa.edge.converting.model.ProcessedDomain; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlFeature; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +import java.util.*; + +public class InstructionsCompiler { + + public List compile(ProcessedDomain domain) { + List ret = new ArrayList<>(domain.size()*4); + + ret.add(new LoadProcessedDomain(domain.domain, domain.state, domain.averageQuality().orElse(-5.))); + + if (domain.documents != null) { + compileUrls(ret, domain.documents); + compileDocuments(ret, domain.documents); + compileFeeds(ret, domain.documents); + + compileLinks(ret, domain.domain, domain.documents); + } + if (domain.redirect != null) { + compileRedirect(ret, domain.domain, domain.redirect); + + } + + return ret; + } + + private void compileRedirect(List ret, EdgeDomain from, EdgeDomain to) { + ret.add(new LoadDomain(to)); + ret.add(new LoadDomainLink(new DomainLink(from, to))); + ret.add(new LoadDomainRedirect(new DomainLink(from, to))); + } + + private void compileUrls(List ret, List documents) { + Set seenUrls = new HashSet<>(documents.size()*4); + Set seenDomains = new HashSet<>(documents.size()); + + documents.stream().map(doc -> doc.url).forEach(seenUrls::add); + + for (var doc : documents) { + if (doc.details == null) continue; + for (var url : doc.details.linksExternal) { + seenDomains.add(url.domain); + } + seenUrls.addAll(doc.details.linksExternal); + seenUrls.addAll(doc.details.linksInternal); + } + + ret.add(new LoadDomain(seenDomains.toArray(EdgeDomain[]::new))); + ret.add(new LoadUrl(seenUrls.toArray(EdgeUrl[]::new))); + } + + private void compileLinks(List ret, EdgeDomain from, List documents) { + DomainLink[] links = documents.stream().map(doc -> doc.details) + .filter(Objects::nonNull) + .flatMap(dets -> dets.linksExternal.stream()) + .map(link -> link.domain) + .distinct() + .map(domain -> new DomainLink(from, domain)) + .toArray(DomainLink[]::new); + + ret.add(new LoadDomainLink(links)); + } + + private void compileFeeds(List ret, List documents) { + + EdgeUrl[] feeds = documents.stream().map(doc -> doc.details) + .filter(Objects::nonNull) + .flatMap(dets -> dets.feedLinks.stream()) + .distinct() + .toArray(EdgeUrl[]::new); + + ret.add(new LoadRssFeed(feeds)); + } + + private void compileDocuments(List ret, List documents) { + + for (var doc : documents) { + compileDocumentDetails(ret, doc); + } + + for (var doc : documents) { + compileWords(ret, doc); + } + + } + + private void compileDocumentDetails(List ret, ProcessedDocument doc) { + var details = doc.details; + + if (details != null) { + ret.add(new LoadProcessedDocument(doc.url, doc.state, details.title, details.description, HtmlFeature.encode(details.features), details.standard, details.length, details.hashCode, details.quality)); + } + else { + ret.add(new LoadProcessedDocumentWithError(doc.url, doc.state)); + } + } + + private void compileWords(List ret, ProcessedDocument doc) { + var words = doc.words; + if (words != null) { + var wordsArray = words.values().stream() + .map(DocumentKeywords::new) + .toArray(DocumentKeywords[]::new); + + ret.add(new LoadKeywords(doc.url, wordsArray)); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/DocumentValuator.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/DocumentValuator.java new file mode 100644 index 00000000..bd49b6f8 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/DocumentValuator.java @@ -0,0 +1,70 @@ +package nu.marginalia.wmsa.edge.converting.processor.logic; + +import crawlercommons.utils.Strings; +import nu.marginalia.wmsa.edge.converting.model.DisqualifiedException; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentLanguageData; +import nu.marginalia.wmsa.edge.model.crawl.EdgeHtmlStandard; +import org.jsoup.nodes.Document; + +import java.util.Set; + +import static nu.marginalia.wmsa.edge.converting.model.DisqualifiedException.DisqualificationReason.LENGTH; + +public class DocumentValuator { + + private static final Set filthTable = Set.of( + "xxx", "sex", "anal", "sexy", + "bdsm", "fetish", "porn", "camgirls", "dildo", + "gangbang", "buttplug", "orgasm", "vibrator", + "cameltoe", "download", "iso", "botox", "torrent", + "jackpot", "vegas", "casino", "coinbase", "poloniex", + "myetherwallet", "ethereum", "binance", "bitcoin", + "litecoin", "seo", "serp" + + ); + + public double getQuality(EdgeHtmlStandard htmlStandard, Document doc, DocumentLanguageData dld) throws DisqualifiedException { + double smutCoefficient = dld.streamLowerCase().filter(filthTable::contains).count(); + double scriptPenalty = getScriptPenalty(doc); + + + int textBodyLength = doc.text().length(); + int rawLength = doc.html().length(); + + if (textBodyLength == 0) { + throw new DisqualifiedException(LENGTH); + } + + return Math.log(textBodyLength / (double) rawLength)*htmlStandard.scale + + htmlStandard.offset + - scriptPenalty + - smutCoefficient; + } + + + private int getScriptPenalty(Document parsed) { + var scriptTags = parsed.getElementsByTag("script"); + String scriptText = scriptTags.html(); + int badScript = 0; + if (scriptText.contains(".createElement(")) { + badScript = 1; + } + + double scriptPenalty = 0; + for (var tag : scriptTags) { + String srcTag = tag.attr("src"); + if (Strings.isBlank(srcTag)) { + scriptPenalty += 1; + } + else if (srcTag.contains("wp-content") || srcTag.contains("wp-includes") || srcTag.contains("jquery")) { + scriptPenalty += 0.49; + } + else { + scriptPenalty += 1; + } + + } + return (int)(scriptPenalty + badScript + (scriptText.length())/1000.); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/FeatureExtractor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/FeatureExtractor.java new file mode 100644 index 00000000..e790d9ec --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/FeatureExtractor.java @@ -0,0 +1,70 @@ +package nu.marginalia.wmsa.edge.converting.processor.logic; + +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlFeature; +import nu.marginalia.wmsa.edge.crawling.model.CrawledDomain; +import org.jsoup.nodes.Document; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class FeatureExtractor { + + private static final List trackers = List.of("adform.net", + "connect.facebook", + "googletagmanager.com", + "googlesyndication.com", + "google.com", + "twitter.com", + "smartadserver.com", + "doubleclick.com", + "2mdn.com", + "dmtry.com", + "bing.com", + "msn.com", + "amazon-adsystem.com", + "alexametrics.com", + "rubiconproject.com", + "chango.com", + "d5nxst8fruw4z.cloudfront.net", + "d31qbv1cthcecs.cloudfront.net", + "linkedin.com"); + + public Set getFeatures(CrawledDomain domain, Document doc) { + Set features = new HashSet<>(); + + var scriptTags = doc.getElementsByTag("script"); + + if (scriptTags.size() > 0) { + features.add(HtmlFeature.JS); + } + + if (!doc.getElementsByTag("object").isEmpty() + || !doc.getElementsByTag("audio").isEmpty() + || !doc.getElementsByTag("video").isEmpty()) { + features.add(HtmlFeature.MEDIA); + } + + if (scriptTags.stream() + .anyMatch(tag -> trackers.stream().anyMatch(tracker -> tag.attr("src").contains(tracker)))) { + features.add(HtmlFeature.TRACKING); + } + + if (scriptTags.html().contains("google-analytics.com")) { + features.add(HtmlFeature.TRACKING); + } + + if (doc.getElementsByTag("a").stream().map(e -> e.attr("href")) + .map(String::toLowerCase) + .anyMatch(href -> + href.contains("amzn.to/") || href.contains("amazon.com/"))) { + features.add(HtmlFeature.AFFILIATE_LINK); + } + + if (!domain.cookies.isEmpty()) { + features.add(HtmlFeature.COOKIES); + } + + return features; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/LinkProcessor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/LinkProcessor.java new file mode 100644 index 00000000..75daa00b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/LinkProcessor.java @@ -0,0 +1,90 @@ +package nu.marginalia.wmsa.edge.converting.processor.logic; + +import nu.marginalia.wmsa.edge.converting.model.ProcessedDocumentDetails; +import nu.marginalia.wmsa.edge.crawler.worker.UrlBlocklist; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +public class LinkProcessor { + private final ProcessedDocumentDetails ret; + private final EdgeUrl baseUrl; + private final Set seenUrls = new HashSet<>(); + private final Set foreignDomains = new HashSet<>(); + + private static final int MAX_INTERNAL_LINK = 250; + private static final int MAX_EXTERNAL_LINK = 100; + private static final UrlBlocklist urlBlocklist = new UrlBlocklist(); + + public LinkProcessor(ProcessedDocumentDetails documentDetails, EdgeUrl baseUrl) { + this.ret = documentDetails; + this.baseUrl = baseUrl; + + ret.linksExternal = new ArrayList<>(); + ret.linksInternal = new ArrayList<>(); + ret.feedLinks = new ArrayList<>(); + } + + public Set getForeignDomains() { + return foreignDomains; + } + + public void accept(EdgeUrl link) { + if (!isLinkPermitted(link)) { + return; + } + + if (!seenUrls.add(link)) { + return; + } + + if (Objects.equals(link.domain, baseUrl.domain)) { // internal link + if (ret.linksInternal.size() < MAX_INTERNAL_LINK) { + ret.linksInternal.add(link); + } + } + else { + if (ret.linksExternal.size() < MAX_EXTERNAL_LINK) { + ret.linksExternal.add(link); + foreignDomains.add(link.domain); + } + } + } + + public void acceptFeed(EdgeUrl link) { + if (!isLinkPermitted(link)) { + return; + } + + if (!seenUrls.add(link)) { + return; + } + + ret.feedLinks.add(link); + } + + private boolean isLinkPermitted(EdgeUrl link) { + if (!isProtoSupported(link.proto)) { + return false; + } + + if (urlBlocklist.isForumLink(link)) { + return false; + } + + if (urlBlocklist.isUrlBlocked(link)) { + return false; + } + + return true; + } + + private boolean isProtoSupported(String proto) { + return proto.equalsIgnoreCase("http") + || proto.equalsIgnoreCase("https"); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/SummaryExtractor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/SummaryExtractor.java new file mode 100644 index 00000000..438c0cfa --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/SummaryExtractor.java @@ -0,0 +1,62 @@ +package nu.marginalia.wmsa.edge.converting.processor.logic; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import org.apache.commons.lang3.StringUtils; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +import java.util.Optional; +import java.util.regex.Pattern; + +public class SummaryExtractor { + private final int maxSummaryLength; + + private final Pattern truncatedCharacters = Pattern.compile("[^a-zA-Z0-9.,!?\\-'\"]+"); + + @Inject + public SummaryExtractor(@Named("max-summary-length") Integer maxSummaryLength) { + this.maxSummaryLength = maxSummaryLength; + } + + public Optional extractSummary(Document parsed) { + var cleanDoc = parsed.clone(); + cleanDoc.select("h1,h2,h3,header,nav,#header,#nav,#navigation,.header,.nav,.navigation,ul,li").remove(); + + return extractSummaryRaw(cleanDoc) + .map(String::trim) + .filter(s -> !s.isBlank() && s.length() > 20) + .or(() -> getOgDescription(parsed)) + .or(() -> getMetaDescription(parsed)) + .map(this::trimLongSpaces) + .map(s -> StringUtils.abbreviate(s, "", maxSummaryLength)) + ; + } + + private String trimLongSpaces(String s) { + return truncatedCharacters.matcher(s).replaceAll(" "); + } + + private Optional extractSummaryRaw(Document parsed) { + StringBuilder content = new StringBuilder(); + + parsed.select("p,div,section,article").stream() + .takeWhile(e -> content.length() <= maxSummaryLength) + .filter(elem -> elem.text().length() > elem.html().length()/2) + .map(Element::text) + .forEach(content::append); + + if (content.length() > 10) { + return Optional.of(content.toString()); + } + return Optional.empty(); + } + + private Optional getMetaDescription(Document parsed) { + return Optional.of(parsed.select("meta[name=description]").attr("content")).filter(s -> !s.isBlank()); + } + + private Optional getOgDescription(Document parsed) { + return Optional.of(parsed.select("meta[name=og:description]").attr("content")).filter(s -> !s.isBlank()); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/TitleExtractor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/TitleExtractor.java new file mode 100644 index 00000000..33b89c03 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/converting/processor/logic/TitleExtractor.java @@ -0,0 +1,57 @@ +package nu.marginalia.wmsa.edge.converting.processor.logic; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentLanguageData; +import org.apache.commons.lang3.StringUtils; +import org.jsoup.nodes.Document; + +public class TitleExtractor { + + private final int maxTitleLength; + + @Inject + public TitleExtractor(@Named("max-title-length") Integer maxTitleLength) { + this.maxTitleLength = maxTitleLength; + + } + + public String getTitleAbbreviated(Document doc, DocumentLanguageData dld, String url) { + return StringUtils.abbreviate(getFullTitle(doc, dld, url), maxTitleLength); + } + public String getFullTitle(Document doc, DocumentLanguageData dld, String url) { + String title; + + title = getFirstTagText(doc, "head > title"); + if (title != null) return title; + + title = getFirstTagText(doc, "h1"); + if (title != null) return title; + + title = getFirstTagText(doc, "h2"); + if (title != null) return title; + + title = getFirstTagText(doc, "h3"); + if (title != null) return title; + + title = getFirstTagText(doc, "h4"); + if (title != null) return title; + + title = getFirstTagText(doc, "h5"); + if (title != null) return title; + + if (dld.sentences.length > 0) { + return dld.sentences[0].originalSentence; + } + + return url; + } + + private String getFirstTagText(Document doc, String selector) { + var firstTag = doc.selectFirst(selector); + if (firstTag != null) { + return firstTag.text(); + } + return null; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/CrawlJobsSpecificationSet.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/CrawlJobsSpecificationSet.java new file mode 100644 index 00000000..dfac5bc2 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/CrawlJobsSpecificationSet.java @@ -0,0 +1,52 @@ +package nu.marginalia.wmsa.edge.crawler; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.crawler.worker.data.CrawlJobsSpecification; +import org.apache.commons.lang3.StringUtils; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +public class CrawlJobsSpecificationSet { + private final List specs = new ArrayList<>(); + + @SneakyThrows + @Inject + public CrawlJobsSpecificationSet(@Named("crawl-specifications-path") Path specsFile) { + Files.readAllLines(specsFile) + .stream() + .map(this::stripComments) + .filter(StringUtils::isNotBlank) + .flatMap(this::generateSpecsFromLine) + .map(CrawlJobsSpecification::new) + .forEach(specs::add); + } + + private Stream generateSpecsFromLine(String s) { + String[] parts = s.split("\\s"); + if (parts.length == 1) { + return Stream.of(Integer.parseInt(s)); + } + else { + int times = Integer.parseInt(parts[0]); + int config = Integer.parseInt(parts[1]); + return Stream.generate(() -> config).limit(times); + } + } + + private String stripComments(String s) { + return s.replaceAll("#.*", ""); + } + + CrawlJobsSpecification get(int i) { + return specs.get(i); + } + int size() { + return specs.size(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/EdgeCrawlerMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/EdgeCrawlerMain.java new file mode 100644 index 00000000..36eabc5f --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/EdgeCrawlerMain.java @@ -0,0 +1,35 @@ +package nu.marginalia.wmsa.edge.crawler; + +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import nu.marginalia.wmsa.configuration.MainClass; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.module.ConfigurationModule; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.configuration.server.Initialization; + +import java.io.IOException; + +public class EdgeCrawlerMain extends MainClass { + private EdgeCrawlerService service; + + @Inject + public EdgeCrawlerMain(EdgeCrawlerService service) throws IOException { + this.service = service; + } + + public static void main(String... args) { + init(ServiceDescriptor.EDGE_CRAWLER, args); + + Injector injector = Guice.createInjector( + new EdgeCrawlerModule(), + new ConfigurationModule(), + new DatabaseModule() + ); + + injector.getInstance(EdgeCrawlerMain.class); + injector.getInstance(Initialization.class).setReady(); + + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/EdgeCrawlerModule.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/EdgeCrawlerModule.java new file mode 100644 index 00000000..13e171c6 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/EdgeCrawlerModule.java @@ -0,0 +1,52 @@ +package nu.marginalia.wmsa.edge.crawler; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.name.Names; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import opennlp.tools.langdetect.LanguageDetector; +import opennlp.tools.langdetect.LanguageDetectorME; +import opennlp.tools.langdetect.LanguageDetectorModel; + +import java.io.FileNotFoundException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Optional; + +public class EdgeCrawlerModule extends AbstractModule { + + + public void configure() { + bind(String.class).annotatedWith(Names.named("user-agent")).toInstance("search.marginalia.nu"); + bind(String.class).annotatedWith(Names.named("user-agent-robots")).toInstance("search.marginalia.nu"); + bind(Path.class).annotatedWith(Names.named("crawl-specifications-path")).toInstance(Path.of("/var/lib/wmsa/crawler-specs.dat")); + + bind(LanguageModels.class).toInstance(new LanguageModels( + Path.of("/var/lib/wmsa/model/ngrams-generous-emstr.bin"), + Path.of("/var/lib/wmsa/model/tfreq-new-algo3.bin"), + Path.of("/var/lib/wmsa/model/opennlp-sentence.bin"), + Path.of("/var/lib/wmsa/model/English.RDR"), + Path.of("/var/lib/wmsa/model/English.DICT"), + Path.of("/var/lib/wmsa/model/opennlp-tok.bin") + )); + } + + @Provides @SneakyThrows + LanguageDetector detector() { + final Path[] paths = new Path[]{ + Path.of("/app/resources/langdetect-183.bin"), + Path.of("/home/vlofgren/Code/wmsa-b/src/main/nlp-models/langdetect-183.bin") + }; + Optional path = Arrays.stream(paths).filter(p->p.toFile().exists()).findAny(); + if (path.isEmpty()) { + throw new FileNotFoundException("Could not find langdetect-183.bin"); + } + + try (var is = Files.newInputStream(path.get())) { + return new LanguageDetectorME(new LanguageDetectorModel(is)); + } + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/EdgeCrawlerService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/EdgeCrawlerService.java new file mode 100644 index 00000000..dc25088c --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/EdgeCrawlerService.java @@ -0,0 +1,107 @@ +package nu.marginalia.wmsa.edge.crawler; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import io.reactivex.rxjava3.schedulers.Schedulers; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.configuration.server.MetricsServer; +import nu.marginalia.wmsa.configuration.server.Service; +import nu.marginalia.wmsa.data_store.client.DataStoreClient; +import nu.marginalia.wmsa.edge.crawler.worker.UploaderWorker; +import nu.marginalia.wmsa.edge.crawler.worker.Worker; +import nu.marginalia.wmsa.edge.crawler.worker.WorkerFactory; +import nu.marginalia.wmsa.edge.crawler.worker.data.CrawlJobsSpecification; +import nu.marginalia.wmsa.edge.crawler.worker.results.WorkerResults; +import nu.marginalia.wmsa.edge.director.client.EdgeDirectorClient; +import nu.marginalia.wmsa.edge.index.client.EdgeIndexClient; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; + +public class EdgeCrawlerService extends Service { + private final EdgeIndexClient indexClient; + private final EdgeDirectorClient directorClient; + private final Initialization init; + private final WorkerFactory workerFactory; + private final CrawlJobsSpecificationSet specifications; + private final DataStoreClient dataStoreClient; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private UploaderWorker uploader; + private final List crawlers = new ArrayList<>(); + + @Inject + public EdgeCrawlerService(@Named("service-host") String ip, + @Named("service-port") Integer port, + DataStoreClient dataStoreClient, + EdgeIndexClient indexClient, + EdgeDirectorClient directorClient, + Initialization init, + WorkerFactory workerFactory, + CrawlJobsSpecificationSet specifications, + Initialization initialization, + MetricsServer metricsServer + ) { + super(ip, port, initialization, metricsServer); + this.dataStoreClient = dataStoreClient; + this.indexClient = indexClient; + this.directorClient = directorClient; + this.init = init; + this.workerFactory = workerFactory; + this.specifications = specifications; + + Schedulers.newThread().scheduleDirect(this::run); + + } + + @SneakyThrows + private void run() { + init.waitReady(); + indexClient.waitReady(); + directorClient.waitReady(); + dataStoreClient.waitReady(); + + directorClient.flushOngoingJobs(Context.internal()); + + dataStoreClient.putUrl(Context.internal(), 0, new EdgeUrl("https://memex.marginalia.nu/")).blockingSubscribe(); + dataStoreClient.putUrl(Context.internal(), 0, new EdgeUrl("http://www.cs.uni.edu/")).blockingSubscribe(); + dataStoreClient.putUrl(Context.internal(), 0, new EdgeUrl("https://www.leonardcohenfiles.com/")).blockingSubscribe(); + dataStoreClient.putUrl(Context.internal(), 0, new EdgeUrl("http://atsf.railfan.net/")).blockingSubscribe(); + dataStoreClient.putUrl(Context.internal(), 0, new EdgeUrl("http://sprott.physics.wisc.edu/")).blockingSubscribe(); + dataStoreClient.putUrl(Context.internal(), 0, new EdgeUrl("http://www.attalus.org/")).blockingSubscribe(); + dataStoreClient.putUrl(Context.internal(), 0, new EdgeUrl("http://www.attalus.org/")).blockingSubscribe(); + + final List> queues = new ArrayList<>(specifications.size()); + + for (int i = 0; i < specifications.size(); i++) { + queues.add(new LinkedBlockingQueue<>(1)); + } + + for (int i = 0; i < specifications.size(); i++) { + var spec = specifications.get(i); + var queue = queues.get(i); + + Worker worker; + if (spec.pass == 0) { + worker = workerFactory.buildDiscoverWorker(queue); + } + else { + worker = workerFactory.buildIndexWorker(queue, spec.pass); + } + + crawlers.add(worker); + + new Thread(worker, "Fetcher-"+i).start(); + } + + uploader = workerFactory.buildUploader(queues); + new Thread(uploader, "Uploader").start(); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/RssScraperMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/RssScraperMain.java new file mode 100644 index 00000000..5018063b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/RssScraperMain.java @@ -0,0 +1,31 @@ +package nu.marginalia.wmsa.edge.crawler; + + +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.edge.crawler.domain.DomainCrawlerRobotsTxt; +import nu.marginalia.wmsa.edge.crawler.domain.RssCrawler; +import nu.marginalia.wmsa.edge.crawler.fetcher.HttpFetcher; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDaoImpl; +import nu.marginalia.wmsa.edge.index.client.EdgeIndexClient; +import org.mariadb.jdbc.Driver; + +import java.io.IOException; + +public class RssScraperMain { + + public static void main(String... args) throws IOException { +// Driver driver = new Driver(); +// +// var conn = new DatabaseModule().provideConnection(); +// var fetcher = new HttpFetcher("search.marginalia.nu"); +// var indexClient = new EdgeIndexClient(); +// indexClient.waitReady(); +// +// new RssCrawler(conn, +// new DomainCrawlerRobotsTxt(fetcher, "search.marginalia.nu"), +// new EdgeDataStoreDaoImpl(conn), +// fetcher, +// keywordExtractor, sentenceExtractor, indexClient).run(); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlResults.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlResults.java new file mode 100644 index 00000000..b9b51abf --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlResults.java @@ -0,0 +1,55 @@ +package nu.marginalia.wmsa.edge.crawler.domain; + +import lombok.AllArgsConstructor; +import lombok.ToString; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainLink; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageContent; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlState; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlVisit; + +import java.util.*; +import java.util.stream.Collectors; + +@AllArgsConstructor @ToString +public class DomainCrawlResults { + public final EdgeDomain domain; + public final double rank; + public final int pass; + + public final long crawlStart = System.currentTimeMillis(); + + public final Set extUrl = new HashSet<>(); + public final Set intUrl = new HashSet<>(); + public final Set visitedUrl = new HashSet<>(); + public final Set feeds = new HashSet<>(); + public final Map urlStates = new HashMap<>(); + + public final Map pageContents = new HashMap<>(); + public final HashSet links = new HashSet<>(); + + public final List visits() { + return visitedUrl.stream().map(url -> { + var page = pageContents.get(url); + if (page != null) { + return new EdgeUrlVisit(url, + page.hash, + page.getMetadata().quality(), + page.metadata.title.replaceAll("[^\\x20-\\x7E\\xA0-\\xFF]", ""), + page.metadata.description.replaceAll("[^\\x20-\\x7E\\xA0-\\xFF]", ""), + page.ipAddress, + page.metadata.htmlStandard.toString(), + page.metadata.features, + page.metadata.textDistinctWords, + page.metadata.totalWords, + urlStates.getOrDefault(url, EdgeUrlState.OK) + ); + } + else { + return new EdgeUrlVisit(url, null, null, null, null,null, "text", 0, 0, 0, + urlStates.getOrDefault(url, EdgeUrlState.OK)); + } + }).collect(Collectors.toList()); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawler.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawler.java new file mode 100644 index 00000000..2f92519b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawler.java @@ -0,0 +1,509 @@ +package nu.marginalia.wmsa.edge.crawler.domain; + +import crawlercommons.robots.SimpleRobotRules; +import gnu.trove.set.hash.TIntHashSet; +import io.reactivex.rxjava3.core.Observable; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.edge.archive.client.ArchiveClient; +import nu.marginalia.wmsa.edge.crawler.domain.language.LanguageFilter; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlProcessor; +import nu.marginalia.wmsa.edge.crawler.domain.processor.PlainTextProcessor; +import nu.marginalia.wmsa.edge.crawler.fetcher.HttpFetcher; +import nu.marginalia.wmsa.edge.crawler.worker.IpBlockList; +import nu.marginalia.wmsa.edge.crawler.worker.UrlBlocklist; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.*; +import org.apache.logging.log4j.util.Strings; +import org.apache.logging.log4j.util.Supplier; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +public class DomainCrawler { + private final HttpFetcher fetcher; + private final EdgeDomain indexDomain; + private int maxDepth; + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final double EXT_LINK_SCORE_THRESHOLD = -15; + public final static int MIN_WORDS_PER_DOCUMENT = 100; + public final static int DEFAULT_CRAWL_DELAY_MS = 1000; + + private final LinkedList queue = new LinkedList<>(); + private final Set visited = new HashSet<>(); + private final TIntHashSet visitedHash = new TIntHashSet(); + + private static final LinkParser linkParser = new LinkParser(); + private static final FeedExtractor feedExtractor = new FeedExtractor(linkParser); + + + private static final UrlsCache URLS_CACHE = new UrlsCache<>(); + private final int pass; + private final int maxExtLinks; + private final int maxIntLinks; + + private final IpBlockList ipBlockList; + + private final PlainTextProcessor plainTextProcessor; + private final HtmlProcessor htmlProcessor; + private final ArchiveClient archiveClient; + private final DomainCrawlerRobotsTxt domainRobotsTxtFetcher; + private final LanguageFilter languageFilter; + private static final UrlBlocklist urlBlocklist = new UrlBlocklist(); + private final double rank; + + public EdgeDomain domain() { + return indexDomain; + } + + public DomainCrawler(HttpFetcher fetcher, + PlainTextProcessor plainTextProcessor, + HtmlProcessor htmlProcessor, + ArchiveClient archiveClient, + DomainCrawlerRobotsTxt domainRobotsTxtFetcher, + LanguageFilter languageFilter, + EdgeIndexTask ingress, + IpBlockList ipBlockList) { + this.fetcher = fetcher; + this.plainTextProcessor = plainTextProcessor; + this.htmlProcessor = htmlProcessor; + this.archiveClient = archiveClient; + this.domainRobotsTxtFetcher = domainRobotsTxtFetcher; + this.languageFilter = languageFilter; + this.ipBlockList = ipBlockList; + this.indexDomain = ingress.domain; + + this.pass = ingress.pass; + this.rank = ingress.rank; + this.maxExtLinks = ingress.limit * 50; + this.maxIntLinks = 100 + ingress.limit * 5; + + if (ingress.pass == 0) { + this.maxDepth = 25; + } + else { + this.maxDepth = 100; + } + + ingress.streamUrls().forEach(queue::add); + visitedHash.addAll(ingress.visited); + } + + + public DomainCrawlResults crawlToExhaustion(int maxCount, Supplier continueSignal) { + maxDepth = maxCount; + + var robotsTxtRules = domainRobotsTxtFetcher.fetchRulesCached(indexDomain); + final DomainCrawlResults results = new DomainCrawlResults(indexDomain, rank, pass); + + fetcher.clearCookies(); + + crawlDelay(0, robotsTxtRules); + + int count = 0; + while (!queue.isEmpty() && count < maxCount) { + if (!continueSignal.get()) break; + + if (crawlNextUrl(results, robotsTxtRules) != 0) + count++; + } + + addLinkWords(results); + + results.feeds.removeIf(url -> isDeadUrl(robotsTxtRules, url)); + results.intUrl.removeAll(results.visitedUrl); + + return results; + } + + public DomainCrawlResults crawl() { + var robotsTxtRules = domainRobotsTxtFetcher.fetchRulesCached(indexDomain); + final DomainCrawlResults results = new DomainCrawlResults(indexDomain, rank, pass); + + fetcher.clearCookies(); + + double scoreRemaining = 2*maxDepth; + + Comparator comparator = Comparator.comparing(u -> + results.pageContents.values().stream().filter(contents -> contents.hasHotLink(u)).count()); + Comparator comparator2 = comparator.thenComparing(EdgeUrl::depth); + + crawlDelay(0, robotsTxtRules); + + while (!queue.isEmpty() && scoreRemaining > 0) { + queue.sort(comparator2); + scoreRemaining += crawlNextUrl(results, robotsTxtRules); + } + + addLinkWords(results); + + results.feeds.removeIf(url -> isDeadUrl(robotsTxtRules, url)); + results.intUrl.removeAll(results.visitedUrl); + + return results; + } + + private double crawlNextUrl(DomainCrawlResults results, SimpleRobotRules robotsTxtRules) { + EdgeUrl url = queue.removeFirst(); + + if (visitedHash.contains(url.hashCode())) { + return 0.; + } + results.visitedUrl.add(url); + + if (!robotsTxtRules.isAllowed(url.toString()) || + urlBlocklist.isUrlBlocked(url) || + isUrlTooLong(url)) { + results.urlStates.put(url, EdgeUrlState.DISQUALIFIED); + return 0.; + } + + if (!visited.add(url)) + { + return 0.; + } + + logger.debug("{}", url); + + return fetchUrl(robotsTxtRules, results, url); + } + + private void addLinkWords(DomainCrawlResults results) { + + results.pageContents.values().forEach(page -> { + page.linkWords.forEach((url,words) -> { + if (words.isEmpty()) return; + + var dest = results.pageContents.get(url); + if (dest != null) { + logger.debug("Amending title words {} -> {}", url, words); + var namesWords = dest.words.get(IndexBlock.Link); + namesWords.words.forEach(words::remove); + dest.words.append(IndexBlock.Link, words); + } + }); + }); + + } + + private boolean isDeadUrl(SimpleRobotRules robotsTxtRules, EdgeUrl edgeUrl) { + try { + if (!robotsTxtRules.isAllowed(edgeUrl.toString())) { + return false; + } + + long tStart = System.currentTimeMillis(); + fetcher.fetchContent(edgeUrl); + + crawlDelay(System.currentTimeMillis() - tStart, robotsTxtRules); + + return true; + } + catch (Exception ex) { + return false; + } + } + + private boolean isUrlTooLong(EdgeUrl url) { + return url.path.length() > 255; + } + + private boolean isEquivalentUrl(EdgeUrl a, EdgeUrl b) { + if ((a == null) != (b == null)) + return false; + if (a == b) + return true; + if (!Objects.equals(a.domain, b.domain)) { + return false; + } + if (!Objects.equals(a.path, b.path)) { + return false; + } + return true; + } + + private double fetchUrl(SimpleRobotRules rules, DomainCrawlResults results, EdgeUrl url) { + try { + var page = fetcher.fetchContent(url); + + if (!isEquivalentUrl(page.redirectUrl, page.url)) { + handleLink(results, page.redirectUrl, -1); + results.urlStates.put(url, EdgeUrlState.REDIRECT); + + logger.debug("Redirect {} -> {}", url, page.redirectUrl); + + return -1; + } + final long startTime = System.currentTimeMillis(); + final long stopTime; + + var contents = parseContent(results, page); + if (contents.isPresent()) { + var content = contents.get(); + + results.pageContents.put(content.url, content); + + archiveClient.submitPage(Context.internal(), url, page); + } + else { + results.urlStates.put(url, EdgeUrlState.DISQUALIFIED); + } + + stopTime = System.currentTimeMillis(); + crawlDelay(stopTime - startTime, rules); + + return contents.map(c -> c.getMetadata().quality()).orElse(-5.); + } + catch (HttpFetcher.BadContentType ex) { + results.urlStates.put(url, EdgeUrlState.DISQUALIFIED); + + logger.debug("Bad content type {}", ex.getMessage()); + return -.1; + } + catch (Exception ex) { + results.urlStates.put(url, EdgeUrlState.DEAD); + + if (logger.isDebugEnabled()) { + ex.printStackTrace(); + logger.debug("Failed to crawl url {} : {} - {}", url, ex.getClass().getSimpleName(), ex.getMessage()); + } + return -.5; + } + + } + + private Optional parseContent(DomainCrawlResults results, EdgeRawPageContents content) { + var contentType = content.contentType.getContentType(); + + if (content.data.length() < 500) { + return Optional.empty(); + } + switch (contentType) { + case "application/xhtml+xml": + case "application/xhtml": + case "text/html": return parseHtmlContent(results, content); + case "text/plain": return plainTextProcessor.parsePlainText(content); + default: { + logger.debug("Skipping contentType {}", content.contentType); + return Optional.empty(); + } + } + } + + private Optional parseHtmlContent(DomainCrawlResults results, EdgeRawPageContents rawContents) { + + var parsed = parseRawContents(rawContents.data); + + var langTagIsInteresting + = languageFilter.isPageInterestingByHtmlTag(parsed) + .or(() -> languageFilter.isPageInterestingByMetaLanguage(parsed)); + + if (langTagIsInteresting.isPresent() && !langTagIsInteresting.get()) { + logger.debug("Rejected due to language tag"); + return Optional.empty(); + } + + var canonicalUrl = Optional.ofNullable(parsed.select("meta[rel=canonical]")) + .map(tag -> tag.attr("href")) + .filter(Strings::isNotBlank) + .flatMap(url -> linkParser.parseLink(rawContents.url, url)) + .filter(url -> !url.equals(rawContents.url)); + + if (canonicalUrl.isPresent()) { + logger.debug("Noncanonical {} -> {}", rawContents.url, canonicalUrl.get()); + handleLink(results, canonicalUrl.get(), -1); + return Optional.empty(); + } + + var processedPageContents = htmlProcessor.processHtmlPage(rawContents, parsed); + if (processedPageContents == null) { + logger.debug("Empty Processed Data for {}", rawContents.url); + return Optional.empty(); + } + + extractLinks(results, rawContents, parsed, processedPageContents); + + if (processedPageContents.numWords() < MIN_WORDS_PER_DOCUMENT) { + logger.debug("Rejected because too few words {} - {}", rawContents.url, processedPageContents.numWords()); + return Optional.empty(); + } + if (processedPageContents.metadata.title.startsWith("Index of")) { + return Optional.empty(); + } + + return Optional.of(processedPageContents); + } + + private Document parseRawContents(String data) { + return Jsoup.parse(data); + } + + private void extractLinks(DomainCrawlResults results, EdgeRawPageContents rawContents, Document parsed, EdgePageContent processedPageContents) { + var links = parsed.getElementsByTag("a"); + + Observable.fromStream(links.stream()) + .mapOptional(elem -> linkParser.parseLink(rawContents.url, elem)) + .blockingForEach(link -> handleLink(results, link, processedPageContents.metadata.quality())); + + var frames = parsed.getElementsByTag("frame"); + + Observable.fromStream(frames.stream()) + .mapOptional(elem -> linkParser.parseFrame(rawContents.url, elem)) + .blockingForEach(link -> handleLink(results, link, processedPageContents.metadata.quality())); + + parsed.select("link[rel=alternate]").forEach(alternateTag -> + feedExtractor + .getFeedFromAlternateTag(rawContents.url, alternateTag) + .ifPresent(results.feeds::add) + + ); + } + + private void handleLink(DomainCrawlResults results, EdgeUrl linkUrl, double pageScore) { + if (!isProtoSupported(linkUrl.proto)) { + return; + } + + if (!linkUrl.domain.equals(indexDomain)) { + handleExternalLink(results, pageScore, linkUrl); + } + else if (!isLinkVisited(linkUrl) + && !urlBlocklist.isForumLink(linkUrl) + && !urlBlocklist.isUrlBlocked(linkUrl) + ) { + enqueueUrl(linkUrl); + + if (results.intUrl.size() < maxIntLinks) { + results.intUrl.add(linkUrl); + } + } + } + + private boolean isProtoSupported(String proto) { + return switch (proto.toLowerCase()) { + case "http", "https" -> true; + default -> false; + }; + } + + private void handleExternalLink(DomainCrawlResults results, + double pageScore, + EdgeUrl linkUrl) { + if (pageScore <= EXT_LINK_SCORE_THRESHOLD) + return; + if (results.extUrl.size() > maxExtLinks) + return; + if (isBlacklistedDomain(linkUrl.domain)) + return; + if (urlBlocklist.isForumLink(linkUrl)) { + return; + } + if (urlBlocklist.isUrlBlocked(linkUrl)) { + return; + } + + if (URLS_CACHE.add(linkUrl)) { + results.extUrl.add(linkUrl); + } + results.links.add(new EdgeDomainLink(indexDomain, linkUrl.domain)); + } + + + private boolean isBlacklistedDomain(EdgeDomain domain) { + if ((isRestrictedTLD(domain.getAddress())) + && !("".equals(domain.subDomain) || "www".equals(domain.subDomain))) + return true; + + if (!ipBlockList.isAllowed(domain)) { + return true; + } + + if (isBlacklistedSubdomain(domain.subDomain)) + return true; + + return false; + } + + private boolean isRestrictedTLD(String domain) { + if (domain.contains("blog")) { + return true; + } + + if (domain.endsWith(".se")) + return false; + if (domain.endsWith(".nu")) + return false; + if (domain.endsWith(".uk")) + return false; + if (domain.endsWith(".jp")) + return false; + if (domain.endsWith(".com")) + return false; + if (domain.endsWith(".net")) + return false; + if (domain.endsWith(".org")) + return false; + if (domain.endsWith(".edu")) + return false; + + return true; + } + + private boolean isBlacklistedSubdomain(String subDomain) { + if (subDomain.equals("git")) { + return true; + } + else if (subDomain.contains("mirror")) { + return true; + } + else if (subDomain.equals("docs")) { + return true; + } + else if (subDomain.equals("mail")) { + return true; + } + else if (subDomain.contains("list")) { + return true; + } + else if (subDomain.startsWith("ftp")) { + return true; + } + + return false; + } + + private boolean isLinkVisited(EdgeUrl linkUrl) { + return visited.contains(linkUrl) + || visitedHash.contains(linkUrl.hashCode()); + } + + private void enqueueUrl(EdgeUrl url) { + if (visited.size() + queue.size() > maxDepth) { + return; + } + queue.addLast(url); + } + + @SneakyThrows + public static void crawlDelay(long timeParsed, SimpleRobotRules rules) { + var delay = rules.getCrawlDelay(); + if (delay >= 1) { + if (timeParsed*1000 > delay) + return; + + Thread.sleep(Math.min(1000*delay-timeParsed, 5000)); + } + else { + if (timeParsed > DEFAULT_CRAWL_DELAY_MS) + return; + + Thread.sleep(DEFAULT_CRAWL_DELAY_MS - timeParsed); + } + } + + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerFactory.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerFactory.java new file mode 100644 index 00000000..bd856d28 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerFactory.java @@ -0,0 +1,40 @@ +package nu.marginalia.wmsa.edge.crawler.domain; + +import com.google.inject.Inject; +import nu.marginalia.wmsa.edge.archive.client.ArchiveClient; +import nu.marginalia.wmsa.edge.crawler.domain.language.LanguageFilter; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlProcessor; +import nu.marginalia.wmsa.edge.crawler.domain.processor.PlainTextProcessor; +import nu.marginalia.wmsa.edge.crawler.fetcher.HttpFetcher; +import nu.marginalia.wmsa.edge.crawler.worker.IpBlockList; +import nu.marginalia.wmsa.edge.model.crawl.EdgeIndexTask; + +public class DomainCrawlerFactory { + private final HttpFetcher fetcher; + private final HtmlProcessor htmlProcessor; + private final ArchiveClient archiveClient; + private DomainCrawlerRobotsTxt domainCrawlerRobotsTxt; + private LanguageFilter languageFilter; + private final IpBlockList blockList; + private final PlainTextProcessor plainTextProcessor; + + @Inject + public DomainCrawlerFactory(HttpFetcher fetcher, + HtmlProcessor htmlProcessor, + PlainTextProcessor plainTextProcessor, ArchiveClient archiveClient, + DomainCrawlerRobotsTxt domainCrawlerRobotsTxt, + LanguageFilter languageFilter, + IpBlockList blockList) { + this.fetcher = fetcher; + this.htmlProcessor = htmlProcessor; + this.plainTextProcessor = plainTextProcessor; + this.archiveClient = archiveClient; + this.domainCrawlerRobotsTxt = domainCrawlerRobotsTxt; + this.languageFilter = languageFilter; + this.blockList = blockList; + } + + public DomainCrawler domainCrawler(EdgeIndexTask indexTask) { + return new DomainCrawler(fetcher, plainTextProcessor, htmlProcessor, archiveClient, domainCrawlerRobotsTxt, languageFilter, indexTask, blockList); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerRobotsTxt.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerRobotsTxt.java new file mode 100644 index 00000000..e8c152ec --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerRobotsTxt.java @@ -0,0 +1,67 @@ +package nu.marginalia.wmsa.edge.crawler.domain; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.inject.name.Named; +import crawlercommons.robots.SimpleRobotRules; +import crawlercommons.robots.SimpleRobotRulesParser; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.crawler.fetcher.HttpFetcher; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.crawl.EdgeRawPageContents; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +@Singleton +public class DomainCrawlerRobotsTxt { + + private static final SimpleRobotRulesParser parser = new SimpleRobotRulesParser(); + + private final Cache urlIdCache = CacheBuilder.newBuilder().maximumSize(1000).build(); + + private final String userAgent; + private final HttpFetcher fetcher; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Inject + public DomainCrawlerRobotsTxt(HttpFetcher fetcher, + @Named("user-agent-robots") String userAgent) { + this.userAgent = userAgent; + this.fetcher = fetcher; + } + + @SneakyThrows + public SimpleRobotRules fetchRulesCached(EdgeDomain domain) { + return urlIdCache.get(domain, () -> fetchRulesRaw(domain)); + } + + private SimpleRobotRules fetchRulesRaw(EdgeDomain domain) { + return fetchRobotsForProto("https", domain) + .or(() -> fetchRobotsForProto("http", domain)) + .orElseGet(() -> new SimpleRobotRules(SimpleRobotRules.RobotRulesMode.ALLOW_ALL)); + } + + private Optional fetchRobotsForProto(String proto, EdgeDomain domain) { + try { + var url = new EdgeUrl(proto, domain, null, "/robots.txt"); + return Optional.of(parseRobotsTxt(fetcher.fetchContent(url))); + } + catch (Exception ex) { + return Optional.empty(); + } + } + + private SimpleRobotRules parseRobotsTxt(EdgeRawPageContents edgePageContent) { + return parser.parseContent(edgePageContent.url.toString(), + edgePageContent.data.getBytes(StandardCharsets.UTF_8), + edgePageContent.contentType.contentType, + userAgent); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/FeedExtractor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/FeedExtractor.java new file mode 100644 index 00000000..c256786e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/FeedExtractor.java @@ -0,0 +1,55 @@ +package nu.marginalia.wmsa.edge.crawler.domain; + +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.jsoup.nodes.Element; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; +import java.util.Optional; + +public class FeedExtractor { + private final LinkParser linkParser; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + public FeedExtractor(LinkParser linkParser) { + this.linkParser = linkParser; + } + + public Optional getFeedFromAlternateTag(EdgeUrl crawlUrl, Element alternateTag) { + var type = alternateTag.attr("type"); + if (type == null) { + return Optional.empty(); + } + + try { + var url = linkParser.parseLink(crawlUrl, alternateTag.attr("href")); + + if (url.isEmpty()) + return Optional.empty(); + + if (!Objects.equals(crawlUrl.domain, url.get().domain)) + return Optional.empty(); + + if ("application/atom+xml".equalsIgnoreCase(type)) { + return url; + } + + if ("application/rss+xml".equalsIgnoreCase(type)) { + return url; + } + + if ("application/rdf+xml".equalsIgnoreCase(type)) { + return url; + } + + + } + catch (Exception ex) { + logger.debug("Bad URI syntax", ex); + } + return Optional.empty(); + } + + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/LinkParser.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/LinkParser.java new file mode 100644 index 00000000..f29fb658 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/LinkParser.java @@ -0,0 +1,166 @@ +package nu.marginalia.wmsa.edge.crawler.domain; + +import com.google.common.base.CharMatcher; +import io.reactivex.rxjava3.core.Maybe; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.jetbrains.annotations.Contract; +import org.jsoup.nodes.Element; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +public class LinkParser { + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final List blockPrefixList = List.of( + "mailto:", "javascript:", "tel:", "itpc:", "#", "file:"); + private final List blockSuffixList = List.of( + ".pdf", ".mp3", ".wmv", ".avi", ".zip", ".7z", + ".bin", ".exe", ".tar.gz", ".tar.bz2", ".xml", ".swf", + ".wav", ".ogg", ".jpg", ".jpeg", ".png", ".gif", ".webp", + ".webm", ".bmp", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", + ".gz", ".asc", ".md5", ".asf", ".mov", ".sig", ".pub", ".iso"); + + @Contract(pure=true) + public Optional parseLink(EdgeUrl baseUrl, Element l) { + return Optional.of(l) + .filter(this::shouldIndexLink) + .map(this::getUrl) + .map(link -> resolveUrl(baseUrl, link)) + .flatMap(this::createURI) + .map(URI::normalize) + .map(this::renormalize) + .flatMap(this::createEdgeUrl); + } + + private Optional createURI(String s) { + try { + return Optional.of(new URI(s)); + } + catch (URISyntaxException e) { + logger.debug("Bad URI {}", s); + return Optional.empty(); + } + } + + private Optional createEdgeUrl(URI uri) { + try { + return Optional.of(new EdgeUrl(uri)); + } + catch (Exception ex) { + logger.debug("Bad URI {}", uri); + return Optional.empty(); + } + } + + @Contract(pure=true) + public Optional parseLink(EdgeUrl baseUrl, String str) { + return Optional.of(str) + .map(link -> resolveUrl(baseUrl, link)) + .flatMap(this::createURI) + .map(URI::normalize) + .map(this::renormalize) + .flatMap(this::createEdgeUrl); + } + + @Contract(pure=true) + public Optional parseFrame(EdgeUrl baseUrl, Element frame) { + return Optional.of(frame) + .map(l -> l.attr("src")) + .map(link -> resolveUrl(baseUrl, link)) + .flatMap(this::createURI) + .map(URI::normalize) + .map(this::renormalize) + .flatMap(this::createEdgeUrl); + } + + @SneakyThrows + private URI renormalize(URI uri) { + if (uri.getPath() == null) { + return renormalize(new URI(uri.getScheme(), uri.getHost(), "/", uri.getFragment())); + } + if (uri.getPath().startsWith("/../")) { + return renormalize(new URI(uri.getScheme(), uri.getHost(), uri.getPath().substring(3), uri.getFragment())); + } + return uri; + } + + private String getUrl(Element element) { + var url = CharMatcher.noneOf(" \r\n\t").retainFrom(element.attr("href")); + + int anchorIndex = url.indexOf('#'); + if (anchorIndex > 0) { + return url.substring(0, anchorIndex); + } + return url; + } + + private static Pattern paramRegex = Pattern.compile("\\?.*$"); + @SneakyThrows + private String resolveUrl(EdgeUrl baseUrl, String s) { + s = paramRegex.matcher(s).replaceAll(""); + + // url looks like http://www.marginalia.nu/ + if (isAbsoluteDomain(s)) { + return s; + } + + // url looks like /my-page + if (s.startsWith("/")) { + return baseUrl.sibling(s).toString(); + } + + return baseUrl.sibling(relativeNavigation(baseUrl) + s.replaceAll(" ", "%20")).toString(); + } + + // for a relative url that looks like /foo or /foo/bar; return / or /foo + private String relativeNavigation(EdgeUrl url) { + + var lastSlash = url.path.lastIndexOf("/"); + if (lastSlash < 0) { + return "/"; + } + return url.path.substring(0, lastSlash+1); + } + + private boolean isAbsoluteDomain(String s) { + return s.matches("^[a-zA-Z]+:.*$"); + } + + private boolean shouldIndexLink(Element link) { + return isUrlRelevant(link.attr("href")) + && isRelRelevant(link.attr("rel")); + + } + + private boolean isRelRelevant(String rel) { + if (null == rel) { + return true; + } + return switch (rel) { + case "noindex" -> false; + default -> true; + }; + } + + private boolean isUrlRelevant(String href) { + if (null == href || "".equals(href)) { + return false; + } + if (blockPrefixList.stream().anyMatch(href::startsWith)) { + return false; + } + if (blockSuffixList.stream().anyMatch(href::endsWith)) { + return false; + } + if (href.length() > 128) { + return false; + } + return true; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/RssCrawler.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/RssCrawler.java new file mode 100644 index 00000000..44de1344 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/RssCrawler.java @@ -0,0 +1,217 @@ +package nu.marginalia.wmsa.edge.crawler.domain; + +import com.zaxxer.hikari.HikariDataSource; +import gnu.trove.list.array.TIntArrayList; +import gnu.trove.map.hash.TIntIntHashMap; +import it.unimi.dsi.fastutil.ints.IntArrays; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.edge.crawler.fetcher.HttpFetcher; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDaoImpl; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDomainBlacklistImpl; +import nu.marginalia.wmsa.edge.index.service.util.ranking.BuggyStandardPageRank; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.jsoup.Jsoup; +import org.mariadb.jdbc.Driver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.LinkedBlockingQueue; + +@Singleton +public class RssCrawler { + + + static LinkedBlockingQueue feedsQueue = new LinkedBlockingQueue<>(); + static LinkedBlockingQueue uploadQueue = new LinkedBlockingQueue<>(2); + + @AllArgsConstructor + static class UploadJob { + int domainId; + EdgeUrl[] urls; + } + private final HikariDataSource dataSource; + + private final HttpFetcher fetcher; + private final LinkParser lp = new LinkParser(); + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + public static void main(String[] args) throws IOException { + org.mariadb.jdbc.Driver driver = new Driver(); + + var dbModule = new DatabaseModule(); + new RssCrawler(dbModule.provideConnection()).run(); + } + + @Inject + public RssCrawler(HikariDataSource dataSource) { + + this.dataSource = dataSource; + this.fetcher = new HttpFetcher("search.marginalia.nu"); + fetcher.setAllowAllContentTypes(true); + } + + @SneakyThrows + public void run() throws IOException { + var rank = new BuggyStandardPageRank(dataSource, "memex.marginalia.nu"); + var nodes = rank.pageRankWithPeripheralNodes(rank.size(), false); + + EdgeDomainBlacklistImpl blacklist = new EdgeDomainBlacklistImpl(dataSource); + + TIntIntHashMap domainRankById = new TIntIntHashMap(nodes.size(), 0.5f, 0, Integer.MAX_VALUE); + + for (int i = 0; i < nodes.size(); i++) { + if (!blacklist.isBlacklisted(nodes.get(i))) { + domainRankById.put(nodes.get(i), i); + } + } + + List feedUrls = new ArrayList<>(15_000); + + TIntArrayList feedDomainIds = new TIntArrayList(); + try (var conn = dataSource.getConnection()) { + + try (var stmt = conn.prepareStatement("SELECT DISTINCT(DOMAIN_ID) FROM EC_FEED_URL INNER JOIN EC_DOMAIN ON EC_DOMAIN.ID=DOMAIN_ID ORDER BY RANK ASC")) { + stmt.setFetchSize(1000); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + int id = rsp.getInt(1); + + if (domainRankById.get(id) < rank.size()) { + feedDomainIds.add(id); + } + } + } + + int[] ids = feedDomainIds.toArray(); + IntArrays.quickSort(ids, (a,b) -> domainRankById.get(a) - domainRankById.get(b)); + + for (int i = 0; i < ids.length; i++) { + try (var stmt = conn.prepareStatement("SELECT DOMAIN_ID, PROTO, URL_PART, PORT, URL from EC_FEED_URL INNER JOIN EC_DOMAIN ON EC_DOMAIN.ID=DOMAIN_ID WHERE DOMAIN_ID=? ORDER BY LENGTH(URL) ASC LIMIT 1")) { + stmt.setFetchSize(10); + stmt.setInt(1, ids[i]); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + var url = new EdgeUrl(rsp.getString(2), new EdgeDomain(rsp.getString(3)), rsp.getInt(4), rsp.getString(5)); + feedUrls.add(url); + } + } + } + } + catch (Exception ex) { + logger.error("SQL error", ex); + } + + + + feedsQueue.addAll(feedUrls); + + List threads = new ArrayList<>(); + threads.add(new Thread(this::uploadThread, "Uploader")); + for (int i = 0; i < 256; i++) { + threads.add(new Thread(this::processor, "Processor")); + } + + threads.forEach(Thread::start); + + Thread.sleep(5*60_000); + threads.forEach(Thread::interrupt); + Thread.sleep(60_000); + + System.exit(0); + } + + @SneakyThrows + private void processor() { + EdgeDataStoreDaoImpl dataStoreDao = new EdgeDataStoreDaoImpl(dataSource); + + while (!feedsQueue.isEmpty()) { + try { + + var url = feedsQueue.take(); + logger.info("{}", url); + + var domainId = dataStoreDao.getDomainId(url.getDomain()); + var contents = fetcher.fetchContent(url); + + if (null != contents) { + List urls = getLinks(url, contents.data); + urls = dataStoreDao.getNewUrls(domainId, urls); + if (!urls.isEmpty()) { + uploadQueue.put(new UploadJob(domainId.getId(), urls.toArray(EdgeUrl[]::new))); + } + } + } + catch (InterruptedException ex) { + break; + } + catch (Exception ex) { + // + } + } + logger.info("Processor done"); + } + + private List getLinks(EdgeUrl base, String str) { + + var doc = Jsoup.parse(str.replaceAll("link", "lnk")); + + Set urls = new LinkedHashSet<>(); + + doc.select("entry > lnk[rel=alternate]").forEach(element -> { + var href = element.attr("href"); + if (href != null && !href.isBlank()) { + lp.parseLink(base, href) + .filter(u -> Objects.equals(u.domain.domain, base.domain.domain)) + .filter(u -> u.proto.startsWith("http")) + .ifPresent(urls::add); + } + }); + + doc.getElementsByTag("lnk").forEach(element -> { + var href = element.text(); + if (href != null && !href.isBlank()) { + lp.parseLink(base, href) + .filter(u -> Objects.equals(u.domain.domain, base.domain.domain)) + .filter(u -> u.proto.startsWith("http")) + .ifPresent(urls::add); + } + }); + + doc.select("item > guid[isPermalink=true]").forEach(element -> { + var href = element.text(); + if (href != null && !href.isBlank()) { + lp.parseLink(base, href) + .filter(u -> Objects.equals(u.domain.domain, base.domain.domain)) + .filter(u -> u.proto.startsWith("http")) + .ifPresent(urls::add); + } + }); + + return new ArrayList<>(urls); +} + + @SneakyThrows + public void uploadThread() { + EdgeDataStoreDaoImpl dao = new EdgeDataStoreDaoImpl(dataSource); + try (var conn = dataSource.getConnection(); var stmt = conn.prepareStatement("UPDATE EC_DOMAIN SET STATE=0, INDEXED=LEAST(8, INDEXED) WHERE ID=?")) { + while (!feedsQueue.isEmpty() || !uploadQueue.isEmpty()) { + var job = uploadQueue.take(); + dao.putUrl(-5., job.urls); + + stmt.setInt(1, job.domainId); + stmt.executeUpdate(); + + logger.info("{}[{}]", job.urls[0].domain, job.urls.length); + } + } + logger.info("Uploader done"); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/UrlsCache.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/UrlsCache.java new file mode 100644 index 00000000..4f83b5ab --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/UrlsCache.java @@ -0,0 +1,110 @@ +package nu.marginalia.wmsa.edge.crawler.domain; + +import gnu.trove.set.hash.TLongHashSet; +import nu.marginalia.wmsa.edge.model.WideHashable; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; + +public class UrlsCache { + private final TLongHashSet _int_set_not_thread_safe = new TLongHashSet(); + private final long[] _inserts_not_thread_safe; + + private int insertP = 0; + private long size = 0; + private final int maxSize; + + private final AtomicBoolean spinLock = new AtomicBoolean(); + + public UrlsCache() { + this(50000); + } + + public UrlsCache(final int maxSize) { + this.maxSize = maxSize; + _inserts_not_thread_safe = new long[maxSize]; + } + + /** + * + * @return true if the set was modified + */ + public boolean add(T entity) { + try { + while (!spinLock.compareAndSet(false, true)); + + return addEntityThreadUnsafe(entity.wideHash()); + } + finally { + spinLock.set(false); + } + } + + private boolean addEntityThreadUnsafe(long hash) { + + if (!_int_set_not_thread_safe.add(hash)) { + return false; + } + + if (size == maxSize) { + _int_set_not_thread_safe.remove(_inserts_not_thread_safe[insertP]); + } + else { + size++; + } + + _inserts_not_thread_safe[insertP] = hash; + insertP = (insertP+1) % maxSize; + + return true; + } + + public void addAll(T... entities) { + try { + while (!spinLock.compareAndSet(false, true)); + + Arrays.stream(entities) + .mapToLong(WideHashable::wideHash) + .forEach(this::addEntityThreadUnsafe); + } + finally { + spinLock.set(false); + } + } + + public boolean contains(T entity) { + try { + while (!spinLock.compareAndSet(false, true)); + + return _int_set_not_thread_safe.contains(entity.wideHash()); + } + finally { + spinLock.set(false); + } + } + + public boolean isMissing(T entity) { + try { + while (!spinLock.compareAndSet(false, true)); + + return !_int_set_not_thread_safe.contains(entity.wideHash()); + } + finally { + spinLock.set(false); + } + } + + + public void clear() { + try { + while (!spinLock.compareAndSet(false, true)) ; + _int_set_not_thread_safe.clear(); + size = 0; + insertP = 0; + } + finally { + spinLock.set(false); + } + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/DocumentDebugger.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/DocumentDebugger.java new file mode 100644 index 00000000..439c84fb --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/DocumentDebugger.java @@ -0,0 +1,132 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language; + +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.KeywordCounter; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.KeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.NameCounter; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentSentence; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.WordRep; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.tag.WordSeparator; +import org.jsoup.nodes.Document; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; + +public class DocumentDebugger { + private final KeywordCounter kc; + private final SentenceExtractor se; + private final KeywordExtractor ke; + private final NameCounter nc; + + Map docsByPath = new TreeMap<>(); + Path tempDir; + public DocumentDebugger(LanguageModels lm) throws IOException { + se = new SentenceExtractor(lm); + var dict = new NGramDict(lm); + ke = new KeywordExtractor(); + + kc = new KeywordCounter(dict, ke); + nc = new NameCounter(ke); + + tempDir = Files.createTempDirectory("documentdebugger"); + } + + public void writeIndex() throws FileNotFoundException { + var output = tempDir.resolve("index.html"); + + try (var pw = new PrintWriter(new FileOutputStream(output.toFile()))) { + pw.println("
      "); + + docsByPath.forEach((name, path) -> { + pw.println("
    • "); + pw.printf("%s", path, name); + pw.println("
    • "); + }); + + + pw.println("
    "); + } + + System.out.println(output); + } + + public Path debugDocument(String name, Document document) throws IOException { + + var output = tempDir.resolve(name.substring(name.lastIndexOf("/")+1)+".html"); + docsByPath.put(name, output); + + document.select("table,sup,.reference").remove(); + var languageData = se.extractSentences(document); + + Set reps = new HashSet<>(); + +// kc.count(languageData, 0.75).forEach(rep -> reps.add(rep.stemmed)); + kc.count(languageData, 0.75).forEach(rep -> reps.add(rep.stemmed)); + + try (var pw = new PrintWriter(new FileOutputStream(output.toFile()))) { + + for (var sent : languageData.titleSentences) { + pw.print("

    "); + printSent(pw, sent, reps); + pw.println("

    "); + } + + for (var sent : languageData.sentences) { + pw.println("
    "); + printSent(pw, sent, reps); + pw.println("
    "); + } + } + + return output; + } + + private void printSent(PrintWriter pw, DocumentSentence sent, Set words) { + TreeMap> spans = new TreeMap<>(); + + var names = ke.getKeywordsFromSentence(sent); + + for (var span : names) { + for (int j = 0; j < span.size(); j++) { + spans.computeIfAbsent(span.start + j, n -> new HashSet<>()).add(new WordRep(sent, span)); + } + } + + for (int i = 0; i < sent.words.length; i++) { + List matches = spans.getOrDefault(i, Collections.emptySet()).stream().filter(rep -> true || words.contains(rep.stemmed)).collect(Collectors.toList()); + + printTag(pw, sent, i, matches); + } + } + + private void printTag(PrintWriter pw, DocumentSentence sent, int i, List matches) { + + String style; + if (matches.isEmpty()) { + style = ""; + } + else if (matches.size() == 1 && !matches.get(0).word.contains("_")) { + style = "text-decoration: underline; color: #00f"; + } + else { + style = "text-decoration: underline; color: #f00"; + } + pw.printf("", + matches.stream().map(rep -> rep.word).collect(Collectors.joining(", ")), + style + ); + pw.print(sent.words[i]); + pw.print(""); pw.println(sent.posTags[i]); pw.print(""); + pw.print(" "); + if (sent.separators[i] == WordSeparator.COMMA) + pw.printf(", "); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/LanguageFilter.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/LanguageFilter.java new file mode 100644 index 00000000..7a5a1411 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/LanguageFilter.java @@ -0,0 +1,90 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language; + +import com.google.common.collect.Sets; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentLanguageData; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentSentence; +import opennlp.tools.langdetect.LanguageDetector; +import org.jsoup.nodes.Document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.text.BreakIterator; +import java.util.*; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +@Singleton +public class LanguageFilter { + + private static final Set interestingLanguages = Set.of("en", "en-us", "en-gb", "eng", "english"); + + private static final Set englishWords = new HashSet<>(); + private static final Logger logger = LoggerFactory.getLogger(LanguageFilter.class); + static { + try (var resource = Objects.requireNonNull(ClassLoader.getSystemResourceAsStream("dictionary/en-1000"), + "Could not load word frequency table"); + var br = new BufferedReader(new InputStreamReader(resource)) + ) { + for (;;) { + String s = br.readLine(); + if (s == null) { + break; + } + englishWords.add(s.toLowerCase()); + } + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + + } + + public double dictionaryAgreement(DocumentLanguageData dld) { + Set seenWords = new HashSet<>(); + int englishCount = 0; + + for (var sent : dld.sentences) { + for (var word : sent.wordsLowerCase) { + if (seenWords.add(word) && englishWords.contains(word)) { + englishCount++; + } + } + } + + double englishAgreement = englishCount / (double) Math.min(seenWords.size(), englishWords.size()); + + logger.debug("Agreement: {}", englishAgreement); + + return englishAgreement; + } + + @Inject + public LanguageFilter() { + } + + public Optional isPageInterestingByHtmlTag(Document parsed) { + return Optional.of(parsed.getElementsByTag("html")) + .map(tag -> tag.attr("lang")) + .filter(s -> !s.isBlank()) + .map(String::toLowerCase) + .map(interestingLanguages::contains); + } + + public Optional isPageInterestingByMetaLanguage(Document parsed) { + return parsed.getElementsByTag("meta").stream().filter(elem -> "content-language".equalsIgnoreCase(elem.attr("http-equiv"))) + .map(elem -> elem.attr("content")) + .filter(s -> !s.isBlank()) + .map(String::toLowerCase) + .map(interestingLanguages::contains) + .findAny(); + } + + public boolean isBlockedUnicodeRange(String data) { + return Arrays.stream(UnicodeRanges.values()) + .parallel().anyMatch(range -> range.test(data)); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/UnicodeRanges.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/UnicodeRanges.java new file mode 100644 index 00000000..347b2f7f --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/UnicodeRanges.java @@ -0,0 +1,77 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language; + +public enum UnicodeRanges { + GREEK(false, 0x0370,0x03FF), + CYRILLIC(false, 0x0400,0x04FF), + CYRILLIC2(false, 0x0500,0x052F), + ARMENIAN(false, 0x0530,0x058F), + HEBREW(false, 0x0590,0x05FF), + ARABIC(false, 0x0600,0x06FF), + SYRIAC(false, 0x0700,0x074F), + THAANA(false, 0x0780,0x07BF), + DEVANAGARI(false, 0x0900,0x097F), + BENGALI(false, 0x0980,0x09FF), + GURMUKHI(false, 0x0A00,0x0A7F), + GUJARATI(false, 0x0A80,0x0AFF), + ORIYA(false, 0x0B00,0x0B7F), + TAMIL(false, 0x0B80,0x0BFF), + TELUGU(false, 0x0C00,0x0C7F), + KANNADA(false, 0x0C80,0x0CFF), + MALAYALAM(false, 0x0D00,0x0D7F), + SINHALA(false, 0x0D80,0x0DFF), + THAI(false, 0x0E00,0x0E7F), + LAO(false, 0x0E80,0x0EFF), + TIBETAN(false, 0x0F00,0x0FFF), + MYANMAR(false, 0x1000,0x109F), + GEORGIAN(false, 0x10A0,0x10FF), + HANGUL(false, 0x1100,0x11FF), + ETHIOPIC(false, 0x1200,0x137F), + CHEROKEE(false, 0x13A0,0x13FF), + ABORIGINAL(false, 0x1400,0x167F), + OGHAM(false, 0x1680,0x169F), + RUNIC(false, 0x16A0,0x16FF), + TAGALOG(false, 0x1700,0x171F), + HANUNOO(false, 0x1720,0x173F), + BUHID(false, 0x1740,0x175F), + TAGBANWA(false, 0x1760,0x177F), + KHMER(false, 0x1780,0x17FF), + MONGOLIAN(false, 0x1800,0x18AF), + LIMBU(false, 0x1900,0x194F), + TAILE(false, 0x1950,0x197F), + KHMER2(false, 0x19E0,0x19FF), + CJKRADICALS(true,0x2E80,0x2EFF), + KANGXIRADICALS(true, 0x2F00,0x2FDF), + IDEOGRAPHICDESCRIPTION(true,0x2FF0,0x2FFF), + CJKSYMBOLS(true, 0x3000,0x303F), + HIRAGANA(true,0x3040,0x309F), + KATAKANA(true,0x30A0,0x30FF), + BOPOMOFO(true,0x3100,0x312F), + HANGULJAMO(true,0x3130,0x318F), + KANBUN(true,0x3190,0x319F), + BOPOMOFOEXTENDED(true,0x31A0,0x31BF), + KATAKANAPHONETIC(true, 0x31F0,0x31FF), + ENCLOSEDCJK(true, 0x3200,0x32FF), + CJKCOMPATIBILITY(true,0x3300,0x33FF), + CJKUNIFIEDA(true,0x3400,0x4DBF), + YIJINGHEXAGRAMSYMBOLS(true,0x4DC0,0x4DFF), + CJKUNIFIEDIDEOGRAPHS(true,0x4E00,0x9FFF), + YISYLLABLES(true,0xA000,0xA48F), + YIRADICALS(true, 0xA490,0xA4CF), + HANGULSYLLABLES(true, 0xAC00,0xD7AF) + ; + final int min; + final int max; + final boolean sensitive; + UnicodeRanges(boolean sensitive, int min, int max) { + this.sensitive = sensitive; + this.min = min; + this.max = max; + } + + boolean test(String text) { + return text.chars().limit(1000).parallel() + .filter(i -> i >= min && i < max) + .count() >= (sensitive ? 15 : 100); + + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/WordPatterns.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/WordPatterns.java new file mode 100644 index 00000000..6b178853 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/WordPatterns.java @@ -0,0 +1,117 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.*; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +public class WordPatterns { + public static final int MIN_WORD_LENGTH = 1; + public static final int MAX_WORD_LENGTH = 64; + + public static final String WORD_TOKEN_JOINER = "_"; + public static final Pattern wordPattern = Pattern.compile("[#]?[_@.a-zA-Z0-9'+\\-\\u00C0-\\u00D6\\u00D8-\\u00f6\\u00f8-\\u00ff]+[#]?"); + public static final Pattern wordPatternRestrictive = Pattern.compile("[#]?[@a-zA-Z0-9'+\\-\\u00C0-\\u00D6\\u00D8-\\u00f6\\u00f8-\\u00ff]+[#]?"); + public static final Pattern keyWordPattern = Pattern.compile("[A-Z\\u00C0-\\u00D6][_a-zA-Z\\u00C0-\\u00D6\\u00D8-\\u00f6\\u00f8-\\u00ff]{0,32}('[a-zA-Z])?"); + public static final Pattern wordAppendixPattern = Pattern.compile("[.]?[0-9a-zA-Z\\u00C0-\\u00D6\\u00D8-\\u00f6\\u00f8-\\u00ff]{1,3}[0-9]?"); + public static final Pattern joinWord = Pattern.compile("(as|an|the|of|in|a)"); + public static final Pattern keywordAppendixPattern = Pattern.compile("([0-9A-Z][A-Z0-9]{0,3})"); + public static final Pattern wordBreakPattern = Pattern.compile("([^_#@.a-zA-Z'+\\-0-9\\u00C0-\\u00D6\\u00D8-\\u00f6\\u00f8-\\u00ff]+)|[|]|(\\.(\\s+|$))"); + public static final Pattern characterNoisePattern = Pattern.compile("^[/+\\-]+$"); + + public static final Predicate wordQualitiesPredicate = wordPattern.asMatchPredicate(); + public static final Predicate restrictivePredicate = wordPatternRestrictive.asMatchPredicate(); + public static final Predicate wordAppendixPredicate = wordAppendixPattern.asMatchPredicate(); + public static final Predicate keywordPredicate = keyWordPattern.asMatchPredicate(); + public static final Predicate keywordAppendixPredicate = keywordAppendixPattern.asMatchPredicate(); + public static final Predicate wordPredicateEither = wordQualitiesPredicate.or(wordAppendixPredicate); + public static final Predicate keywordPredicateEither = keywordPredicate.or(keywordAppendixPredicate); + public static final Predicate characterNoisePredicate = characterNoisePattern.asMatchPredicate(); + + public static final Set topWords; + static { + topWords = new HashSet<>(200); + try (var resource = Objects.requireNonNull(ClassLoader.getSystemResourceAsStream("dictionary/en-stopwords"), + "Could not load word frequency table"); + var br = new BufferedReader(new InputStreamReader(resource)) + ) { + while (true) { + String s = br.readLine(); + if (s == null) { + break; + } + topWords.add(s); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static boolean hasMoreThanTwo(String s, char c, int max) { + int idx = 0; + for (int i = 0; i <= max; i++) { + idx = s.indexOf(c, idx+1); + if (idx < 0 || idx >= s.length() - 1) + return false; + } + return true; + } + + + public static boolean filter(String word) { + if (word.isBlank()) { + return false; + } + if (hasMoreThanTwo(word, '-', 2)) { + return false; + } + if (hasMoreThanTwo(word, '+', 2)) { + return false; + } + if (word.startsWith("-") + || word.endsWith("-") + ) { + return false; + } + + int numDigits = 0; + for (int i = 0; i < word.length(); i++) { + if (Character.isDigit(word.charAt(i))) { + numDigits++; + } + if (numDigits > 6) + return false; + } + + return true; + } + + public static boolean filterStrict(String word) { + + int numDigits = (int) word.chars().filter(Character::isDigit).count(); + if (numDigits == word.length()) { + return false; + } + + return true; + } + + public static boolean isStopWord(String s) { + if (s.length() < MIN_WORD_LENGTH) { + return true; + } + if (!wordQualitiesPredicate.test(s)) { + return true; + } + if (!filter(s)) { + return true; + } + if (topWords.contains(s.toLowerCase())) { + return true; + } + return false; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/conf/LanguageModels.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/conf/LanguageModels.java new file mode 100644 index 00000000..3823b5ae --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/conf/LanguageModels.java @@ -0,0 +1,15 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language.conf; + +import lombok.AllArgsConstructor; + +import java.nio.file.Path; + +@AllArgsConstructor +public class LanguageModels { + public final Path ngramDictionary; + public final Path ngramFrequency; + public final Path openNLPSentenceDetectionData; + public final Path posRules; + public final Path posDict; + public final Path openNLPTokenData; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/AsciiFlattener.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/AsciiFlattener.java new file mode 100644 index 00000000..56e540ad --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/AsciiFlattener.java @@ -0,0 +1,51 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language.processing; + +import java.util.function.Predicate; +import java.util.regex.Pattern; + +public class AsciiFlattener { + + private static final Pattern nonAscii = Pattern.compile("[^a-zA-Z0-9_.'+@#:]+"); + private static final Pattern plainAsciiPattern = Pattern.compile("^[a-zA-Z0-9_.'+@#:]+$"); + private static final Predicate plainAscii = plainAsciiPattern.asMatchPredicate(); + + public static String flattenUnicode(String s) { + if (plainAscii.test(s)) { + return s; + } + + var cdata = s.toCharArray(); + var newCdata = new char[cdata.length]; + for (int i = 0; i < cdata.length; i++) { + if ("àáâãäåæ".indexOf(cdata[i]) >= 0) { + newCdata[i] = 'a'; + } + else if ("ç".indexOf(cdata[i]) >= 0) { + newCdata[i] = 'g'; + } + else if ("òóôõöø".indexOf(cdata[i]) >= 0) { + newCdata[i] = 'o'; + } + else if ("ùúûü".indexOf(cdata[i]) >= 0) { + newCdata[i] = 'u'; + } + else if ("ýÿÞþ".indexOf(cdata[i]) >= 0) { + newCdata[i] = 'y'; + } + else if ("ìíîï".indexOf(cdata[i]) >= 0) { + newCdata[i] = 'i'; + } + else if ("èéêë".indexOf(cdata[i]) >= 0) { + newCdata[i] = 'e'; + } + else if ("ß".indexOf(cdata[i]) >= 0) { + newCdata[i] = 's'; + } + else { + newCdata[i] = cdata[i]; + } + } + return nonAscii.matcher(new String(newCdata)).replaceAll(""); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/DocumentKeywordExtractor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/DocumentKeywordExtractor.java new file mode 100644 index 00000000..19f2a117 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/DocumentKeywordExtractor.java @@ -0,0 +1,150 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language.processing; + +import com.google.common.collect.Sets; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.WordPatterns; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentLanguageData; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.WordRep; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageWordSet; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageWords; +import org.jetbrains.annotations.NotNull; + +import javax.inject.Inject; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class DocumentKeywordExtractor { + + private final KeywordExtractor keywordExtractor; + private final KeywordCounter tfIdfCounter; + private final NameCounter nameCounter; + private final LongNameCounter longNameCounter; + private final SubjectCounter subjectCounter; + + private final NGramDict dict; + + @Inject + public DocumentKeywordExtractor(NGramDict dict) { + this.dict = dict; + + keywordExtractor = new KeywordExtractor(); + + tfIdfCounter = new KeywordCounter(dict, keywordExtractor); + nameCounter = new NameCounter(keywordExtractor); + longNameCounter = new LongNameCounter(dict, keywordExtractor); + subjectCounter = new SubjectCounter(keywordExtractor); + } + + public EdgePageWordSet extractKeywords(DocumentLanguageData documentLanguageData) { + + var titleWords = extractTitleWords(documentLanguageData); + + var wordsTfIdf = tfIdfCounter.count(documentLanguageData, 0.75); + var wordsNamesRepeated = nameCounter.count(documentLanguageData, 2); + var wordsNamesAll = nameCounter.count(documentLanguageData, 1); + var subjects = subjectCounter.count(documentLanguageData); + + List wordsLongName = longNameCounter.count(documentLanguageData); + + int totalSize = wordsTfIdf.size(); + + List lowKeywords = new ArrayList<>(totalSize / 2); + List midKeywords = new ArrayList<>(totalSize / 2); + List topKeywords = new ArrayList<>(totalSize / 2); + + for(var v : wordsTfIdf) { + if (topKeywords.size() < totalSize / 10) topKeywords.add(v); + else if (midKeywords.size() < totalSize / 5) midKeywords.add(v); + else lowKeywords.add(v); + } + + var wordsToMatchWithTitle = joinWordLists(topKeywords, midKeywords, wordsNamesRepeated, subjects); + + var words = getSimpleWords(documentLanguageData); + + for (var w : wordsLongName) + words.add(w.word); + for (var w : lowKeywords) + words.remove(w.word); + for (var w : midKeywords) + words.remove(w.word); + for (var w : topKeywords) + words.remove(w.word); + + var wordSet = new EdgePageWordSet( + createWords(IndexBlock.TitleKeywords, overlappingStems(titleWords, wordsToMatchWithTitle)), + createWords(IndexBlock.Topic, subjects), + createWords(IndexBlock.Title, titleWords), + createWords(IndexBlock.NamesWords, wordsNamesAll), + createWords(IndexBlock.Top, topKeywords), + createWords(IndexBlock.Middle, midKeywords), + createWords(IndexBlock.Low, lowKeywords) + ); + + wordSet.append(IndexBlock.Words, words); + + return wordSet; + } + + private List extractTitleWords(DocumentLanguageData documentLanguageData) { + return Arrays.stream(documentLanguageData.titleSentences).flatMap(sent -> + keywordExtractor.getWordsFromSentence(sent).stream().sorted().distinct().map(w -> new WordRep(sent, w))) + .limit(100) + .collect(Collectors.toList()); + } + + private Collection joinWordLists(List... words) { + int size = 0; + for (var lst : words) { + size += lst.size(); + } + if (size == 0) + return Collections.emptyList(); + + final LinkedHashSet ret = new LinkedHashSet<>(size); + for (var lst : words) { + ret.addAll(lst); + } + return ret; + } + + @NotNull + private Set getSimpleWords(DocumentLanguageData documentLanguageData) { + Map counts = new HashMap<>(documentLanguageData.totalNumWords()); + + for (var sent : documentLanguageData.sentences) { + for (int i = 0; i < sent.length(); i++) { + if (!sent.isStopWord(i)) { + String w = AsciiFlattener.flattenUnicode(sent.wordsLowerCase[i]); + if (counts.containsKey(w) || (WordPatterns.wordQualitiesPredicate.test(w) && WordPatterns.filter(w))) { + counts.merge(w, 1, Integer::sum); + } + } + } + } + + return counts.entrySet().stream().filter(c2 -> c2.getValue()>=1) + .sorted(Comparator.comparing(this::value)) + .map(Map.Entry::getKey) + .limit(512).collect(Collectors.toSet()); + } + + private double value(Map.Entry e) { + double N = 11820118.; // Number of documents in term freq dictionary + + return (1+Math.log(e.getValue())) * Math.log((1.+dict.getTermFreq(e.getKey()))/N); + } + + public EdgePageWords createWords(IndexBlock block, Collection words) { + return new EdgePageWords(block, words.stream().map(w -> w.word).map(AsciiFlattener::flattenUnicode).filter(WordPatterns.wordQualitiesPredicate).collect(Collectors.toSet())); + } + + private Set overlappingStems(Collection wordsA, Collection wordsB) { + Set stemmedA = wordsA.stream().map(WordRep::getStemmed).collect(Collectors.toSet()); + Set stemmedB = wordsB.stream().map(WordRep::getStemmed).collect(Collectors.toSet()); + Set stemmedIntersect = Sets.intersection(stemmedA, stemmedB); + return Stream.concat(wordsA.stream(), wordsB.stream()).filter(w -> stemmedIntersect.contains(w.getStemmed())).collect(Collectors.toSet()); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/KeywordCounter.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/KeywordCounter.java new file mode 100644 index 00000000..89bd9950 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/KeywordCounter.java @@ -0,0 +1,93 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language.processing; + +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentLanguageData; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentSentence; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.WordRep; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.WordSpan; + +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class KeywordCounter { + private final KeywordExtractor keywordExtractor; + private final NGramDict dict; + + public KeywordCounter(NGramDict dict, KeywordExtractor keywordExtractor) { + this.dict = dict; + this.keywordExtractor = keywordExtractor; + } + + public List count(DocumentLanguageData dld, double cutoff) { + HashMap counts = new HashMap<>(1000); + HashMap> instances = new HashMap<>(1000); + + for (int i = 0; i < dld.sentences.length; i++) { + DocumentSentence sent = dld.sentences[i]; + double value = 1.0 / Math.log(1+i); + var keywords = keywordExtractor.getKeywordsFromSentence(sent); + for (var span : keywords) { + var stemmed = sent.constructStemmedWordFromSpan(span); + if (stemmed.isBlank()) + continue; + + counts.merge(stemmed, value, Double::sum); + + instances.computeIfAbsent(stemmed, k -> new HashSet<>()).add(sent.constructWordFromSpan(span)); + } + }; + + var topWords = counts.entrySet().stream() + .filter(w -> w.getValue() > cutoff) + .sorted(Comparator.comparing(this::getTermValue)) + .limit(Math.min(100, counts.size()/2)) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + var topWordsSet = new HashSet<>(topWords); + + final Set keywords = new HashSet<>(); + + for (var sentence : dld.sentences) { + for (WordSpan kw : keywordExtractor.getKeywordsFromSentence(sentence)) { + String stemmedWord = sentence.constructStemmedWordFromSpan(kw); + if (topWords.contains(stemmedWord)) { + keywords.add(new WordRep(sentence, kw)); + } + } + } + + for (var sentence : dld.sentences) { + for (var kw : keywordExtractor.getKeywordsFromSentenceStrict(sentence, topWordsSet, true)) { + keywords.add(new WordRep(sentence, kw)); + } + } + + Map sortOrder = IntStream.range(0, topWords.size()).boxed().collect(Collectors.toMap(topWords::get, i->i)); + + Comparator comp = Comparator.comparing(wr -> sortOrder.getOrDefault(wr.stemmed, topWords.size())); + + var ret = new ArrayList<>(keywords); + ret.sort(comp); + return ret; + } + + private static Pattern separator = Pattern.compile("_"); + + public double getTermValue(Map.Entry e) { + String[] parts = separator.split(e.getKey()); + double totalValue = 0.; + for (String part : parts) { + totalValue += value(part, e.getValue()); + } + return totalValue / Math.sqrt(parts.length); + } + + double value(String key, double value) { + return (1+Math.log(value)) * Math.log((1.+dict.getTermFreq(key))/11820118.); + } + + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/KeywordExtractor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/KeywordExtractor.java new file mode 100644 index 00000000..6c847daf --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/KeywordExtractor.java @@ -0,0 +1,380 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language.processing; + +import nu.marginalia.wmsa.edge.crawler.domain.language.WordPatterns; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentSentence; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.tag.WordSeparator; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.WordSpan; + +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +public class KeywordExtractor { + + public boolean isLegacy() { + return legacy; + } + + public void setLegacy(boolean legacy) { + this.legacy = legacy; + } + + private boolean legacy; + + public WordSpan[] getNameLikes(DocumentSentence sentence) { + var direct = IntStream.range(0, sentence.length()) + .filter(i -> sentence.posTags[i].startsWith("N")) + .mapToObj(i -> new WordSpan(i, i+1)) + ; + var two = IntStream.range(1, sentence.length()) + .filter(i -> sentence.separators[i-1] == WordSeparator.SPACE) + .filter(i -> isName(i, sentence, Collections.emptySet())) + .filter(i -> isName(i -1, sentence, Collections.emptySet())) + .mapToObj(i -> new WordSpan(i-1, i+1)) + ; + + var a_in_b = IntStream.range(2, sentence.length()) + .filter(i -> sentence.separators[i-1] == WordSeparator.SPACE) + .filter(i -> isProperNoun(i, sentence)) + .filter(i -> isJoiner(sentence, i-1)) + .filter(i -> isProperNoun(i-2, sentence)) + .mapToObj(i -> new WordSpan(i-2, i+1)) + ; + + var a_in_det_b = IntStream.range(3, sentence.length()) + .filter(i -> sentence.separators[i-1] == WordSeparator.SPACE + && sentence.separators[i-2] == WordSeparator.SPACE) + .filter(i -> isProperNoun(i, sentence)) + .filter(i -> isJoiner(sentence, i-1)) + .filter(i -> sentence.posTags[i-2].equals("DT")) + .filter(i -> isProperNoun(i-3, sentence)) + .mapToObj(i -> new WordSpan(i-3, i+1)) + ; + var a_in_in_b = IntStream.range(3, sentence.length()) + .filter(i -> sentence.separators[i-1] == WordSeparator.SPACE + && sentence.separators[i-2] == WordSeparator.SPACE) + .filter(i -> isProperNoun(i, sentence)) + .filter(i -> isJoiner(sentence, i-1) || isProperNoun(i-1, sentence)) + .filter(i -> isJoiner(sentence, i-2) || isProperNoun(i-2, sentence)) + .filter(i -> isProperNoun(i-3, sentence)) + .mapToObj(i -> new WordSpan(i-3, i+1)) + ; + var three = IntStream.range(2, sentence.length()) + .filter(i -> sentence.separators[i-1] == WordSeparator.SPACE + && sentence.separators[i-2] == WordSeparator.SPACE) + .filter(i -> isName(i, sentence, Collections.emptySet())) + .filter(i -> isName(i-1, sentence, Collections.emptySet())) + .filter(i -> isName(i-2, sentence, Collections.emptySet())) + .mapToObj(i -> new WordSpan(i-2, i+1)) + ; + var four = IntStream.range(3, sentence.length()) + .filter(i -> sentence.separators[i-1] == WordSeparator.SPACE + && sentence.separators[i-2] == WordSeparator.SPACE + && sentence.separators[i-3] == WordSeparator.SPACE) + .filter(i -> isName(i, sentence, Collections.emptySet())) + .filter(i -> isName(i - 1, sentence, Collections.emptySet())) + .filter(i -> isName(i - 2, sentence, Collections.emptySet())) + .filter(i -> isName(i - 3, sentence, Collections.emptySet())) + .mapToObj(i -> new WordSpan(i-3, i+1)) + ; + + return Stream.of(direct, two, a_in_b, a_in_in_b, a_in_det_b, three, four).flatMap(Function.identity()) + .toArray(WordSpan[]::new); + } + + + public WordSpan[] getNames(DocumentSentence sentence) { + List spans = new ArrayList<>(sentence.length()); + + for (int i = 0; i < sentence.length(); i++) { + if (isProperNoun(i, sentence)) + spans.add(new WordSpan(i, i+1)); + } + + for (int i = 1; i < sentence.length(); i++) { + if (sentence.separators[i-1] == WordSeparator.COMMA) { continue; } + + if (isProperNoun(i, sentence) && isProperNoun(i-1, sentence)) + spans.add(new WordSpan(i-1, i+1)); + } + + for (int i = 2; i < sentence.length(); i++) { + if (sentence.separators[i-2] == WordSeparator.COMMA) { continue; } + if (sentence.separators[i-1] == WordSeparator.COMMA) { i++; continue; } + + if (isProperNoun(i, sentence) && (isJoiner(sentence, i-1) || isProperNoun(i-1, sentence)) && isProperNoun(i-2, sentence)) + spans.add(new WordSpan(i-2, i+1)); + } + + for (int i = 3; i < sentence.length(); i++) { + if (sentence.separators[i-3] == WordSeparator.COMMA) { continue; } + if (sentence.separators[i-2] == WordSeparator.COMMA) { i++; continue; } + if (sentence.separators[i-1] == WordSeparator.COMMA) { i+=2; continue; } + + if (isProperNoun(i, sentence) && isProperNoun(i-3, sentence)) { + if (isProperNoun(i - 1, sentence) && isProperNoun(i - 2, sentence)) { + spans.add(new WordSpan(i-3, i+1)); + } + else if (isJoiner(sentence, i-2) && sentence.posTags[i-1].equals("DT")) { + spans.add(new WordSpan(i-3, i+1)); + } + else if ((isJoiner(sentence, i-1)||isProperNoun(i - 1, sentence)) && (isJoiner(sentence, i-2)||isProperNoun(i - 2, sentence))) { + spans.add(new WordSpan(i-3, i+1)); + } + } + } + + return spans.toArray(WordSpan[]::new); + } + + public WordSpan[] getNamesStrict(DocumentSentence sentence) { + List spans = new ArrayList<>(sentence.length()); + + + for (int i = 0; i < sentence.length(); i++) { + if (isProperNoun(i, sentence)) + spans.add(new WordSpan(i, i+1)); + } + + for (int i = 1; i < sentence.length(); i++) { + if (sentence.separators[i-1] == WordSeparator.COMMA) { continue; } + if (isProperNoun(i, sentence) && isProperNoun(i-1, sentence)) + spans.add(new WordSpan(i-1, i+1)); + } + + for (int i = 2; i < sentence.length(); i++) { + if (sentence.separators[i-2] == WordSeparator.COMMA) { continue; } + if (sentence.separators[i-1] == WordSeparator.COMMA) { i++; continue; } + if (isProperNoun(i, sentence) && (isJoiner(sentence, i-1) || isProperNoun(i-1, sentence)) && isProperNoun(i-2, sentence)) + spans.add(new WordSpan(i-2, i+1)); + } + + for (int i = 3; i < sentence.length(); i++) { + if (sentence.separators[i-3] == WordSeparator.COMMA) { continue; } + if (sentence.separators[i-2] == WordSeparator.COMMA) { i++; continue; } + if (sentence.separators[i-1] == WordSeparator.COMMA) { i+=2; continue; } + + if (isProperNoun(i, sentence) && isProperNoun(i-3, sentence)) { + if (isProperNoun(i - 1, sentence) && isProperNoun(i - 2, sentence)) { + spans.add(new WordSpan(i-3, i+1)); + } + else if (isJoiner(sentence, i-1) && sentence.posTags[i-2].equals("DT")) { + spans.add(new WordSpan(i-3, i+1)); + } + } + } + + return spans.toArray(WordSpan[]::new); + } + + public boolean isProperNoun(int i, DocumentSentence sent) { + return "NNP".equals(sent.posTags[i]) || "NNPS".equals(sent.posTags[i]); + } + + public boolean isJoiner(DocumentSentence sent, int i) { + if(sent.posTags[i].equals("IN")) { + return true; + } + if (sent.posTags[i].equals("TO")) { + return true; + } + if (sent.posTags[i].equals("CC")) { + return sent.wordsLowerCase[i].equals("and"); + } + return false; + } + + public List getWordsFromSentence(DocumentSentence sentence) { + List spans = new ArrayList<>(); + + for (int k = 0; k < 4; k++) { + for (int i = k; i < sentence.length(); i++) { + var w = new WordSpan(i-k, i + 1); + + if (isViableSpanForWord(sentence, w)) { + spans.add(w); + } + } + } + + return spans; + } + + private boolean isViableSpanForWord(DocumentSentence sentence, WordSpan w) { + + for (int i = w.start; i < w.end-1; i++) { + if (sentence.separators[i] == WordSeparator.COMMA) { + return false; + } + } + String word = sentence.constructWordFromSpan(w); + + if (word.isBlank() || WordPatterns.isStopWord(word)) return false; + if (sentence.posTags[w.start].equals("CC")) return false; + if (sentence.posTags[w.end-1].equals("IN")) return false; + if (sentence.posTags[w.end-1].equals("DT")) return false; + if (sentence.posTags[w.end-1].equals("CC")) return false; + if (sentence.posTags[w.end-1].equals("TO")) return false; + + return true; + } + + public WordSpan[] getKeywordsFromSentence(DocumentSentence sentence) { + if (sentence.keywords != null) { + return sentence.keywords.get(); + } + List spans = new ArrayList<>(sentence.length()); + + Set topWords = Collections.emptySet(); + + for (int i = 0; i < sentence.length(); i++) { + if (isName(i, sentence, topWords) || isTopAdj(i, sentence, topWords)) + spans.add(new WordSpan(i, i+1)); + } + + for (int i = 1; i < sentence.length(); i++) { + if (sentence.separators[i-1] == WordSeparator.COMMA) { continue; } + + if (isName(i, sentence, topWords)) { + if (isName(i - 1, sentence, topWords) || isTopAdj(i-1, sentence, topWords)) + spans.add(new WordSpan(i - 1, i + 1)); + } + if (sentence.posTags[i].equals("CD") && isName(i-1, sentence, topWords)) { + spans.add(new WordSpan(i - 1, i + 1)); + } + } + + for (int i = 2; i < sentence.length(); i++) { + if (sentence.separators[i-1] == WordSeparator.COMMA) { i++; continue; } + if (sentence.separators[i-2] == WordSeparator.COMMA) { continue; } + + if (isName(i, sentence, topWords)) { + if ((isName(i-1, sentence, topWords) || isTopAdj(i-1, sentence, topWords)) + && (isName(i-2, sentence, topWords) || isTopAdj(i-2, sentence, topWords))) { + spans.add(new WordSpan(i - 2, i + 1)); + } + else if ((isProperNoun(i-1, sentence) || isJoiner(sentence, i-1)) && isProperNoun(i-2, sentence)) { + spans.add(new WordSpan(i - 2, i + 1)); + } + } + else if (sentence.posTags[i].equals("CD") && isName(i-1, sentence, topWords) && isName(i-2, sentence, topWords)) { + spans.add(new WordSpan(i - 2, i + 1)); + } + } + + for (int i = 3; i < sentence.length(); i++) { + if (sentence.separators[i-1] == WordSeparator.COMMA) { i+=2; continue; } + if (sentence.separators[i-2] == WordSeparator.COMMA) { i++; continue; } + if (sentence.separators[i-3] == WordSeparator.COMMA) { continue; } + + if (isName(i, sentence, topWords) && + (isName(i-1, sentence, topWords) || isTopAdj(i-1, sentence, topWords)) && + (isName(i-2, sentence, topWords) || isTopAdj(i-2, sentence, topWords)) && + (isName(i-3, sentence, topWords) || isTopAdj(i-3, sentence, topWords))) { + spans.add(new WordSpan(i - 3, i + 1)); + } + else if (isProperNoun(i, sentence) && isProperNoun(i-3, sentence)) { + if (isProperNoun(i - 1, sentence) && isProperNoun(i - 2, sentence)) { + spans.add(new WordSpan(i-3, i+1)); + } + else if (isJoiner(sentence, i-1) && sentence.posTags[i-2].equals("DT")) { + spans.add(new WordSpan(i-3, i+1)); + } + else if ((isProperNoun(i-1, sentence) || isJoiner(sentence, i-1)) && (isProperNoun(i-2, sentence)|| isJoiner(sentence, i-2))) { + spans.add(new WordSpan(i-3, i + 1)); + } + } + + } + + var ret = spans.toArray(WordSpan[]::new); + sentence.keywords = new SoftReference<>(ret); + + return ret; + } + + public WordSpan[] getKeywordsFromSentenceStrict(DocumentSentence sentence, Set topWords, boolean reducePartials) { + List spans = new ArrayList<>(sentence.length()); + + if (!reducePartials) { + for (int i = 0; i < sentence.length(); i++) { + if (topWords.contains(sentence.stemmedWords[i])) + spans.add(new WordSpan(i, i + 1)); + } + } + + for (int i = 1; i < sentence.length(); i++) { + if (sentence.separators[i-1] == WordSeparator.COMMA) { continue; } + + if (topWords.contains(sentence.stemmedWords[i]) + && !sentence.words[i].endsWith("'s") + && topWords.contains(sentence.stemmedWords[i-1])) { + spans.add(new WordSpan(i-1, i + 1)); + } + } + for (int i = 2; i < sentence.length(); i++) { + if (sentence.separators[i-2] == WordSeparator.COMMA) { continue; } + if (sentence.separators[i-1] == WordSeparator.COMMA) { i++; continue; } + + if (topWords.contains(sentence.stemmedWords[i]) + && !sentence.words[i].endsWith("'s") + && (topWords.contains(sentence.stemmedWords[i-1]) || isJoiner(sentence, i-1)) + && topWords.contains(sentence.stemmedWords[i-2]) + ) { + spans.add(new WordSpan(i-2, i + 1)); + } + } + + for (int i = 3; i < sentence.length(); i++) { + if (sentence.separators[i-3] == WordSeparator.COMMA) { continue; } + if (sentence.separators[i-2] == WordSeparator.COMMA) { i++; continue; } + if (sentence.separators[i-1] == WordSeparator.COMMA) { i+=2; continue; } + if (!sentence.words[i-2].endsWith("'s")) { continue; } + if (!sentence.words[i-3].endsWith("'s")) { continue; } + + if (topWords.contains(sentence.stemmedWords[i]) + && !sentence.words[i].endsWith("'s") && topWords.contains(sentence.stemmedWords[i-3])) { + if (topWords.contains(sentence.stemmedWords[i-1]) && topWords.contains(sentence.stemmedWords[i-2])) { + spans.add(new WordSpan(i-3, i + 1)); + } + else if (topWords.contains(sentence.stemmedWords[i-1]) && isJoiner(sentence, i-2)) { + spans.add(new WordSpan(i-3, i + 1)); + } + else if (isJoiner(sentence, i-2) && sentence.posTags[i-1].equals("DT")) { + spans.add(new WordSpan(i-3, i + 1)); + } + else if (isJoiner(sentence, i-2) && isJoiner(sentence, i-1)) { + spans.add(new WordSpan(i-3, i + 1)); + } + } + } + + return spans.toArray(WordSpan[]::new); + } + + private boolean isName(int i, DocumentSentence sentence, Set topWords) { + if (!topWords.isEmpty()) { + String posTag = sentence.posTags[i]; + String word = sentence.stemmedWords[i]; + + return ((topWords.contains(word)) && (posTag.startsWith("N") || "VBN".equals(posTag)) && !sentence.isStopWord(i)); + } + + + String posTag = sentence.posTags[i]; + +// if (posTag.startsWith("N") || posTag.startsWith("V") || posTag.startsWith("R") || posTag.startsWith("J")) + return (posTag.startsWith("N") || "VBN".equals(posTag)) && !sentence.isStopWord(i); + } + + private boolean isTopAdj(int i, DocumentSentence sentence, Set topWords) { + String posTag = sentence.posTags[i]; + + return (posTag.startsWith("JJ") || posTag.startsWith("R") || posTag.startsWith("VBG")); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/LongNameCounter.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/LongNameCounter.java new file mode 100644 index 00000000..332a6bbe --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/LongNameCounter.java @@ -0,0 +1,63 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language.processing; + +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentLanguageData; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentSentence; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.WordRep; + +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class LongNameCounter { + private final KeywordExtractor keywordExtractor; + + private final NGramDict dict; + public LongNameCounter(NGramDict dict, KeywordExtractor keywordExtractor) { + this.dict = dict; + this.keywordExtractor = keywordExtractor; + } + + public List count(DocumentLanguageData dld) { + HashMap counts = new HashMap<>(1000); + HashMap> instances = new HashMap<>(1000); + + for (int i = 0; i < dld.sentences.length; i++) { + DocumentSentence sent = dld.sentences[i]; + var keywords = keywordExtractor.getNamesStrict(sent); + for (var span : keywords) { + var stemmed = sent.constructStemmedWordFromSpan(span); + counts.merge(stemmed, 1., Double::sum); + instances.computeIfAbsent(stemmed, k -> new HashSet<>()).add(new WordRep(sent, span)); + } + }; + + return counts.entrySet().stream().filter(e -> termSize(e.getKey()) > 1) + .sorted(Comparator.comparing(this::getTermValue)) + .limit(Math.min(50, counts.size()/3)) + .map(Map.Entry::getKey) + .flatMap(w -> instances.get(w).stream()).collect(Collectors.toList()); + } + + int termSize(String word) { + return 1 + (int) word.chars().filter(c -> c == '_').count(); + } + + + Pattern separator = Pattern.compile("_"); + + public double getTermValue(Map.Entry e) { + String[] parts = separator.split(e.getKey()); + double totalValue = 0.; + for (String part : parts) { + totalValue += value(part, e.getValue()); + } + return totalValue / Math.sqrt(parts.length); + } + + double value(String key, double value) { + return (1+Math.log(value)) * Math.log((1.+dict.getTermFreq(key))/11820118.); + } + + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/NameCounter.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/NameCounter.java new file mode 100644 index 00000000..4023154b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/NameCounter.java @@ -0,0 +1,40 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language.processing; + +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentLanguageData; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentSentence; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.WordRep; + +import java.util.*; +import java.util.stream.Collectors; + +public class NameCounter { + private final KeywordExtractor keywordExtractor; + + public NameCounter(KeywordExtractor keywordExtractor) { + this.keywordExtractor = keywordExtractor; + } + + public List count(DocumentLanguageData dld, int minCount) { + HashMap counts = new HashMap<>(1000); + HashMap> instances = new HashMap<>(1000); + + for (int i = 0; i < dld.sentences.length; i++) { + DocumentSentence sent = dld.sentences[i]; + var keywords = keywordExtractor.getNames(sent); + for (var span : keywords) { + var stemmed = sent.constructStemmedWordFromSpan(span); + + counts.merge(stemmed, 1., Double::sum); + instances.computeIfAbsent(stemmed, k -> new HashSet<>()).add(new WordRep(sent, span)); + } + }; + + return counts.entrySet().stream() + .filter(e -> e.getValue() >= minCount) + .sorted(Comparator.comparing(e -> -e.getValue())) + .limit(150) + .map(Map.Entry::getKey) + .flatMap(w -> instances.get(w).stream()).collect(Collectors.toList()); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/SentenceExtractor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/SentenceExtractor.java new file mode 100644 index 00000000..564ff100 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/SentenceExtractor.java @@ -0,0 +1,299 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language.processing; + +import com.github.datquocnguyen.RDRPOSTagger; +import gnu.trove.list.array.TIntArrayList; +import gnu.trove.map.hash.TObjectIntHashMap; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentLanguageData; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentSentence; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.tag.WordSeparator; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlTagCleaner; +import opennlp.tools.sentdetect.SentenceDetectorME; +import opennlp.tools.sentdetect.SentenceModel; +import opennlp.tools.stemmer.PorterStemmer; +import org.jetbrains.annotations.NotNull; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +import static nu.marginalia.wmsa.edge.crawler.domain.language.WordPatterns.*; + +public class SentenceExtractor { + + private SentenceDetectorME sentenceDetector; + private RDRPOSTagger rdrposTagger; + + private final PorterStemmer porterStemmer = new PorterStemmer(); + private boolean legacyMode = false; + private static final Logger logger = LoggerFactory.getLogger(SentenceExtractor.class); + + private static final HtmlTagCleaner tagCleaner = new HtmlTagCleaner(); + + @SneakyThrows @Inject + public SentenceExtractor(LanguageModels models) { + try (InputStream modelIn = new FileInputStream(models.openNLPSentenceDetectionData.toFile())) { + var sentenceModel = new SentenceModel(modelIn); + sentenceDetector = new SentenceDetectorME(sentenceModel); + } + catch (IOException ex) { + sentenceDetector = null; + logger.error("Could not initialize sentence detector", ex); + } + + try { + rdrposTagger = new RDRPOSTagger(models.posDict, models.posRules); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + public DocumentLanguageData extractSentences(Document doc) { + final String text = asText(doc); + final DocumentSentence[] textSentences = extractSentencesFromString(text); + + String title = doc.getElementsByTag("title").text() + " . " + + Optional.ofNullable(doc.getElementsByTag("h1").first()).map(Element::text).orElse(""); + + if (title.trim().length() < 3) { + title = Optional.ofNullable(doc.getElementsByTag("h2").first()).map(Element::text).orElse(""); + } + + if (title.trim().length() < 3 && textSentences.length > 0) { + for (DocumentSentence textSentence : textSentences) { + if (textSentence.length() > 0) { + title = textSentence.originalSentence.toLowerCase(); + break; + } + } + } + + TObjectIntHashMap counts = calculateWordCounts(textSentences); + var titleSentences = extractSentencesFromString(title.toLowerCase()); + return new DocumentLanguageData(textSentences, titleSentences, counts); + } + + public DocumentLanguageData extractSentences(String text) { + final DocumentSentence[] textSentences = extractSentencesFromString(text); + + String title = ""; + for (DocumentSentence textSentence : textSentences) { + if (textSentence.length() > 0) { + title = textSentence.originalSentence.toLowerCase(); + break; + } + } + + + TObjectIntHashMap counts = calculateWordCounts(textSentences); + + return new DocumentLanguageData(textSentences, extractSentencesFromString(title.toLowerCase()), counts); + } + + + public DocumentLanguageData extractSentences(String text, String title) { + final DocumentSentence[] textSentences = extractSentencesFromString(text); + + TObjectIntHashMap counts = calculateWordCounts(textSentences); + + return new DocumentLanguageData(textSentences, extractSentencesFromString(title.toLowerCase()), counts); + } + + + @NotNull + private TObjectIntHashMap calculateWordCounts(DocumentSentence[] textSentences) { + TObjectIntHashMap counts = new TObjectIntHashMap<>(textSentences.length*10, 0.5f, 0); + + for (var sent : textSentences) { + for (var word : sent.stemmedWords) { + counts.adjustOrPutValue(word, 1, 1); + } + } + return counts; + } + + private static final Pattern dotPattern = Pattern.compile("\\.+$"); + private static final Pattern splitPattern = Pattern.compile("( -|- |\\|)"); + private static final Pattern badCharPattern = Pattern.compile("([^_#@.a-zA-Z'+\\-0-9\\u00C0-\\u00D6\\u00D8-\\u00f6\\u00f8-\\u00ff]+)|(\\.(\\s+|$))"); + private static final Pattern possessivePattern = Pattern.compile("'(s)?$"); + + public DocumentSentence extractSentence(String text) { + var wordsAndSeps = splitSegment(text); + + var words = wordsAndSeps.words; + var seps = wordsAndSeps.separators; + var lc = toLc(wordsAndSeps.words); + + return new DocumentSentence( + badCharPattern.matcher(text).replaceAll(" "), words, seps, lc, rdrposTagger.tagsForEnSentence(words), stemSentence(lc) + ); + } + + public DocumentSentence[] extractSentencesFromString(String text) { + String[] sentences; + + String textNormalizedSpaces = text.replaceAll("\\s", " "); + try { + sentences = sentenceDetector.sentDetect(textNormalizedSpaces); + } + catch (Exception ex) { + sentences = textNormalizedSpaces.split("[.]"); + } + + if (sentences.length > 250) { + sentences = Arrays.copyOf(sentences, 250); + } + + sentences = Arrays.stream(sentences) + .filter(s -> !s.isBlank()) + .flatMap(s -> Arrays.stream(splitPattern.split(s))) + .toArray(String[]::new); + + final String[][] tokens = new String[sentences.length][]; + final int[][] separators = new int[sentences.length][]; + final String[][] posTags = new String[sentences.length][]; + final String[][] tokensLc = new String[sentences.length][]; + final String[][] stemmedWords = new String[sentences.length][]; + + for (int i = 0; i < tokens.length; i++) { + + var wordsAndSeps = splitSegment(sentences[i]); //tokenizer.tokenize(sentences[i]); + tokens[i] = wordsAndSeps.words; + separators[i] = wordsAndSeps.separators; + if (tokens[i].length > 250) { + tokens[i] = Arrays.copyOf(tokens[i], 250); + separators[i] = Arrays.copyOf(separators[i], 250); + } + for (int j = 0; j < tokens[i].length; j++) { + tokens[i][j] = dotPattern.matcher(tokens[i][j]).replaceAll( ""); + } + } + + for (int i = 0; i < tokens.length; i++) { + posTags[i] = rdrposTagger.tagsForEnSentence(tokens[i]); + } + + for (int i = 0; i < tokens.length; i++) { + tokensLc[i] = toLc(tokens[i]); + } + + for (int i = 0; i < tokens.length; i++) { + stemmedWords[i] = stemSentence(tokensLc[i]); + } + + DocumentSentence[] ret = new DocumentSentence[sentences.length]; + for (int i = 0; i < ret.length; i++) { + ret[i] = new DocumentSentence(badCharPattern.matcher(sentences[i]).replaceAll(" "), tokens[i], separators[i], tokensLc[i], posTags[i], stemmedWords[i]); + } + return ret; + } + + private String[] stemSentence(String[] strings) { + String[] stemmed = new String[strings.length]; + for (int i = 0; i < stemmed.length; i++) { + var sent = possessivePattern.matcher(strings[i]).replaceAll(""); + try { + stemmed[i] = porterStemmer.stem(sent); + } + catch (Exception ex) { + stemmed[i] = "NN"; // ??? + } + } + return stemmed; + } + + private String[] toLc(String[] words) { + String[] lower = new String[words.length]; + for (int i = 0; i < lower.length; i++) { + lower[i] = possessivePattern.matcher(words[i].toLowerCase()).replaceAll(""); + } + return lower; + } + + public String asText(Document dc) { + + tagCleaner.clean(dc); + + String text = dc.getElementsByTag("body").text(); + + return text.substring(0, (int) (text.length()*0.95)); + } + + @AllArgsConstructor @Getter + private static class WordsAndSeparators { + String[] words; + int[] separators; + } + + private WordsAndSeparators splitSegment(String segment) { + var matcher = wordBreakPattern.matcher(segment); + + List words = new ArrayList<>(segment.length()/6); + TIntArrayList separators = new TIntArrayList(segment.length()/6); + + int start = 0; + int wordStart = 0; + while (wordStart <= segment.length()) { + if (!matcher.find(wordStart)) { + words.add(segment.substring(wordStart)); + separators.add(WordSeparator.SPACE); + break; + } + + if (wordStart != matcher.start()) { + words.add(segment.substring(wordStart, matcher.start())); + separators.add(segment.substring(matcher.start(), matcher.end()).isBlank() ? WordSeparator.SPACE : WordSeparator.COMMA); + } + wordStart = matcher.end(); + } + + String[] parts = words.toArray(String[]::new); + int length = 0; + for (int i = 0; i < parts.length; i++) { + if (parts[i].isBlank() || parts[i].length() >= MAX_WORD_LENGTH || characterNoisePredicate.test(parts[i])) { + parts[i] = null; + } + else { + length++; + } + } + + String[] ret = new String[length]; + int[] seps = new int[length]; + for (int i = 0, j=0; i < parts.length; i++) { + if (parts[i] != null) { + seps[j] = separators.getQuick(i); + ret[j++] = parts[i]; + } + } + + for (int i = 0; i < ret.length; i++) { + if (ret[i].startsWith("'") && ret[i].length() > 1) { ret[i] = ret[i].substring(1); } + if (ret[i].endsWith("'") && ret[i].length() > 1) { ret[i] = ret[i].substring(0, ret[i].length()-1); } + } + return new WordsAndSeparators(ret, seps); + } + + + public boolean isLegacyMode() { + return legacyMode; + } + public void setLegacyMode(boolean legacyMode) { + this.legacyMode = legacyMode; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/SubjectCounter.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/SubjectCounter.java new file mode 100644 index 00000000..24a71c22 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/SubjectCounter.java @@ -0,0 +1,47 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language.processing; + +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentLanguageData; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.WordRep; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.WordSpan; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.tag.WordSeparator; + +import java.util.*; +import java.util.stream.Collectors; + +public class SubjectCounter { + private final KeywordExtractor keywordExtractor; + + public SubjectCounter(KeywordExtractor keywordExtractor) { + this.keywordExtractor = keywordExtractor; + } + + public List count(DocumentLanguageData dld) { + + Map counts = new HashMap<>(); + for (var sentence : dld.sentences) { + for (WordSpan kw : keywordExtractor.getNames(sentence)) { + if (kw.end + 2 >= sentence.length()) { + continue; + } + if (sentence.separators[kw.end] == WordSeparator.COMMA + || sentence.separators[kw.end + 1] == WordSeparator.COMMA) + break; + + if (("VBZ".equals(sentence.posTags[kw.end]) || "VBP".equals(sentence.posTags[kw.end])) + && ("DT".equals(sentence.posTags[kw.end + 1]) || "RB".equals(sentence.posTags[kw.end]) || sentence.posTags[kw.end].startsWith("VB")) + ) { + counts.merge(new WordRep(sentence, new WordSpan(kw.start, kw.end)), -1, Integer::sum); + } + } + } + + int best = counts.values().stream().mapToInt(Integer::valueOf).min().orElse(0); + + return counts.entrySet().stream().sorted(Map.Entry.comparingByValue()) + .filter(e -> e.getValue()<-2 && e.getValue() wordCount; + + public int totalNumWords() { + int ret = 0; + for (int i = 0; i < sentences.length; i++) { + ret += sentences[i].length(); + } + return ret; + } + + public Stream streamLowerCase() { + return Arrays.stream(sentences).map(sent -> sent.wordsLowerCase).flatMap(Arrays::stream); + } + + public Stream stream() { + return Arrays.stream(sentences).map(sent -> sent.words).flatMap(Arrays::stream); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/DocumentSentence.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/DocumentSentence.java new file mode 100644 index 00000000..690683a8 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/DocumentSentence.java @@ -0,0 +1,88 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language.processing.model; + + +import nu.marginalia.wmsa.edge.crawler.domain.language.WordPatterns; + +import java.lang.ref.SoftReference; +import java.util.BitSet; +import java.util.StringJoiner; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class DocumentSentence { + public final String originalSentence; + public final String[] words; + public final int[] separators; + public final String[] wordsLowerCase; + public final String[] posTags; + public final String[] stemmedWords; + + private final BitSet isStopWord; + + public SoftReference keywords; + + public DocumentSentence(String originalSentence, String[] words, int[] separators, String[] wordsLowerCase, String[] posTags, String[] stemmedWords) { + this.originalSentence = originalSentence; + this.words = words; + this.separators = separators; + this.wordsLowerCase = wordsLowerCase; + this.posTags = posTags; + this.stemmedWords = stemmedWords; + + isStopWord = new BitSet(words.length); + + for (int i = 0; i < words.length; i++) { + if (WordPatterns.isStopWord(words[i])) + isStopWord.set(i); + } + } + + public boolean isStopWord(int idx) { + return isStopWord.get(idx); + } + public void setIsStopWord(int idx, boolean val) { + if (val) + isStopWord.set(idx); + else + isStopWord.clear(); + } + public int length() { + return words.length; + } + + private final static Pattern trailingJunkPattern = Pattern.compile("(^[\"'_*]+|[_*'\"]+$)"); + private final static Pattern joinerPattern = Pattern.compile("[-+.]+"); + + public String constructWordFromSpan(WordSpan span) { + StringJoiner sj = new StringJoiner("_"); + for (int i = span.start; i < span.end; i++) { + sj.add(wordsLowerCase[i]); + } + + return trailingJunkPattern.matcher(sj.toString()).replaceAll(""); + } + + public String constructStemmedWordFromSpan(WordSpan span) { + StringJoiner sj = new StringJoiner("_"); + for (int i = span.start; i < span.end; i++) { + if (includeInStemming(i)) + sj.add(joinerPattern.matcher(stemmedWords[i]).replaceAll("_")); + + } + return sj.toString(); + } + + private boolean includeInStemming(int i) { + if (posTags[i].equals("IN") || posTags[i].equals("TO") || posTags[i].equals("CC") || posTags[i].equals("DT")) { + return false; + } + return true; + } + + + @Override + public String toString() { + return IntStream.range(0, length()).mapToObj(i -> String.format("%s[%s]", words[i], posTags[i])).collect(Collectors.joining(" ")); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/WordRef.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/WordRef.java new file mode 100644 index 00000000..34a6d79a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/WordRef.java @@ -0,0 +1,46 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language.processing.model; + +import lombok.AllArgsConstructor; + +import java.util.Objects; +import java.util.Optional; + +@AllArgsConstructor +public class WordRef { + public final int sentenceIndex; + public final int wordIndex; + + public String getWord(DocumentLanguageData dld) { + return dld.sentences[sentenceIndex].words[wordIndex]; + } + + public String getWordStemmed(DocumentLanguageData dld) { + return dld.sentences[sentenceIndex].stemmedWords[wordIndex]; + } + + public Optional next(DocumentLanguageData dld) { + if (wordIndex + 1 < dld.sentences[sentenceIndex].length()) { + return Optional.of(new WordRef(sentenceIndex, wordIndex+1)); + } + return Optional.empty(); + } + public Optional prev() { + if (wordIndex - 1 >= 0) { + return Optional.of(new WordRef(sentenceIndex, wordIndex-1)); + } + return Optional.empty(); + } + + @Override + public int hashCode() { + return Objects.hash(sentenceIndex, wordIndex); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + WordRef wordRef = (WordRef) o; + return sentenceIndex == wordRef.sentenceIndex && wordIndex == wordRef.wordIndex; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/WordRep.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/WordRep.java new file mode 100644 index 00000000..946545b2 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/WordRep.java @@ -0,0 +1,31 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language.processing.model; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +@AllArgsConstructor @EqualsAndHashCode @Getter +public class WordRep implements Comparable { + public WordRep(DocumentSentence sent, WordSpan span) { + word = sent.constructWordFromSpan(span); + stemmed = sent.constructStemmedWordFromSpan(span); + length = span.end - span.start; + } + public final int length; + public final String word; + public final String stemmed; + + @Override + public int compareTo(@NotNull WordRep o) { + return stemmed.compareTo(o.stemmed); + } + + @Override + public String toString() { + return word; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/WordSpan.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/WordSpan.java new file mode 100644 index 00000000..5ec4f912 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/WordSpan.java @@ -0,0 +1,53 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language.processing.model; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import org.jetbrains.annotations.NotNull; + +@AllArgsConstructor @EqualsAndHashCode +public class WordSpan implements Comparable{ + public final int start; + public final int end; + + public int size() { + return end - start; + } + @Override + public int compareTo(@NotNull WordSpan o) { + return start - o.start; + } + + public boolean overlaps(WordSpan other) { + if (other.start >= start && other.start <= end) return true; + if (other.end >= start && other.end <= end) return true; + if (start >= other.start && start <= other.end) return true; + return false; + } + + public int distance(WordSpan other) { + if (overlaps(other)) { + return 0; + } + if (start < other.start) { + return end - other.start; + } + else { + return other.end - start; + } + + } + + public boolean hasSimilarWords(DocumentSentence s, WordSpan other) { + for (int i = start; i < end; i++) { + for (int j = other.start; j < other.end; j++) { + if (s.stemmedWords[i].equals(s.stemmedWords[j])) + return true; + } + } + return false; + } + + public String toString() { + return String.format("WordSpan[%s,%s]", start, end); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/tag/WordSeparator.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/tag/WordSeparator.java new file mode 100644 index 00000000..cfaa3e9b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/tag/WordSeparator.java @@ -0,0 +1,6 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.tag; + +public final class WordSeparator { + public static final int COMMA = 0; + public static final int SPACE = 1; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/tag/WordTag.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/tag/WordTag.java new file mode 100644 index 00000000..d4a6402a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/model/tag/WordTag.java @@ -0,0 +1,8 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.tag; + +public class WordTag { + public static int UNSET = 0; + public static int STOP_WORD = 1; + public static int NAME = 2; + public static int NOT_NAME = 3; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlFeature.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlFeature.java new file mode 100644 index 00000000..dda2cbf9 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlFeature.java @@ -0,0 +1,28 @@ +package nu.marginalia.wmsa.edge.crawler.domain.processor; + +import java.util.Collection; + +public enum HtmlFeature { + MEDIA(0), + JS(1), + AFFILIATE_LINK(2), + TRACKING(3), + COOKIES(4) + ; + + public int bit; + + HtmlFeature(int bit) { + this.bit = bit; + } + + public static int encode(Collection featuresAll) { + return featuresAll.stream().mapToInt(f -> 1 << f.bit).reduce(0, (l, r) -> (l|r)); + } + public static boolean hasFeature(int value, HtmlFeature feature) { + return (value & (1<< feature.bit)) != 0; + } + public static int addFeature(int value, HtmlFeature feature) { + return (value | (1<< feature.bit)); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlProcessor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlProcessor.java new file mode 100644 index 00000000..cf21f815 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlProcessor.java @@ -0,0 +1,328 @@ +package nu.marginalia.wmsa.edge.crawler.domain.processor; + +//import net.sf.classifier4J.summariser.SimpleSummariser; + +import nu.marginalia.wmsa.edge.crawler.domain.LinkParser; +import nu.marginalia.wmsa.edge.crawler.domain.language.LanguageFilter; +import nu.marginalia.wmsa.edge.crawler.domain.language.WordPatterns; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.DocumentKeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.KeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentSentence; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.*; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +import static nu.marginalia.wmsa.edge.model.crawl.EdgeHtmlStandard.UNKNOWN; + +@Singleton +public class HtmlProcessor { + private static final LanguageFilter languageFilter = new LanguageFilter(); + private static final Logger logger = LoggerFactory.getLogger(HtmlProcessor.class); + + private final DocumentKeywordExtractor documentKeywordExtractor; + private final SentenceExtractor sentenceExtractor; + private final KeywordExtractor keywordExtractor = new KeywordExtractor(); + + private static final Set filthTable = Set.of( + "xxx", "sex", "anal", "sexy", + "bdsm", "fetish", "porn", "camgirls", "dildo", + "gangbang", "buttplug", "orgasm", "vibrator", + "cameltoe", "download", "iso", "botox", "torrent", + "jackpot", "vegas", "casino", "coinbase", "poloniex", + "myetherwallet", "ethereum", "binance", "bitcoin", + "litecoin", "seo", "serp" + + ); + + private static final LinkParser linkParser = new LinkParser(); + + @Inject + public HtmlProcessor(DocumentKeywordExtractor documentKeywordExtractor, SentenceExtractor sentenceExtractor) { + this.documentKeywordExtractor = documentKeywordExtractor; + this.sentenceExtractor = sentenceExtractor; + } + + public EdgePageContent processHtmlPage(EdgeRawPageContents rawPageContent, Document parsed) { + + var parsed2 = parsed.clone(); + final String text = parsed.getElementsByTag("body").text(); + + if (languageFilter.isBlockedUnicodeRange(text)) { + logger.debug("Skipping {} , foreign unicode ranges in excessive presence", rawPageContent.url); + return null; + } + + int rawLength = rawPageContent.data.length(); + int scriptTags = getScriptPenalty(parsed); + int textLength = text.length(); + + EdgeHtmlStandard htmlStandard = HtmlStandardExtractor.parseDocType(parsed.documentType()); + if (UNKNOWN.equals(htmlStandard)) { + htmlStandard = HtmlStandardExtractor.sniffHtmlStandard(parsed); + } + + var dld = sentenceExtractor.extractSentences(parsed.clone()); + var keywords = documentKeywordExtractor.extractKeywords(dld); + + var featureSet = getFeatureSet(parsed, scriptTags, rawPageContent.hasCookies); + addTags(keywords, htmlStandard, rawPageContent.url, featureSet); + + final String title = Optional.ofNullable(parsed.getElementsByTag("title")) + .map(Elements::first) + .map(Element::text) + .or(() -> Optional.ofNullable(parsed.getElementsByTag("h1").first()).map(Element::text)) + .or(() -> Optional.ofNullable(parsed.getElementsByTag("h2").first()).map(Element::text)) + .or(() -> Optional.ofNullable(parsed.getElementsByTag("h3").first()).map(Element::text)) + .or(() -> { + if (dld.sentences.length > 0) return Optional.of(dld.sentences[0].originalSentence); + return Optional.empty(); + }) + .map(str -> StringUtils.truncate(str, 128)) + .orElseGet(rawPageContent.url::toString); + + int wc = dld.totalNumWords(); + + var bodyWords = keywords.get(IndexBlock.Words); + if (wc > 100) { + double languageAgreement = languageFilter.dictionaryAgreement(dld); + if (languageAgreement < 0.01 || (wc > 200 && languageAgreement < 0.05) ) { + logger.debug("Skipping {} , poor language agreement {}", rawPageContent.url, languageAgreement); + return null; + } + } + + double smutCoefficient = bodyWords.words.stream().filter(filthTable::contains).count(); + + Set summaryKeywords = new HashSet<>(); + + summaryKeywords.addAll(keywords.get(IndexBlock.Low).words); + summaryKeywords.addAll(keywords.get(IndexBlock.Middle).words); + summaryKeywords.addAll(keywords.get(IndexBlock.Top).words); + summaryKeywords.addAll(keywords.get(IndexBlock.Title).words); + + var description = extractSummary(parsed2, summaryKeywords) + .or(() -> getOgDescription(parsed2)) + .or(() -> getMetaDescription(parsed2)); + + int totalWords = Arrays.stream(dld.sentences).mapToInt(DocumentSentence::length).sum(); + + final var metadata = new EdgePageMetadata( + HtmlFeature.encode(featureSet), scriptTags, rawLength, textLength, wc, + title, description.orElse(""), smutCoefficient, + totalWords, htmlStandard); + + Map> linkWords = extractLinkWords(keywords, rawPageContent.getUrl(), parsed); + + return new EdgePageContent(rawPageContent.url, keywords, linkWords, metadata, rawPageContent.getData().hashCode(), + rawPageContent.ip); + } + + List trackers = List.of("adform.net", + "connect.facebook", + "googletagmanager.com", + "googlesyndication.com", + "google.com", + "twitter.com", + "smartadserver.com", + "doubleclick.com", + "2mdn.com", + "dmtry.com", + "bing.com", + "msn.com", + "amazon-adsystem.com", + "alexametrics.com", + "rubiconproject.com", + "chango.com", + "d5nxst8fruw4z.cloudfront.net", + "d31qbv1cthcecs.cloudfront.net", + "linkedin.com"); + + private Set getFeatureSet(Document parsed, int scriptTags, boolean cookies) { + Set features = new HashSet<>(); + + if (scriptTags > 0) { + features.add(HtmlFeature.JS); + } + if (!parsed.getElementsByTag("object").isEmpty() + || !parsed.getElementsByTag("audio").isEmpty() + || !parsed.getElementsByTag("video").isEmpty()) { + features.add(HtmlFeature.MEDIA); + } + if (parsed.getElementsByTag("script").stream() + .filter(tag -> tag.attr("src") != null) + .anyMatch(tag -> trackers.stream().anyMatch(tracker -> tag.attr("src").contains(tracker)))) { + features.add(HtmlFeature.TRACKING); + } + if (parsed.getElementsByTag("script").html().contains("google-analytics.com")) { + features.add(HtmlFeature.TRACKING); + } + if (parsed.getElementsByTag("a").stream().map(e -> e.attr("href")) + .filter(Objects::nonNull) + .map(String::toLowerCase) + .anyMatch(href -> + href.contains("amzn.to/") || href.contains("amazon.com/"))) { + features.add(HtmlFeature.AFFILIATE_LINK); + } + if (cookies) { + features.add(HtmlFeature.COOKIES); + } + + return features; + } + + private void addTags(EdgePageWordSet wordSet, EdgeHtmlStandard htmlStandard, EdgeUrl url, Set features) { + List tagWords = new ArrayList<>(); + tagWords.add("format:"+htmlStandard.toString().toLowerCase()); + tagWords.add("site:"+url.domain.toString().toLowerCase()); + tagWords.add("proto:"+url.proto.toLowerCase()); + tagWords.add("js:" + Boolean.toString(features.contains(HtmlFeature.JS)).toLowerCase()); + if (features.contains(HtmlFeature.MEDIA)) { + tagWords.add("special:media"); + } + if (features.contains(HtmlFeature.TRACKING)) { + tagWords.add("special:tracking"); + } + if (features.contains(HtmlFeature.AFFILIATE_LINK)) { + tagWords.add("special:affiliate"); + } + if (features.contains(HtmlFeature.COOKIES)) { + tagWords.add("special:cookies"); + } + wordSet.append(IndexBlock.Meta, tagWords); + wordSet.append(IndexBlock.Words, tagWords); + } + + private int getScriptPenalty(Document parsed) { + var scriptTags = parsed.getElementsByTag("script"); + String scriptText = scriptTags.html(); + int badScript = 0; + if (scriptText.contains(".createElement(")) { + badScript = 1; + } + + double scriptPenalty = 0; + for (var tag : scriptTags) { + String srcTag = tag.attr("src"); + if (srcTag == null) { + scriptPenalty += 1; + } + else if (srcTag.contains("wp-content") || srcTag.contains("wp-includes") || srcTag.contains("jquery")) { + scriptPenalty += 0.49; + } + else { + scriptPenalty += 1; + } + + } + return (int)(scriptPenalty + badScript + (scriptText.length())/1000.); + } + + private Map> extractLinkWords(EdgePageWordSet keywords, EdgeUrl pageUrl, Document parsed) { + + + List> urls = new ArrayList<>(); + + Set linkKeywords = new HashSet<>(); + + Map> linkTextWords = new ConcurrentHashMap<>(); + + for (var tag : parsed.getElementsByTag("a")) { + if (!tag.hasAttr("href")) { + continue; + } + if (urls.size() > 100) { + break; + } + + var linkOpt = linkParser.parseLink(pageUrl, tag); + if (linkOpt.isEmpty()) + continue; + + var link = linkOpt.get(); + + urls.add(Pair.of(link, tag.text())); + + if (!Objects.equals(link.domain.domain, pageUrl.domain.domain) + && linkKeywords.size() <= 25) + { + linkKeywords.add("links:" + link.domain.domain); + } + + Set words = new HashSet<>(); + + for (var sent : sentenceExtractor.extractSentencesFromString(tag.text())) { + for (var keyword : keywordExtractor.getWordsFromSentence(sent)) { + words.add(sent.constructWordFromSpan(keyword)); + } + } + + linkTextWords.compute(link, (k, set) -> { + if (set == null) return words; + else { set.addAll(words); return set; } + }); + + } + + keywords.get(IndexBlock.Meta).addAll(linkKeywords); + + if (WordPatterns.wordQualitiesPredicate.test(pageUrl.domain.domain.toLowerCase())) { + keywords.get(IndexBlock.Link).addJust(pageUrl.domain.domain.toLowerCase()); + } + + return linkTextWords; + } + + public Optional extractSummary(Document parsed, Set keywords) { + var cleanDoc = parsed.clone(); + cleanDoc.getElementsByTag("nav").remove(); + cleanDoc.getElementsByTag("header").remove(); + Optional.ofNullable(cleanDoc.getElementById("header")).ifPresent(Element::remove); + cleanDoc.getElementsByClass("header").remove(); + cleanDoc.getElementsByClass("nav").remove(); + + return extractSummaryRaw(cleanDoc) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .filter(s -> !s.isBlank() && s.length() > 20) + .map(s -> s.substring(0, Math.min(500, s.length()))) + ; + } + + private Optional extractSummaryRaw(Document parsed) { + StringBuilder content = new StringBuilder(); + + parsed.getElementsByTag("p").forEach( + elem -> { + if (elem.text().length() > elem.html().length()/2) { + content.append(elem.text()); + } + } + ); + + if (content.length() > 10) { + return Optional.of(content.toString()); + } + return Optional.empty(); + } + + private Optional getMetaDescription(Document parsed) { + return Optional.ofNullable(parsed.select("meta[name=description]")).map(tag -> tag.attr("content")).filter(s -> !s.isBlank()); + } + private Optional getOgDescription(Document parsed) { + return Optional.ofNullable(parsed.select("meta[name=og:description]")).map(tag -> tag.attr("content")).filter(s -> !s.isBlank()); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlStandardExtractor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlStandardExtractor.java new file mode 100644 index 00000000..862293b5 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlStandardExtractor.java @@ -0,0 +1,84 @@ +package nu.marginalia.wmsa.edge.crawler.domain.processor; + +import com.google.common.base.Strings; +import nu.marginalia.wmsa.edge.model.crawl.EdgeHtmlStandard; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.DocumentType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static nu.marginalia.wmsa.edge.model.crawl.EdgeHtmlStandard.*; + +public class HtmlStandardExtractor { + + + private static final Logger logger = LoggerFactory.getLogger(HtmlStandardExtractor.class); + + public static EdgeHtmlStandard parseDocType(DocumentType docType) { + if (null == docType) { + return UNKNOWN; + } + String publicId = docType.publicId(); + if (Strings.isNullOrEmpty(publicId)) + return HTML5; + + publicId = publicId.toUpperCase(); + if (publicId.startsWith("-//SOFTQUAD SOFTWARE//DTD") && publicId.contains("HTML 4")) { + return HTML4; + } + if (publicId.startsWith("-//SOFTQUAD SOFTWARE//DTD") && publicId.contains("HTML 3")) { + return HTML123; + } + if (publicId.startsWith("-//INTERNET/RFC XXXX//EN")) + return HTML123; + if (publicId.startsWith("-//NETSCAPE COMM. CORP")) + return HTML123; + if (publicId.startsWith("-//SQ//DTD HTML 2")) + return HTML123; + if (publicId.startsWith("-//SOFTQUAD//DTD HTML 2")) + return HTML123; + if (publicId.startsWith("-//W3O//DTD W3 HTML 2")) + return HTML123; + if (publicId.startsWith("-//IETF//DTD HTML 2")) + return HTML123; + if (publicId.startsWith("-//IETF//DTD HTML//EN")) + return HTML123; + if (publicId.startsWith("-/W3C//DTD HTML 3")) + return HTML123; + if (publicId.startsWith("-/W3C/DTD HTML 3")) + return HTML123; + if (publicId.startsWith("-//IETF//DTD HTML 3")) + return HTML123; + if (publicId.startsWith("-//W3C//DTD XHTML")) + return XHTML; + if (publicId.startsWith("ISO/IEC 15445:2000//DTD")) + return XHTML; + if (publicId.startsWith("-//W3C//DTD HTML")) + return HTML4; + + logger.debug("Unknown publicID standard {}", publicId); + return UNKNOWN; + } + + public static EdgeHtmlStandard sniffHtmlStandard(Document parsed) { + int html4Attributes = 0; + int html5Attributes = 0; + + if (parsed.getElementsByTag("article").size() > 0) html5Attributes++; + if (parsed.getElementsByTag("header").size() > 0) html5Attributes++; + if (parsed.getElementsByTag("footer").size() > 0) html5Attributes++; + if (parsed.getElementsByTag("video").size() > 0) html5Attributes++; + if (parsed.getElementsByTag("audio").size() > 0) html5Attributes++; + if (parsed.getElementsByTag("canvas").size() > 0) html5Attributes++; + if (parsed.getElementsByTag("link").stream().anyMatch(elem -> "stylesheet".equals(elem.attr("rel")))) { + html4Attributes++; + } + if (html5Attributes > 0) { + return HTML5; + } + if (html4Attributes > 0) { + return HTML4; + } + return HTML123; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlSummarizer.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlSummarizer.java new file mode 100644 index 00000000..36e41573 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlSummarizer.java @@ -0,0 +1,139 @@ +package nu.marginalia.wmsa.edge.crawler.domain.processor; + +import gnu.trove.list.array.TIntArrayList; +import it.unimi.dsi.fastutil.ints.IntArrays; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +import java.util.*; +import java.util.regex.Pattern; + +public class HtmlSummarizer { + private static Pattern extendedJunk = Pattern.compile("[^a-zA-Z0-9]{4,}"); + + private static final int MAX_CONSIDERABLE_SENTENCES = 200; + private static final int MIN_SUMMARY_LENGTH = 20; + private static final int MIN_TAG_LENGTH = 25; + private static final int MAX_SUMMARY_LENGTH = 255; + + private final SentenceExtractor sentenceExtractor; + + + public HtmlSummarizer(SentenceExtractor sentenceExtractor) { + this.sentenceExtractor = sentenceExtractor; + } + + public Optional getSummary(Document parsed, Set keywords) { + List candidates = extractCandidates(parsed); + TIntArrayList scores = new TIntArrayList(candidates.size()); + + for (String sentence : candidates) { + scores.add(calculateScore(sentence, keywords)); + } + + String summary = constructSummary(candidates, scores); + if (summary.isBlank() || summary.length() < MIN_SUMMARY_LENGTH) { + return Optional.empty(); + } + return Optional.of(summary); + } + + private String constructSummary(List candidates, TIntArrayList scores) { + int[] scoresReversed = getIndicesByScore(scores, candidates); + TIntArrayList includedParts = new TIntArrayList(); + + int length = 0; + for (int i = 0; length < MAX_SUMMARY_LENGTH && i < scoresReversed.length; i++) { + String sentence = candidates.get(scoresReversed[i]); + length += sentence.length(); + includedParts.add(scoresReversed[i]); + } + includedParts.sort(); + + StringBuilder summary = new StringBuilder(); + includedParts.forEach(i -> { + var candidate = candidates.get(i).trim(); + + summary.append(candidate); + if (endsInLetterOrNumber(candidate.trim())) { + summary.append(". "); + } + else if (!candidate.isEmpty() && !Character.isSpaceChar(candidate.charAt(candidate.length()-1))) { + summary.append(" "); + } + return true; + }); + + return summary.toString(); + } + + private boolean endsInLetterOrNumber(String candidate) { + if (candidate.isBlank()) return false; + char lastChar = candidate.charAt(candidate.length()-1); + + return (Character.isAlphabetic(lastChar) || Character.isDigit(lastChar)); + } + + private int[] getIndicesByScore(TIntArrayList scores, List candidates) { + int[] scoresReversed = new int[scores.size()]; + for (int i = 0; i { + int d = scores.get(b) - scores.get(a); + if (d == 0) { + return candidates.get(a).length() - candidates.get(b).length(); + } + return d; + }); + return scoresReversed; + } + + private List extractCandidates(Document parsed) { + var clone = parsed.clone(); + clone.getElementsByTag("br").remove(); + + List ret = new ArrayList<>(); + + for (var elem : clone.select("p,div,section,article")) { + if (isCandidate(elem)) { + ret.add(cleanText(elem.text())); + } + if (ret.size() > MAX_CONSIDERABLE_SENTENCES) { + break; + } + }; + + return ret; + } + private String cleanText(String text) { + return extendedJunk.matcher(text).replaceAll(" "); + + } + + private int calculateScore(String sentence, Set keywords) { + int score = 0; + + final var data = sentenceExtractor.extractSentencesFromString(sentence); + + for (var s : data) { + for (var word : s.wordsLowerCase) { + if (keywords.contains(word)) { + score++; + } + } + } + + return score; + } + + private boolean isCandidate(Element elem) { + if (elem.html().length() < MIN_TAG_LENGTH) { + return false; + } + + if (elem.childrenSize() > 3) + return false; + + return elem.text().length() > 0.75*elem.html().length(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlTagCleaner.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlTagCleaner.java new file mode 100644 index 00000000..98fa6918 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlTagCleaner.java @@ -0,0 +1,40 @@ +package nu.marginalia.wmsa.edge.crawler.domain.processor; + +import org.jsoup.nodes.Document; +import org.jsoup.nodes.TextNode; + +import java.util.regex.Pattern; + +public class HtmlTagCleaner { + public final int MAX_CODE_TAG_LENGTH = 32; + public final Pattern codeTagJunkPattern = Pattern.compile("(\\.|<|>|<|>|\\([^)]*\\)[;]?$)"); + + public void clean(Document doc) { + cleanCodeTags(doc); + + doc.select("nav,form,input,code,body>title").remove(); + + // Create "sentences" out of elements that sometimes lack a period at the end to help + // NLP work better + doc.select("li,h1,h2,h3,h4,h5,h6,td,th,p,div,title").forEach(e -> e.appendText(". ")); + doc.select("br,hr").forEach(e -> e.prependText(". ")); + } + + private void cleanCodeTags(Document doc) { + for (var codeTag : doc.getElementsByTag("code")) { + var text = codeTag.text(); + + if (text.length() <= MAX_CODE_TAG_LENGTH) { + codeTag.replaceWith(new TextNode(trimCodeTagContents(text))); + } + else { + codeTag.remove(); + } + + } + } + + private String trimCodeTagContents(String text) { + return codeTagJunkPattern.matcher(text).replaceAll(" "); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/PlainTextProcessor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/PlainTextProcessor.java new file mode 100644 index 00000000..16721708 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/domain/processor/PlainTextProcessor.java @@ -0,0 +1,74 @@ +package nu.marginalia.wmsa.edge.crawler.domain.processor; + +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.DocumentKeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.EdgeHtmlStandard; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageContent; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageMetadata; +import nu.marginalia.wmsa.edge.model.crawl.EdgeRawPageContents; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; + +import javax.inject.Inject; +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; +import java.util.stream.Collectors; + +public class PlainTextProcessor { + + private final DocumentKeywordExtractor keywordExtractor; + private final SentenceExtractor sentenceExtractor; + @Inject + public PlainTextProcessor(DocumentKeywordExtractor keywordExtractor, SentenceExtractor sentenceExtractor) { + this.keywordExtractor = keywordExtractor; + this.sentenceExtractor = sentenceExtractor; + } + + public Optional parsePlainText(EdgeRawPageContents rawContents) { + if (!isFileEndingAllowed(rawContents.url)) { + return Optional.empty(); + } + final String textData = rawContents.getData(); + final String[] textLines = textData.substring(0, Math.min(5000, textData.length())).split("\n"); + + var dld = sentenceExtractor.extractSentences(textData); + var keywords = keywordExtractor.extractKeywords(dld); + keywords.get(IndexBlock.Meta).addJust("format:plain"); + keywords.get(IndexBlock.Words).addJust("format:plain"); + + final var metadata = new EdgePageMetadata(0, 0, textData.length(), + textData.length(), dld.totalNumWords(), + rawContents.url.fileName(), + getDescription(textLines), 0., 1, + EdgeHtmlStandard.PLAIN); + + return Optional.of(new EdgePageContent(rawContents.url, + keywords, + Collections.emptyMap(), + metadata, + rawContents.getData().hashCode(), + rawContents.ip)); + } + + private boolean isFileEndingAllowed(EdgeUrl url) { + String urlString = url.toString().toLowerCase(); + if (urlString.endsWith(".txt")) { + return true; + } + if (urlString.endsWith(".md")) { + return true; + } + if (urlString.endsWith(".gmi")) { + return true; + } + return false; + } + + private String getDescription(String[] textLines) { + return StringUtils.truncate(Arrays.stream(textLines).filter(Strings::isNotBlank).limit(10).collect(Collectors.joining("\n")), 200); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/ContentTypeParser.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/ContentTypeParser.java new file mode 100644 index 00000000..b86fa118 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/ContentTypeParser.java @@ -0,0 +1,77 @@ +package nu.marginalia.wmsa.edge.crawler.fetcher; + +import crawlercommons.mimetypes.MimeTypeDetector; +import nu.marginalia.wmsa.edge.model.crawl.EdgeContentType; +import org.jsoup.Jsoup; + +import java.util.Arrays; +import java.util.Optional; + +public class ContentTypeParser { + + static MimeTypeDetector mimeTypeDetector = new MimeTypeDetector(); + + public static EdgeContentType parse(String contentType, byte[] data) { + return getContentTypeFromContentTypeString(contentType) + .or(() -> getContentTypeStringFromTag(data)) + .orElseGet(() -> { + Optional charset = getCharsetFromTag(data); + return new EdgeContentType( + Optional.ofNullable(contentType) + .or(() -> Optional.ofNullable(mimeTypeDetector.detect(data))) + .orElseGet(() -> ContentTypeParser.shittyMimeSniffer(data)), charset.orElse("ISO_8859_1")); + }); + } + + private static Optional getContentTypeFromContentTypeString(String contentType) { + if (contentType != null && contentType.contains(";")) { + var parts = contentType.split(";"); + var content = parts[0].trim(); + var extra = parts[1].trim(); + if (extra.startsWith("charset=")) { + return Optional.of(new EdgeContentType(content, extra.substring("charset=".length()))); + } + } + return Optional.empty(); + } + + private static String shittyMimeSniffer(byte[] data) { + + for (int i = 0; i < data.length && i < 128; i++) { + if (data[i] < 32) { + return "application/binary"; + } + } + + String startStr = new String(Arrays.copyOf(data, Math.min(128, data.length))).trim().toLowerCase(); + if (startStr.contains(" getContentTypeStringFromTag(byte[] data) { + String header = new String(Arrays.copyOf(data, Math.min(1024, data.length))); + var doc = Jsoup.parse(header); + for (var metaTag : doc.getElementsByTag("meta")) { + if ("content-type".equalsIgnoreCase(metaTag.attr("http-equiv"))) { + return getContentTypeFromContentTypeString(metaTag.attr("content")); + } + } + return Optional.empty(); + } + + private static Optional getCharsetFromTag(byte[] data) { + String header = new String(Arrays.copyOf(data, Math.min(1024, data.length))); + var doc = Jsoup.parse(header); + for (var metaTag : doc.getElementsByTag("meta")) { + if (metaTag.hasAttr("charset")) { + return Optional.of(metaTag.attr("charset")); + } + } + return Optional.empty(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/Cookies.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/Cookies.java new file mode 100644 index 00000000..0264259f --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/Cookies.java @@ -0,0 +1,44 @@ +package nu.marginalia.wmsa.edge.crawler.fetcher; + +import okhttp3.Cookie; +import okhttp3.CookieJar; +import okhttp3.HttpUrl; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; + +public class Cookies { + final ThreadLocal>> cookieJar = ThreadLocal.withInitial(ConcurrentHashMap::new); + + public CookieJar getJar() { + return new CookieJar() { + + @Override + public void saveFromResponse(HttpUrl url, List cookies) { + if (!cookies.isEmpty()) { + cookieJar.get().put(url, cookies); + } + } + + @Override + public List loadForRequest(HttpUrl url) { + return cookieJar.get().getOrDefault(url, Collections.emptyList()); + } + }; + } + + public void clear() { + cookieJar.get().clear(); + } + + public boolean hasCookies() { + return !cookieJar.get().isEmpty(); + } + + public List getCookies() { + return cookieJar.get().values().stream().flatMap(List::stream).map(Cookie::toString).toList(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/HttpFetcher.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/HttpFetcher.java new file mode 100644 index 00000000..1770e6d9 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/HttpFetcher.java @@ -0,0 +1,256 @@ +package nu.marginalia.wmsa.edge.crawler.fetcher; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import lombok.ToString; +import nu.marginalia.wmsa.client.exception.NetworkException; +import nu.marginalia.wmsa.edge.crawler.domain.LinkParser; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.crawl.EdgeRawPageContents; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.apache.commons.io.input.BOMInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.X509TrustManager; +import java.io.IOException; +import java.net.InetAddress; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.zip.GZIPInputStream; + +public class HttpFetcher { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final String userAgent; + private final int maxFetchSize = 1024*512; + private Cookies cookies = new Cookies(); + + private final LinkParser linkParser = new LinkParser(); + + public void setAllowAllContentTypes(boolean allowAllContentTypes) { + this.allowAllContentTypes = allowAllContentTypes; + } + + private boolean allowAllContentTypes = false; + + private final OkHttpClient client = createClient(); + + public enum FetchResultState { + OK, + REDIRECT, + ERROR; + }; + + @AllArgsConstructor @ToString + public static class FetchResult { + public final FetchResultState state; + public final EdgeDomain domain; + + public boolean ok() { + return state == FetchResultState.OK; + } + }; + + @SneakyThrows + private OkHttpClient createClient() { + return new OkHttpClient.Builder() + .sslSocketFactory(NoSecuritySSL.buildSocketFactory(), (X509TrustManager) NoSecuritySSL.trustAllCerts[0]) + .hostnameVerifier(NoSecuritySSL.buildHostnameVerifyer()) + .cookieJar(cookies.getJar()) + .followRedirects(true) + .followSslRedirects(true) + .connectTimeout(8, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .build(); + } + + public boolean hasCookies() { + return cookies.hasCookies(); + } + + public void clearCookies() { + cookies.clear(); + } + + @Inject + public HttpFetcher(@Named("user-agent") String userAgent) { + this.userAgent = userAgent; + } + + @SneakyThrows + public FetchResult probeDomain(EdgeUrl url) { + var head = new Request.Builder().head().addHeader("User-agent", userAgent) + .url(new EdgeUrl(url.proto, url.domain, url.port, "/").toString()) + .build(); + + var call = client.newCall(head); + + try (var rsp = call.execute()) { + var requestUrl = rsp.request().url().toString(); + EdgeDomain requestDomain = new EdgeUrl(requestUrl).domain; + + if (!Objects.equals(requestDomain, url.domain)) { + return new FetchResult(FetchResultState.REDIRECT, requestDomain); + } + return new FetchResult(FetchResultState.OK, requestDomain); + } + catch (Exception ex) { + return new FetchResult(FetchResultState.ERROR, url.domain); + } + + + } + + @SneakyThrows + public EdgeRawPageContents fetchContent(EdgeUrl url) { + if (isUrlLikeBinary(url) && !probeContentType(url)) { + return null; + } + + var get = new Request.Builder().get().addHeader("User-agent", userAgent) + .url(url.toString()) + .addHeader("Accept-Encoding", "gzip") + .build(); + + var call = client.newCall(get); + + try (var rsp = call.execute()) { + if (rsp.code() >= 400) { + throw new NetworkException("Bad status " + rsp.code()); + } + return extractBody(url, rsp); + } + } + + private final Predicate probableHtmlPattern = Pattern.compile("^.*\\.(htm|html|php|txt)(\\?.*)?$").asPredicate(); + private final Predicate probableBinaryPattern = Pattern.compile("^.*\\.[a-z]+$").asPredicate(); + + public boolean isUrlLikeBinary(EdgeUrl url) { + String urlString = url.toString().toLowerCase(); + + return (!probableHtmlPattern.test(urlString) && probableBinaryPattern.test(urlString)); + } + + @SneakyThrows + private boolean probeContentType(EdgeUrl url) { + logger.debug("Probing suspected binary {}", url); + + var head = new Request.Builder().get().addHeader("User-agent", userAgent) + .url(url.toString()) + .addHeader("Accept-Encoding", "gzip") + .build(); + + var call = client.newCall(head); + + try (var rsp = call.execute()) { + if (rsp.code() >= 400) { + throw new NetworkException("Bad status " + rsp.code()); + } + var contentTypeHeader = rsp.header("Content-type"); + if (contentTypeHeader != null && !isAllowableContentType(contentTypeHeader)) { + return false; + } + } + + return true; + } + + @SneakyThrows + private EdgeRawPageContents extractBody(EdgeUrl url, Response response) { + try { + var body = response.body(); + if (null == body) { + throw new NetworkException("No body in response"); + } + + var byteStream = body.byteStream(); + if (null == byteStream) { + throw new NetworkException("No body in response"); + } + if ("gzip".equals(response.header("Content-encoding"))) { + byteStream = new GZIPInputStream(byteStream); + } + byteStream = new BOMInputStream(byteStream); + + var contentTypeHeader = response.header("Content-type"); + if (contentTypeHeader != null && !isAllowableContentType(contentTypeHeader)) { + throw new BadContentType(contentTypeHeader); + } + + byte[] data = byteStream.readNBytes(maxFetchSize); + + var contentType = ContentTypeParser.parse(contentTypeHeader, data); + if (!isAllowableContentType(contentType.contentType)) { + throw new BadContentType(contentType.contentType); + } + + if ("Shift_JIS".equalsIgnoreCase(contentType.charset)) { + throw new BadContentType(contentType.contentType); + } + + var strData = new String(data, Charset.forName(contentType.charset)); + + return new EdgeRawPageContents(url, + getRedirectUrl(url, response), + strData, + contentType, + InetAddress.getByName(url.domain.getAddress()).getHostAddress(), + hasCookies(), + LocalDateTime.now().toString()); + } + catch (IOException ex) { + throw new NetworkException(ex); + } + } + + private EdgeUrl getRedirectUrl(EdgeUrl url, Response response) throws URISyntaxException { + + final String canonicalHeader = response.header("rel=canonical"); + if (null != canonicalHeader) { + var ret = linkParser.parseLink(url, canonicalHeader); + return ret.orElse(url); + } + + var responseUrl = new EdgeUrl(response.request().url().toString()); + if (!responseUrl.equals(url)) { + return new EdgeUrl(response.request().url().toString()); + } + + return url; + } + + private boolean isAllowableContentType(String contentType) { + return allowAllContentTypes || contentType.startsWith("text") + || contentType.startsWith("application/xhtml") + || contentType.startsWith("application/xml") + || contentType.startsWith("application/atom+xml") + || contentType.startsWith("application/rss+xml") + || contentType.startsWith("application/x-rss+xml") + || contentType.startsWith("application/rdf+xml") + || contentType.startsWith("x-rss+xml"); + } + + + public static class BadContentType extends RuntimeException { + public BadContentType(String type) { + super(type); + } + + @Override + public Throwable fillInStackTrace() { + return this; + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/HttpRedirectResolver.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/HttpRedirectResolver.java new file mode 100644 index 00000000..44def33f --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/HttpRedirectResolver.java @@ -0,0 +1,105 @@ +package nu.marginalia.wmsa.edge.crawler.fetcher; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import io.reactivex.rxjava3.core.Observable; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.client.exception.NetworkException; +import nu.marginalia.wmsa.edge.crawler.domain.LinkParser; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import okhttp3.Call; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.X509TrustManager; +import java.util.concurrent.TimeUnit; + +@Singleton +public class HttpRedirectResolver { + private static final LinkParser linkParser = new LinkParser(); + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final String userAgent; + private Cookies cookies = new Cookies(); + + private final OkHttpClient client = createClient(); + + @SneakyThrows + private OkHttpClient createClient() { + + return new OkHttpClient.Builder() + .sslSocketFactory(NoSecuritySSL.buildSocketFactory(), (X509TrustManager) NoSecuritySSL.trustAllCerts[0]) + .hostnameVerifier(NoSecuritySSL.buildHostnameVerifyer()) + .cookieJar(cookies.getJar()) + .followRedirects(false) + .followSslRedirects(false) + .connectTimeout(8, TimeUnit.SECONDS) + .build(); + } + + @Inject + public HttpRedirectResolver(@Named("user-agent") String userAgent) { + this.userAgent = userAgent; + } + + @SneakyThrows + public Observable probe(EdgeUrl url) { + return probe(url, 0); + } + + private Observable probe(EdgeUrl url, int depth) { + if (depth > 10) { + return Observable.error(new IllegalStateException("Too many redirects")); + } + if (!url.proto.toLowerCase().startsWith("http")) { + return Observable.empty(); + } + var head = new Request.Builder().get().addHeader("User-agent", userAgent) + .url(url.toString()) + .addHeader("Accept-Encoding", "gzip") + .build(); + + return Observable.just(client.newCall(head)) + .map(Call::execute) + .flatMap(data -> resolveRedirects(depth, url, data)) + .timeout(10, TimeUnit.SECONDS); + } + + @SneakyThrows + private Observable resolveRedirects(int depth, EdgeUrl url, Response response) { + int code = response.code(); + response.close(); + + if (code < 300) { + return Observable.just(url); + } + if (code < 309) { + String newUrl = response.header("Location"); + return Observable.fromOptional(linkParser.parseLink(url, newUrl)) + .flatMap(u -> probe(u, depth + 1)); + } + if (code >= 400) { + return Observable.just(url); + } + return Observable.error(new IllegalStateException("HttpStatusCode " + code)); + } + + + private boolean failOnBadStatus(Response response) { + if (response.code() >= 400) { + response.close(); + throw new NetworkException("Bad status " + response.code()); + } + return true; + }; + + public static class BadContentType extends RuntimeException { + public BadContentType(String type) { + super(type); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/NoSecuritySSL.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/NoSecuritySSL.java new file mode 100644 index 00000000..ba70f868 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/fetcher/NoSecuritySSL.java @@ -0,0 +1,44 @@ +package nu.marginalia.wmsa.edge.crawler.fetcher; + +import lombok.SneakyThrows; + +import javax.net.ssl.*; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +public class NoSecuritySSL { + + // Create a trust manager that does not validate certificate chains + public static final TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + @Override + public void checkClientTrusted(java.security.cert.X509Certificate[] chain, + String authType) throws CertificateException { + } + + @Override + public void checkServerTrusted(java.security.cert.X509Certificate[] chain, + String authType) throws CertificateException { + } + + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } + }; + + + @SneakyThrows + public static SSLSocketFactory buildSocketFactory() { + // Install the all-trusting trust manager + final SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); + // Create an ssl socket factory with our all-trusting manager + return sslContext.getSocketFactory(); + } + + public static HostnameVerifier buildHostnameVerifyer() { + return (hn, session) -> true; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/CrawlerDiscoverWorker.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/CrawlerDiscoverWorker.java new file mode 100644 index 00000000..509706eb --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/CrawlerDiscoverWorker.java @@ -0,0 +1,131 @@ +package nu.marginalia.wmsa.edge.crawler.worker; + +import io.reactivex.rxjava3.core.Observable; +import nu.marginalia.wmsa.edge.crawler.domain.DomainCrawlerFactory; +import nu.marginalia.wmsa.edge.crawler.fetcher.HttpRedirectResolver; +import nu.marginalia.wmsa.edge.crawler.worker.facade.TaskProvider; +import nu.marginalia.wmsa.edge.crawler.worker.results.DomainAliasResult; +import nu.marginalia.wmsa.edge.crawler.worker.results.DomainCrawlerWorkerResults; +import nu.marginalia.wmsa.edge.crawler.worker.results.InvalidTaskResult; +import nu.marginalia.wmsa.edge.crawler.worker.results.WorkerResults; +import nu.marginalia.wmsa.edge.model.crawl.EdgeIndexTask; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.LinkedBlockingQueue; + +public class CrawlerDiscoverWorker implements Worker { + + private HttpRedirectResolver redirectResolver; + private TaskProvider taskProvider; + private final DomainCrawlerFactory domainCrawlerFactory; + private final IpBlockList blockList; + private final LinkedBlockingQueue queue; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + public CrawlerDiscoverWorker( + DomainCrawlerFactory domainCrawlerFactory, TaskProvider taskProvider, HttpRedirectResolver redirectResolver, + IpBlockList blockList, LinkedBlockingQueue queue) + { + this.redirectResolver = redirectResolver; + this.taskProvider = taskProvider; + this.domainCrawlerFactory = domainCrawlerFactory; + this.blockList = blockList; + this.queue = queue; + } + + @Override + public void runCycle() throws InterruptedException { + + var ingress = taskProvider.getDiscoverTask(); + + + if (ingress.isEmpty()) { + wmsa_edge_crawler_idle_worker.inc(); + Thread.sleep(1000); + return; + } + + try { + if (ingress.rank > 0.25 && !blockList.isAllowed(ingress.domain) + ) { + logger.info("{} IP-blacklisted", ingress.domain); + queue.put(new InvalidTaskResult(ingress.domain, "IP blacklisted")); + return; + } + + Optional results + = resolveRedirects(ingress); + if (results.isPresent()) { + queue.put(results.get()); + return; + } + + long start = System.currentTimeMillis(); + + var dc = domainCrawlerFactory.domainCrawler(ingress); + var res = dc.crawl(); + + wmsa_edge_crawler_thread_run_times.observe(System.currentTimeMillis() - start); + + queue.put(new DomainCrawlerWorkerResults(res)); + } + catch (RuntimeException ex) { + logger.warn("Leaking {}", ingress.domain); + logger.error("Uncaught exception", ex); + } + catch (StackOverflowError er) { + logger.error("Stack Overflow on {}", ingress.domain); + queue.put(new InvalidTaskResult(ingress.domain, "Stack overflow")); + } + catch (InterruptedException e) { + logger.warn("ex", e); + } + } + + @Override + public void run() { + try { + for (;;) { + runCycle(); + } + } + catch (InterruptedException ex) { + logger.error("Interrupted", ex); + } + catch (Throwable t) { + logger.error("Fetcher thread terminating on uncaught exception", t); + throw t; + } + } + + private Optional resolveRedirects(EdgeIndexTask ingress) { + try { + EdgeUrl firstUrl = ingress.urls.get(0); + EdgeUrl homeUrl = new EdgeUrl(firstUrl.proto, firstUrl.domain, firstUrl.port, "/"); + + EdgeUrl[] resolvedUrl = Observable.just(homeUrl) + .flatMap(url -> redirectResolver.probe(url).onErrorComplete()) + .blockingStream().toArray(EdgeUrl[]::new); + + if (resolvedUrl.length == 0) { + return Optional.of(new InvalidTaskResult(ingress.domain, "Failed to resolve redirect 1 @ " + ingress.urls.get(0))); + } + if (Objects.equals(resolvedUrl[0].domain, ingress.domain)) { + return Optional.empty(); + } + + logger.debug("Aliased domain {} -> {}", ingress.domain, resolvedUrl[0].domain); + + return Optional.of(new DomainAliasResult(ingress.domain, resolvedUrl[0].domain, resolvedUrl)); + } + catch (Exception ex) { + logger.info("Could not alias ingress {}", ingress.domain); + } + return Optional.of(new InvalidTaskResult(ingress.domain, "Failed to resolve redirect 2")); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/CrawlerIndexWorker.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/CrawlerIndexWorker.java new file mode 100644 index 00000000..c7ec5643 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/CrawlerIndexWorker.java @@ -0,0 +1,89 @@ +package nu.marginalia.wmsa.edge.crawler.worker; + +import nu.marginalia.wmsa.edge.crawler.domain.DomainCrawlerFactory; +import nu.marginalia.wmsa.edge.crawler.worker.facade.TaskProvider; +import nu.marginalia.wmsa.edge.crawler.worker.results.DomainCrawlerWorkerResults; +import nu.marginalia.wmsa.edge.crawler.worker.results.InvalidTaskResult; +import nu.marginalia.wmsa.edge.crawler.worker.results.WorkerResults; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.LinkedBlockingQueue; + +public class CrawlerIndexWorker implements Worker { + private final DomainCrawlerFactory domainCrawlerFactory; + private final TaskProvider taskProvider; + private final IpBlockList blockList; + + private final LinkedBlockingQueue queue; + private final int pass; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + public CrawlerIndexWorker( + DomainCrawlerFactory domainCrawlerFactory, + TaskProvider taskProvider, + IpBlockList blockList, LinkedBlockingQueue queue, int pass) { + this.domainCrawlerFactory = domainCrawlerFactory; + this.taskProvider = taskProvider; + this.blockList = blockList; + this.queue = queue; + this.pass = pass; + } + + @Override + public void runCycle() throws InterruptedException { + + var ingress = taskProvider + .getIndexTask(pass); + + if (ingress.isEmpty()) { + wmsa_edge_crawler_idle_worker.inc(); + Thread.sleep(100); + return; + } + try { + if (ingress.rank > 0.25 && !blockList.isAllowed(ingress.domain)) { + queue.put(new InvalidTaskResult(ingress.domain, "IP blocked")); + logger.info("{} IP-blacklisted", ingress.domain); + return; + } + + long start = System.currentTimeMillis(); + + var dc = domainCrawlerFactory.domainCrawler(ingress); + var res = dc.crawl(); + + wmsa_edge_crawler_thread_run_times.observe(System.currentTimeMillis() - start); + + queue.put(new DomainCrawlerWorkerResults(res)); + } + catch (RuntimeException ex) { + logger.warn("Leaking {}", ingress.domain); + logger.error("Uncaught exception", ex); + } + catch (StackOverflowError er) { + logger.error("Stack Overflow on {}", ingress.domain); + queue.put(new InvalidTaskResult(ingress.domain, "Stack overflow")); + } + catch (InterruptedException e) { + throw e; + } + } + + @Override + public void run() { + for (;;) { + try { + runCycle(); + } + catch (InterruptedException ex) { + logger.error("Interrupted", ex); + break; + } + catch (Exception ex) { + logger.error("Uncaught exception in Fetcher thread", ex); + } + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/GeoIpBlocklist.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/GeoIpBlocklist.java new file mode 100644 index 00000000..482f5bbf --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/GeoIpBlocklist.java @@ -0,0 +1,101 @@ +package nu.marginalia.wmsa.edge.crawler.worker; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.inject.Singleton; +import com.opencsv.CSVReader; +import com.opencsv.exceptions.CsvValidationException; +import lombok.AllArgsConstructor; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetAddress; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ExecutionException; + +@Singleton +public class GeoIpBlocklist { + private final TreeMap ranges = new TreeMap<>(); + private Set blacklist = Set.of("CN", "HK"); + private Set graylist = Set.of("RU", "TW", "IN", "ZA", "SG", "UA"); + + private final Cache countryCache = CacheBuilder.newBuilder().maximumSize(100_000).build(); + + private static final Logger logger = LoggerFactory.getLogger(GeoIpBlocklist.class); + + @AllArgsConstructor + static class IpRange { + public final long from; + public final long to; + public final String country; + }; + + public GeoIpBlocklist() throws IOException, CsvValidationException { + var resource = Objects.requireNonNull(ClassLoader.getSystemResourceAsStream("IP2LOCATION-LITE-DB1.CSV"), + "Could not load IP location db"); + + try (var reader = new CSVReader(new InputStreamReader(resource, StandardCharsets.UTF_8))) { + for (;;) { + String[] vals = reader.readNext(); + if (vals == null) { + break; + } + if (!(blacklist.contains(vals[2]) || graylist.contains(vals[2]))) { + continue; + } + var range = new GeoIpBlocklist.IpRange(Long.parseLong(vals[0]), + Long.parseLong(vals[1]), + vals[2]); + ranges.put(range.from, range); + } + } + + logger.info("Loaded {} IP ranges", ranges.size()); + } + + public String getCountry(InetAddress address) { + byte[] bytes = address.getAddress(); + long ival = ((long)bytes[0]&0xFF) << 24 | ((long)bytes[1]&0xFF) << 16 | ((long)bytes[2]&0xFF)<< 8 | ((long)bytes[3]&0xFF); + + Long key = ranges.floorKey(ival); + if (null == key) { + return "-"; + } + + var range = ranges.get(key); + if (ival >= key && ival < range.to) { + return range.country; + } + + return "-"; + } + + public boolean isAllowed(EdgeDomain domain) { + String country = getCountry(domain); + + if (blacklist.contains(country)) { + return false; + } + if (graylist.contains(country)) { + return "www".equals(domain.subDomain); + } + + return true; + } + + public String getCountry(EdgeDomain domain) { + try { + return getCountry(InetAddressCache.getAddress(domain)); + } + catch (Throwable ex) { + logger.debug("Failed to resolve {}", domain); + return "-"; + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/InetAddressCache.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/InetAddressCache.java new file mode 100644 index 00000000..3bf5d616 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/InetAddressCache.java @@ -0,0 +1,23 @@ +package nu.marginalia.wmsa.edge.crawler.worker; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import nu.marginalia.wmsa.edge.model.EdgeDomain; + +import java.net.InetAddress; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +public class InetAddressCache { + private static Cache cache = CacheBuilder.newBuilder().maximumSize(10_000_000).expireAfterAccess(1, TimeUnit.HOURS).build(); + public static InetAddress getAddress(EdgeDomain domain) throws Throwable { + try { + return cache.get(domain, ()->{ + return InetAddress.getByName(domain.getAddress()); + }); + } + catch (ExecutionException ex) { + throw ex.getCause(); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/IpBlockList.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/IpBlockList.java new file mode 100644 index 00000000..ce33abcb --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/IpBlockList.java @@ -0,0 +1,88 @@ +package nu.marginalia.wmsa.edge.crawler.worker; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import org.apache.commons.net.util.SubnetUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +@Singleton +public class IpBlockList { + private final GeoIpBlocklist geoIpBlocklist; + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final List badSubnets = new ArrayList<>(); + + @Inject + public IpBlockList(GeoIpBlocklist geoIpBlocklist) { + this.geoIpBlocklist = geoIpBlocklist; + + var resource = Objects.requireNonNull( + ClassLoader.getSystemResourceAsStream("ip-banned-cidr.txt"), + "Could not load IP blacklist"); + + try (var reader = new BufferedReader(new InputStreamReader(resource, StandardCharsets.UTF_8))) { + + for (;;) { + var cidr = reader.readLine(); + if (cidr == null) { + break; + } + if (!cidr.isBlank() && !cidr.startsWith("#") && !cidr.contains(":")) { + badSubnets.add(new SubnetUtils(cidr).getInfo()); + } + } + } catch (IOException e) { + logger.error("Failed to read IP list"); + } + + logger.info("Loaded {} CIDRs", badSubnets.size()); + } + + Predicate numericPattern = Pattern.compile(".*\\d{4}.*").asMatchPredicate(); + + public boolean isAllowed(EdgeDomain domain) { + if (domain.domain.endsWith(".cn")) { + logger.debug("Blocking {} on .cn-end", domain); + return false; + } + if (numericPattern.test(domain.toString())) { + logger.debug("Blocking {} on numeric", domain); + return false; + } + + try { + var hostAddress = InetAddressCache.getAddress(domain).getHostAddress(); + var subnet = badSubnets.stream().filter(sn -> sn.isInRange(hostAddress)).findFirst(); + if (subnet.isPresent()) { + logger.debug("Blocking {} on IP range: {}", domain, subnet.get()); + return false; + } + } catch (Throwable t) { + return false; + } + + var geo = geoIpBlocklist.isAllowed(domain); + if (!geo) { + logger.debug("Blocking {} on geo blocklist", domain); + } + return geo; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/UploaderWorker.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/UploaderWorker.java new file mode 100644 index 00000000..08b71868 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/UploaderWorker.java @@ -0,0 +1,200 @@ +package nu.marginalia.wmsa.edge.crawler.worker; + +import io.prometheus.client.Counter; +import io.prometheus.client.Histogram; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.crawler.domain.DomainCrawlResults; +import nu.marginalia.wmsa.edge.crawler.worker.facade.UploadFacade; +import nu.marginalia.wmsa.edge.crawler.worker.results.WorkerResults; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.List; +import java.util.OptionalDouble; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +public class UploaderWorker implements Runnable { + private final List> queues; + private final UploadFacade uploadFacade; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final static double UNKNOWN_SITE_ATTRACTOR = -2.5; + public final static double QUALITY_LOWER_BOUND_CUTOFF = -15; + + private static final Counter wmsa_edge_crawler_pages_indexed = Counter.build("wmsa_edge_crawler_pages_indexed", "Pages Indexed") + .register(); + private static final Counter wmsa_edge_crawler_domains_indexed = Counter.build("wmsa_edge_crawler_domains_indexed", "Domains Indexed") + .register(); + private static final Counter wmsa_edge_crawler_links_discovered = Counter.build("wmsa_edge_crawler_links_discovered", "Links Discovered") + .register(); + private static final Counter wmsa_edge_crawler_duds = Counter.build("wmsa_edge_crawler_duds", "Duds") + .register(); + private static final Counter wmsa_edge_crawler_domain_alias = Counter.build("wmsa_edge_crawler_domain_alias", "Alias") + .register(); + private static final Histogram wmsa_edge_crawler_publish_time = Histogram.build("wmsa_edge_crawler_publish_time", "Post wait") + .register(); + private static final Histogram wmsa_uploader_job_wait_time = Histogram.build("wmsa_uploader_job_wait_time", "Underrun Time") + .register(); + + public UploaderWorker(List> queues, UploadFacade uploadFacade) { + this.queues = queues; + this.uploadFacade = uploadFacade; + } + + volatile int queueDepth = 0; + + @SneakyThrows + @Override + public void run() { + uploaderThread(); + } + + @SneakyThrows + private void uploaderThread() { + for (;;) { + long waitStart = System.currentTimeMillis(); + int waitTicks = 0; + + updateQueueDepth(); + + for (var queue : queues) { + WorkerResults res; + + if (waitTicks++ < queues.size()) { + res = queue.poll(); + } + else { + res = queue.poll(10, TimeUnit.MILLISECONDS); + } + + try { + if (null != res) { + waitTicks = 0; + wmsa_uploader_job_wait_time.observe(System.currentTimeMillis() - waitStart); + res.upload(this); + waitStart = System.currentTimeMillis(); + } + } catch (Exception ex) { + logger.error("Error", ex); + } + } + } + } + + private void updateQueueDepth() { + int qd = 0; + for (var queue : queues) { + qd += queue.size(); + } + queueDepth = qd; + } + + + @SneakyThrows + public void onDomainCrawlResults(DomainCrawlResults dc) { + while (uploadFacade.isBlocked()) { + Thread.sleep(1000); + } + var domain = dc.domain; + + long start = System.currentTimeMillis(); + + updateStatsForResults(dc); + + double avgQuality = calculateMedianQuality(dc).orElse(-5.); + + if (logger.isInfoEnabled()) { + + String log = String.format("QD:%2d\t%2d\tQ:%4.2f\tR:%4.2f\t%3d\t%4.2f\t%s", + queueDepth, dc.pass, + Math.round(100 * avgQuality) / 100., + Math.round(10000 * (1 - dc.rank)) / 100., + dc.pageContents.size(), + (System.currentTimeMillis() - dc.crawlStart) / 1000., + domain); + + logger.info(log); + } + + uploadResults(dc, avgQuality); + + double depthPenalty = dc.pass / 250.; + uploadFacade.finishTask(domain, avgQuality - depthPenalty, EdgeDomainIndexingState.ACTIVE); + + wmsa_edge_crawler_publish_time.observe(System.currentTimeMillis() - start); + } + + private void updateStatsForResults(DomainCrawlResults dc) { + if (dc.pageContents.isEmpty()) { + wmsa_edge_crawler_duds.inc(); + } + + wmsa_edge_crawler_domains_indexed.inc(); + wmsa_edge_crawler_pages_indexed.inc(dc.pageContents.size()); + wmsa_edge_crawler_links_discovered.inc(dc.extUrl.size()); + } + + public static OptionalDouble calculateMedianQuality(DomainCrawlResults dc) { + + double[] qualities = dc.pageContents.values().stream().mapToDouble(page -> page.metadata.quality()).sorted().toArray(); + if (qualities.length <= 5) { + return Arrays.stream(qualities).average(); + } + else { + return OptionalDouble.of(qualities[qualities.length/2]); + } + } + + public static double calculateExternalLinkPenalty(DomainCrawlResults dc) { + return dc.extUrl.size() / ((1+dc.pageContents.size())*50.); + } + + private void uploadResults(DomainCrawlResults dc, double avgQuality) { + + final double extLinkPenalty = calculateExternalLinkPenalty(dc); + if (uploadFacade.isBlacklisted(dc.domain)) { + return; + } + + final double linkQualityRating = -5; //(avgQuality + UNKNOWN_SITE_ATTRACTOR)/2 - extLinkPenalty; + + uploadFacade.putUrls(dc.extUrl, linkQualityRating); + uploadFacade.putUrls(dc.intUrl, linkQualityRating); + uploadFacade.putUrlVisits(dc.visits()); + uploadFacade.putFeeds(dc.feeds); + + if (avgQuality < QUALITY_LOWER_BOUND_CUTOFF) { + return; + } + + uploadFacade.putLinks(dc.links, false); + uploadFacade.putWords(dc.pageContents.values(), 0); + } + + public void onDomainAlias(EdgeDomain source, EdgeDomain dest, EdgeUrl[] urls) { + wmsa_edge_crawler_domain_alias.inc(); + + if (urls.length == 0) { + wmsa_edge_crawler_duds.inc(); + } + + long start = System.currentTimeMillis(); + uploadFacade.putDomainAlias(source, dest); + uploadFacade.putUrls(Arrays.asList(urls), -2); + uploadFacade.finishTask(source, -1000, EdgeDomainIndexingState.REDIR); + + wmsa_edge_crawler_publish_time.observe(System.currentTimeMillis() - start); + } + + public void onInvalidDomain(EdgeDomain domain, String why) { + logger.warn("Setting domain {} state to ERROR: {}", domain, why); + long start = System.currentTimeMillis(); + uploadFacade.finishTask(domain, -1000, EdgeDomainIndexingState.ERROR); + wmsa_edge_crawler_publish_time.observe(System.currentTimeMillis() - start); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/UrlBlocklist.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/UrlBlocklist.java new file mode 100644 index 00000000..9ed749bf --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/UrlBlocklist.java @@ -0,0 +1,55 @@ +package nu.marginalia.wmsa.edge.crawler.worker; + +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +public class UrlBlocklist { + private final List> patterns = new ArrayList<>(); + + public UrlBlocklist() { + patterns.add(Pattern.compile(".*/[a-f0-9]{40}(/|$)").asPredicate()); + patterns.add(Pattern.compile("/download(-([A-Za-z]+|[0-9]+)){4,}\\.(htm|html|php)$").asPredicate()); + patterns.add(Pattern.compile("/download(-([A-Za-z]+|[0-9]+)){4,}\\.(htm|html|php)$").asPredicate()); + patterns.add(Pattern.compile("/permalink/[a-z]+(-([A-Za-z]+|[0-9]+)){3,}\\.(htm|html|php)$").asPredicate()); + patterns.add(Pattern.compile("(webrx3|lib|pdf|book|720p).*/[A-Za-z]+(-([A-Za-z]+|[0-9]+)){3,}((-[0-9]+)?/|\\.(php|htm|html))$").asPredicate()); + patterns.add(Pattern.compile("/node/.*/[a-z]+(-[a-z0-9]+)+.htm$").asPredicate()); + patterns.add(Pattern.compile(".*-download-free$").asPredicate()); + } + + public boolean isUrlBlocked(EdgeUrl url) { + try { + if ("github.com".equals(url.domain.domain)) { + return url.path.chars().filter(c -> c == '/').count() > 2; + } + + return patterns.stream().anyMatch(p -> p.test(url.path)); + } + catch (StackOverflowError ex) { + return true; + } + } + + public boolean isForumLink(EdgeUrl linkUrl) { + var path = linkUrl.path; + if (path.startsWith("/forum")) { + return true; + } + if (path.startsWith("/lists/")) { + return true; + } + if (path.startsWith("mailinglist")) { + return true; + } + if (path.contains("phpbb")) { + return true; + } + return false; + } + + + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/Worker.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/Worker.java new file mode 100644 index 00000000..4454ec48 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/Worker.java @@ -0,0 +1,15 @@ +package nu.marginalia.wmsa.edge.crawler.worker; + +import io.prometheus.client.Counter; +import io.prometheus.client.Histogram; + +public interface Worker extends Runnable { + static Histogram wmsa_edge_crawler_thread_run_times = + Histogram.build("wmsa_edge_crawler_thread_run_times", "Run Times") + .register(); + static Counter wmsa_edge_crawler_idle_worker = + Counter.build("wmsa_edge_crawler_idle_worke", "No work, no money") + .register(); + + void runCycle() throws InterruptedException; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/WorkerFactory.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/WorkerFactory.java new file mode 100644 index 00000000..3a58ff9b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/WorkerFactory.java @@ -0,0 +1,47 @@ +package nu.marginalia.wmsa.edge.crawler.worker; + +import com.google.inject.Inject; +import nu.marginalia.wmsa.edge.crawler.domain.DomainCrawlerFactory; +import nu.marginalia.wmsa.edge.crawler.fetcher.HttpRedirectResolver; +import nu.marginalia.wmsa.edge.crawler.worker.facade.TaskProvider; +import nu.marginalia.wmsa.edge.crawler.worker.facade.UploadFacade; +import nu.marginalia.wmsa.edge.crawler.worker.results.WorkerResults; + +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; + +public class WorkerFactory { + + private final DomainCrawlerFactory domainCrawlerFactory; + private final TaskProvider taskProvider; + private final HttpRedirectResolver redirectResolver; + private final UploadFacade uploadFacade; + private final IpBlockList blockList; + + @Inject + public WorkerFactory(DomainCrawlerFactory domainCrawlerFactory, + TaskProvider taskProvider, + + HttpRedirectResolver redirectResolver, + UploadFacade uploadFacade, IpBlockList blockList) + { + this.domainCrawlerFactory = domainCrawlerFactory; + this.taskProvider = taskProvider; + this.uploadFacade = uploadFacade; + this.redirectResolver = redirectResolver; + this.blockList = blockList; + } + + public CrawlerIndexWorker buildIndexWorker(LinkedBlockingQueue queue, int pass) { + return new CrawlerIndexWorker(domainCrawlerFactory, taskProvider, blockList, queue, pass); + } + + + public CrawlerDiscoverWorker buildDiscoverWorker(LinkedBlockingQueue queue) { + return new CrawlerDiscoverWorker(domainCrawlerFactory, taskProvider, redirectResolver, blockList, queue); + } + + public UploaderWorker buildUploader(List> queues) { + return new UploaderWorker(queues, uploadFacade); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/data/CrawlJobsSpecification.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/data/CrawlJobsSpecification.java new file mode 100644 index 00000000..4e4aa5d7 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/data/CrawlJobsSpecification.java @@ -0,0 +1,8 @@ +package nu.marginalia.wmsa.edge.crawler.worker.data; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class CrawlJobsSpecification { + public final int pass; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/data/UploaderMetrics.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/data/UploaderMetrics.java new file mode 100644 index 00000000..e325a6b7 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/data/UploaderMetrics.java @@ -0,0 +1,15 @@ +package nu.marginalia.wmsa.edge.crawler.worker.data; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.With; + +@NoArgsConstructor @AllArgsConstructor @With +public class UploaderMetrics { + public long pagesIndexed = 0L; + public long domainsIndexed = 0L; + public long extLinksDiscovered = 0L; + public long duds = 0L; + public long waitTime = 0L; + public long aliasedDomains = 0L; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/facade/TaskProvider.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/facade/TaskProvider.java new file mode 100644 index 00000000..73355310 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/facade/TaskProvider.java @@ -0,0 +1,10 @@ +package nu.marginalia.wmsa.edge.crawler.worker.facade; + +import com.google.inject.ImplementedBy; +import nu.marginalia.wmsa.edge.model.crawl.EdgeIndexTask; + +@ImplementedBy(TaskProviderImpl.class) +public interface TaskProvider { + EdgeIndexTask getIndexTask(int pass); + EdgeIndexTask getDiscoverTask(); +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/facade/TaskProviderImpl.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/facade/TaskProviderImpl.java new file mode 100644 index 00000000..01274d82 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/facade/TaskProviderImpl.java @@ -0,0 +1,47 @@ +package nu.marginalia.wmsa.edge.crawler.worker.facade; + +import nu.marginalia.wmsa.client.exception.RouteNotConfiguredException; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.edge.director.client.EdgeDirectorClient; +import nu.marginalia.wmsa.edge.model.crawl.EdgeIndexTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; + +public class TaskProviderImpl implements TaskProvider { + + private final EdgeDirectorClient client; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Inject + public TaskProviderImpl(EdgeDirectorClient client) { + this.client = client; + } + + @Override + public EdgeIndexTask getIndexTask(int pass) { + try { + return client.getIndexTask(Context.internal(), pass, 100) + .onErrorReturn(t -> new EdgeIndexTask(null, 0, 0, 1.)) + .blockingFirst(); + } + catch (RouteNotConfiguredException ex) { + logger.warn("No route to Director"); + return new EdgeIndexTask(null, 0, 0, 1.); + } + } + + @Override + public EdgeIndexTask getDiscoverTask() { + try { + return client.getDiscoverTask(Context.internal()) + .onErrorReturn(t -> new EdgeIndexTask(null, 0, 0, 1.)) + .blockingFirst(); + } + catch (RouteNotConfiguredException ex) { + logger.warn("No route to Data Store"); + return new EdgeIndexTask(null, 0, 0, 1.); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/facade/UploadFacade.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/facade/UploadFacade.java new file mode 100644 index 00000000..03e63c7c --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/facade/UploadFacade.java @@ -0,0 +1,25 @@ +package nu.marginalia.wmsa.edge.crawler.worker.facade; + +import com.google.inject.ImplementedBy; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainLink; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageContent; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlVisit; + +import java.util.Collection; + +@ImplementedBy(UploadFacadeDirectImpl.class) +public interface UploadFacade { + void putLinks(Collection links, boolean wipeExisting); + void putUrls(Collection urls, double quality); + void putFeeds(Collection urls); + void putUrlVisits(Collection visits); + void putDomainAlias(EdgeDomain src, EdgeDomain dst); + void finishTask(EdgeDomain domain, double quality, EdgeDomainIndexingState state); + + void putWords(Collection pages, int writer); + + boolean isBlacklisted(EdgeDomain domain); + boolean isBlocked(); +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/facade/UploadFacadeDirectImpl.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/facade/UploadFacadeDirectImpl.java new file mode 100644 index 00000000..aa29d787 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/facade/UploadFacadeDirectImpl.java @@ -0,0 +1,145 @@ +package nu.marginalia.wmsa.edge.crawler.worker.facade; + +import com.google.inject.Inject; +import io.prometheus.client.Histogram; +import io.reactivex.rxjava3.core.BackpressureStrategy; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDao; +import nu.marginalia.wmsa.edge.director.client.EdgeDirectorClient; +import nu.marginalia.wmsa.edge.index.client.EdgeIndexClient; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainLink; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageContent; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlVisit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.concurrent.TimeUnit; + +public class UploadFacadeDirectImpl implements UploadFacade { + private final EdgeDataStoreDao dataStore; + private final EdgeIndexClient indexClient; + private final EdgeDirectorClient directorClient; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private static final Histogram wmsa_edge_upload_metrics = Histogram + .build("wmsa_edge_upload_metrics", "upload times") + .labelNames("action") + .register(); + + @Inject + public UploadFacadeDirectImpl(EdgeDataStoreDao dataStore, + EdgeIndexClient indexClient, + EdgeDirectorClient directorClient) { + this.dataStore = dataStore; + this.indexClient = indexClient; + this.directorClient = directorClient; + } + + @Override + public void putLinks(Collection links, boolean wipeExisting) { + wmsa_edge_upload_metrics + .labels("putLinks") + .time(() -> { + dataStore.putLink(wipeExisting, links.toArray(EdgeDomainLink[]::new)); + }); + } + + @Override + public void putUrls(Collection urls, double quality) { + wmsa_edge_upload_metrics + .labels("putUrls") + .time(() -> { + dataStore.putUrl(quality, urls.toArray(EdgeUrl[]::new)); + }); + } + @Override + public void putFeeds(Collection feeds) { + if (feeds.isEmpty()) { + return; + } + wmsa_edge_upload_metrics + .labels("putFeeds") + .time(() -> { + dataStore.putFeeds(feeds.toArray(EdgeUrl[]::new)); + }); + } + + @Override + public void putUrlVisits(Collection visits) { + wmsa_edge_upload_metrics + .labels("putUrlVisits") + .time(() -> { + dataStore.putUrlVisited(visits.toArray(EdgeUrlVisit[]::new)); + }); + } + + @Override + public void putDomainAlias(EdgeDomain src, EdgeDomain dst) { + wmsa_edge_upload_metrics + .labels("putDomainAlias") + .time(() -> { + dataStore.putDomainAlias(src, dst); + }); + } + + @Override + public void finishTask(EdgeDomain domain, double quality, EdgeDomainIndexingState state) { + wmsa_edge_upload_metrics + .labels("finishTask") + .time(() -> { + directorClient.finishTask(Context.internal(), domain, quality, state).blockingSubscribe(); + }); + } + + @Override + public void putWords(Collection pages, int writer) { + wmsa_edge_upload_metrics + .labels("putWords") + .time(() -> { + Flowable.fromIterable(pages) + + .parallel(4) + .flatMap(page -> indexClient + .putWords(Context.internal(), + dataStore.getDomainId(page.url.domain), + dataStore.getUrlId(page.url), + page.metadata.quality(), + page.words, + writer + + ).subscribeOn(Schedulers.io()) + .retryWhen((Observable f) -> f.take(5).delay(30, TimeUnit.SECONDS)) + .toFlowable(BackpressureStrategy.BUFFER) + ).reduce((a,b)->a) + .blockingSubscribe(); + }); + } + + @Override + public boolean isBlacklisted(EdgeDomain domain) { + return dataStore.isBlacklisted(domain); + } + + @Override + public boolean isBlocked() { + var ctx = Context.internal(); + + try { + return directorClient.isBlocked(ctx).blockingFirst() + || indexClient.isBlocked(ctx).blockingFirst(); + } + catch (Exception ex) { + return false; + } + } + + public void updateDomainIndexTimestamp(EdgeDomain domain, EdgeDomainIndexingState state, EdgeDomain alias, int minIndexed) { + dataStore.updateDomainIndexTimestamp(domain, state, alias, minIndexed); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/results/DomainAliasResult.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/results/DomainAliasResult.java new file mode 100644 index 00000000..c6e1ea06 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/results/DomainAliasResult.java @@ -0,0 +1,19 @@ +package nu.marginalia.wmsa.edge.crawler.worker.results; + +import lombok.AllArgsConstructor; +import lombok.ToString; +import nu.marginalia.wmsa.edge.crawler.worker.UploaderWorker; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +@AllArgsConstructor @ToString +public class DomainAliasResult implements WorkerResults { + private final EdgeDomain source; + private final EdgeDomain dest; + private final EdgeUrl[] urls; + + @Override + public void upload(UploaderWorker uploader) { + uploader.onDomainAlias(source, dest, urls); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/results/DomainCrawlerWorkerResults.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/results/DomainCrawlerWorkerResults.java new file mode 100644 index 00000000..5c9f4ee6 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/results/DomainCrawlerWorkerResults.java @@ -0,0 +1,16 @@ +package nu.marginalia.wmsa.edge.crawler.worker.results; + +import lombok.AllArgsConstructor; +import lombok.ToString; +import nu.marginalia.wmsa.edge.crawler.domain.DomainCrawlResults; +import nu.marginalia.wmsa.edge.crawler.worker.UploaderWorker; + +@AllArgsConstructor @ToString +public class DomainCrawlerWorkerResults implements WorkerResults { + private final DomainCrawlResults results; + + @Override + public void upload(UploaderWorker uploader) { + uploader.onDomainCrawlResults(results); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/results/InvalidTaskResult.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/results/InvalidTaskResult.java new file mode 100644 index 00000000..d4ae6a8c --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/results/InvalidTaskResult.java @@ -0,0 +1,18 @@ +package nu.marginalia.wmsa.edge.crawler.worker.results; + +import lombok.AllArgsConstructor; +import lombok.ToString; +import nu.marginalia.wmsa.edge.crawler.worker.UploaderWorker; +import nu.marginalia.wmsa.edge.model.EdgeDomain; + +@AllArgsConstructor @ToString +public class InvalidTaskResult implements WorkerResults { + private final EdgeDomain domain; + public String why; + + + @Override + public void upload(UploaderWorker uploader) { + uploader.onInvalidDomain(domain, why); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/results/WorkerResults.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/results/WorkerResults.java new file mode 100644 index 00000000..77dfd162 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawler/worker/results/WorkerResults.java @@ -0,0 +1,9 @@ +package nu.marginalia.wmsa.edge.crawler.worker.results; + +import nu.marginalia.wmsa.edge.crawler.worker.UploaderWorker; + +public interface WorkerResults { + + void upload(UploaderWorker uploader); + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/AbortMonitor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/AbortMonitor.java new file mode 100644 index 00000000..4a87acb5 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/AbortMonitor.java @@ -0,0 +1,47 @@ +package nu.marginalia.wmsa.edge.crawling; + + +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.tools.ReindexMain; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Files; +import java.nio.file.Path; + +public class AbortMonitor { + private volatile boolean abort = false; + private static volatile AbortMonitor instance = null; + private static final Logger logger = LoggerFactory.getLogger(AbortMonitor.class); + + public static AbortMonitor getInstance() { + if (instance == null) { + synchronized (ReindexMain.AbortMonitor.class) { + if (instance == null) { + instance = new AbortMonitor(); + new Thread(instance::run, "AbortMon").start(); + } + } + } + return instance; + } + + private AbortMonitor() { + } + + @SneakyThrows + public void run() { + for (;;) { + Thread.sleep(1000); + if (Files.exists(Path.of("/tmp/stop"))) { + logger.warn("Abort file found"); + abort = true; + Files.delete(Path.of("/tmp/stop")); + } + } + } + + public boolean isAlive() { + return !abort; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlJobExtractorMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlJobExtractorMain.java new file mode 100644 index 00000000..a13d630a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlJobExtractorMain.java @@ -0,0 +1,224 @@ +package nu.marginalia.wmsa.edge.crawling; + +import com.github.luben.zstd.ZstdOutputStream; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.zaxxer.hikari.HikariDataSource; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.edge.crawling.model.CrawlingSpecification; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDomainBlacklistImpl; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import org.mariadb.jdbc.Driver; + +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.stream.Stream; + +public class CrawlJobExtractorMain { + + private static final String specificDomainSql = + """ + SELECT ID + FROM EC_DOMAIN + WHERE URL_PART=? + """; + + private static final String domainsSql = + """ + SELECT ID, LOWER(EC_DOMAIN.URL_PART) + FROM EC_DOMAIN + WHERE QUALITY_RAW>-100 + AND INDEXED>0 + AND STATE<2 + ORDER BY + INDEX_DATE ASC, + DISCOVER_DATE ASC, + STATE DESC, + INDEXED DESC, + EC_DOMAIN.ID + """; + + private static final String urlsSql = + """ + SELECT CONCAT(PROTO, "://", ?, URL) + FROM EC_URL + WHERE DOMAIN_ID=? + ORDER BY + VISITED DESC, + DATA_HASH IS NOT NULL DESC, + ID + LIMIT 25000 + """; + + private static final String visitedUrlsSql = + """ + SELECT COUNT(*) + FROM EC_URL + WHERE DOMAIN_ID=? + AND VISITED + ; + """; + private static final int MIN_VISIT_COUNT = 100; + private static final int MAX_VISIT_COUNT = 5000; + + private final EdgeDomainBlacklistImpl blacklist; + + private final Connection conn; + private final HashFunction hasher = Hashing.murmur3_128(0); + + public static void main(String... args) throws SQLException, IOException { + Driver driver = new Driver(); + var outFile = Path.of(args[0]); + + Gson gson = new GsonBuilder().create(); + String[] targetDomains = Arrays.stream(args).skip(1).toArray(String[]::new); + + + try (var out = new PrintWriter(new ZstdOutputStream(new BufferedOutputStream(new FileOutputStream(outFile.toFile()))))) { + final var extractor = new CrawlJobExtractorMain(new DatabaseModule().provideConnection()); + final Stream jobs; + + if (targetDomains.length > 0) { + jobs = Arrays.stream(targetDomains).map(EdgeDomain::new).map(extractor::extractDomain); + } else { + jobs = extractor.extractDomains(); + } + + jobs.map(gson::toJson).forEach(out::println); + } + } + + private record DomainWithId(String domainName, int id) {}; + + private Stream extractDomains() { + List ids = new ArrayList<>(100_000); + + try (var stmt = conn.prepareStatement(domainsSql)) { + stmt.setFetchSize(10_000); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + ids.add(new DomainWithId(rsp.getString(2), rsp.getInt(1))); + } + } + catch (SQLException ex) { + ex.printStackTrace(); + } + + Collections.shuffle(ids); + return ids.stream() + .filter(id -> !blacklist.isBlacklisted(id.id)) + .map(this::createCrawlJobForDomain); + } + + private CrawlingSpecification createCrawlJobForDomain(DomainWithId domainWithId) { + var spec = new CrawlingSpecification(); + spec.id = createId(domainWithId); + spec.domain = domainWithId.domainName; + spec.urls = new ArrayList<>(); + spec.crawlDepth = getCrawlDepth(domainWithId); + + try (var stmt = conn.prepareStatement(urlsSql)) { + stmt.setFetchSize(1000); + stmt.setString(1, domainWithId.domainName); + stmt.setInt(2, domainWithId.id); + var rsp = stmt.executeQuery(); + + while (rsp.next()) { + spec.urls.add(rsp.getString(1)); + } + } + catch (SQLException ex) { + ex.printStackTrace(); + } + + spec.urls.sort(Comparator.naturalOrder()); + + return spec; + } + + public CrawlJobExtractorMain(HikariDataSource ds) throws SQLException { + blacklist = new EdgeDomainBlacklistImpl(ds); + conn = ds.getConnection(); + } + + public CrawlingSpecification extractDomain(EdgeDomain domain) { + CrawlingSpecification spec = new CrawlingSpecification(); + spec.domain = domain.toString(); + spec.id = createId(domain); + spec.urls = new ArrayList<>(1000); + + + try (var domainQuery = conn.prepareStatement(specificDomainSql); + var urlQuery = conn.prepareStatement(urlsSql)) + { + domainQuery.setString(1, domain.toString()); + ResultSet rsp = domainQuery.executeQuery(); + int domainId = rsp.next() ? rsp.getInt(1) : -1; + + spec.crawlDepth = getCrawlDepth(new DomainWithId(domain.toString(), domainId)); + + urlQuery.setString(1, domain.toString()); + urlQuery.setInt(2, domainId); + urlQuery.setFetchSize(1000); + rsp = urlQuery.executeQuery(); + + while (rsp.next()) { + spec.urls.add(rsp.getString(1)); + } + + } catch (SQLException e) { + e.printStackTrace(); + } + + if (spec.urls.isEmpty()) { + spec.urls.add("https://"+domain+"/"); + } + + return spec; + } + + private String createId(DomainWithId domainWithId) { + return hasher.hashUnencodedChars(domainWithId.domainName).toString(); + } + + private String createId(EdgeDomain domain) { + return hasher.hashUnencodedChars(domain.toString()).toString(); + } + + private int getCrawlDepth(DomainWithId domainWithId) { + try (var domainQuery = conn.prepareStatement(visitedUrlsSql)) { + domainQuery.setInt(1, domainWithId.id); + var rsp = domainQuery.executeQuery(); + if (rsp.next()) { + return calculateCrawlDepthFromVisitedCount(rsp.getInt(1)); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return MIN_VISIT_COUNT; + } + + private int calculateCrawlDepthFromVisitedCount(int count) { + count = count + 100 + count / 4; + + if (count < MIN_VISIT_COUNT) { + count = MIN_VISIT_COUNT; + } + + if (count > MAX_VISIT_COUNT) { + count = MAX_VISIT_COUNT; + } + + return count; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlJobExtractorPageRankMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlJobExtractorPageRankMain.java new file mode 100644 index 00000000..5865935a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlJobExtractorPageRankMain.java @@ -0,0 +1,203 @@ +package nu.marginalia.wmsa.edge.crawling; + +import com.github.luben.zstd.ZstdOutputStream; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.zaxxer.hikari.HikariDataSource; +import gnu.trove.list.array.TIntArrayList; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.edge.crawling.model.CrawlingSpecification; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDomainBlacklistImpl; +import nu.marginalia.wmsa.edge.index.service.util.ranking.BetterReversePageRank; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeId; +import org.mariadb.jdbc.Driver; + +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.stream.Stream; + +public class CrawlJobExtractorPageRankMain { + + private static final String specificDomainSql = + """ + SELECT ID + FROM EC_DOMAIN + WHERE URL_PART=? + """; + private static final String specificDomainSqlFromId = + """ + SELECT LOWER(URL_PART) + FROM EC_DOMAIN + WHERE ID=? + """; + + private static final String urlsSql = + """ + SELECT CONCAT(PROTO, "://", ?, URL) + FROM EC_URL + WHERE DOMAIN_ID=? + ORDER BY + VISITED DESC, + DATA_HASH IS NOT NULL DESC, + ID + LIMIT 25000 + """; + + private static final String visitedUrlsSql = + """ + SELECT COUNT(*) + FROM EC_URL + WHERE DOMAIN_ID=? + AND VISITED + ; + """; + private static final int MIN_VISIT_COUNT = 100; + private static final int MAX_VISIT_COUNT = 5000; + + private final EdgeDomainBlacklistImpl blacklist; + + private final Connection conn; + private final HashFunction hasher = Hashing.murmur3_128(0); + + public static void main(String... args) throws SQLException, IOException { + Driver driver = new Driver(); + var outFile = Path.of(args[0]); + + Gson gson = new GsonBuilder().create(); + + var rpr = new BetterReversePageRank(new DatabaseModule().provideConnection(), "bikobatanari.art", "sadgrl.online", "wiki.xxiivv.com", "%neocities.org"); + rpr.setMaxKnownUrls(750); + + var targetDomainIds = rpr.pageRankWithPeripheralNodes(rpr.size(), false); + + try (var out = new PrintWriter(new ZstdOutputStream(new BufferedOutputStream(new FileOutputStream(outFile.toFile()))))) { + final var extractor = new CrawlJobExtractorPageRankMain(new DatabaseModule().provideConnection()); + + targetDomainIds.forEach(i -> { + out.println(gson.toJson(extractor.extractDomain(new EdgeId<>(i)))); + return true; + }); + } + } + + private record DomainWithId(String domainName, int id) {}; + + public CrawlJobExtractorPageRankMain(HikariDataSource ds) throws SQLException { + blacklist = new EdgeDomainBlacklistImpl(ds); + conn = ds.getConnection(); + } + + public CrawlingSpecification extractDomain(EdgeId domainId) { + CrawlingSpecification spec = new CrawlingSpecification(); + + String domainName = ""; + try (var domainQuery = conn.prepareStatement(specificDomainSqlFromId); + var urlQuery = conn.prepareStatement(urlsSql)) + { + domainQuery.setInt(1, domainId.getId()); + ResultSet rsp = domainQuery.executeQuery(); + domainName = rsp.next() ? rsp.getString(1) : ""; + + spec.domain = domainName; + spec.id = createId(new EdgeDomain(domainName)); + spec.urls = new ArrayList<>(1000); + + spec.crawlDepth = getCrawlDepth(new DomainWithId(domainName, domainId.getId())); + + urlQuery.setString(1, domainName.toString()); + urlQuery.setInt(2, domainId.getId()); + urlQuery.setFetchSize(1000); + rsp = urlQuery.executeQuery(); + + while (rsp.next()) { + spec.urls.add(rsp.getString(1)); + } + + } catch (SQLException e) { + e.printStackTrace(); + } + + if (spec.urls.isEmpty()) { + spec.urls.add("https://"+domainName+"/"); + } + + return spec; + } + public CrawlingSpecification extractDomain(EdgeDomain domain) { + CrawlingSpecification spec = new CrawlingSpecification(); + spec.domain = domain.toString(); + spec.id = createId(domain); + spec.urls = new ArrayList<>(1000); + + + try (var domainQuery = conn.prepareStatement(specificDomainSql); + var urlQuery = conn.prepareStatement(urlsSql)) + { + domainQuery.setString(1, domain.toString()); + ResultSet rsp = domainQuery.executeQuery(); + int domainId = rsp.next() ? rsp.getInt(1) : -1; + + spec.crawlDepth = getCrawlDepth(new DomainWithId(domain.toString(), domainId)); + + urlQuery.setString(1, domain.toString()); + urlQuery.setInt(2, domainId); + urlQuery.setFetchSize(1000); + rsp = urlQuery.executeQuery(); + + while (rsp.next()) { + spec.urls.add(rsp.getString(1)); + } + + } catch (SQLException e) { + e.printStackTrace(); + } + + if (spec.urls.isEmpty()) { + spec.urls.add("https://"+domain+"/"); + } + + return spec; + } + + private String createId(EdgeDomain domain) { + return hasher.hashUnencodedChars(domain.toString()).toString(); + } + + private int getCrawlDepth(DomainWithId domainWithId) { + try (var domainQuery = conn.prepareStatement(visitedUrlsSql)) { + domainQuery.setInt(1, domainWithId.id); + var rsp = domainQuery.executeQuery(); + if (rsp.next()) { + return calculateCrawlDepthFromVisitedCount(rsp.getInt(1)); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return MIN_VISIT_COUNT; + } + + private int calculateCrawlDepthFromVisitedCount(int count) { + count = count + 100 + count / 4; + + if (count < MIN_VISIT_COUNT) { + count = MIN_VISIT_COUNT; + } + + if (count > MAX_VISIT_COUNT) { + count = MAX_VISIT_COUNT; + } + + return count; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlPlanLoader.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlPlanLoader.java new file mode 100644 index 00000000..f4060aff --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlPlanLoader.java @@ -0,0 +1,26 @@ +package nu.marginalia.wmsa.edge.crawling; + +import nu.marginalia.wmsa.edge.model.EdgeCrawlPlan; +import org.yaml.snakeyaml.Yaml; + +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Path; + +public class CrawlPlanLoader { + private final Yaml yaml; + + public CrawlPlanLoader() { + yaml = new Yaml(); + } + + public EdgeCrawlPlan load(Path yamlFile) throws IOException { + try (var reader = new FileReader(yamlFile.toFile())) { + return yaml.loadAs(reader, EdgeCrawlPlan.class); + } + catch (IOException ex) { + throw new IOException("Failed to load crawl plan " + yamlFile, ex); + } + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawledDomainReader.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawledDomainReader.java new file mode 100644 index 00000000..e9b0b72e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawledDomainReader.java @@ -0,0 +1,28 @@ +package nu.marginalia.wmsa.edge.crawling; + +import com.github.luben.zstd.ZstdInputStream; +import com.github.luben.zstd.ZstdOutputStream; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import nu.marginalia.wmsa.edge.crawling.model.CrawledDomain; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; + +public class CrawledDomainReader { + private final Gson gson = new GsonBuilder().create(); + private static final Logger logger = LoggerFactory.getLogger(CrawledDomainReader.class); + + public CrawledDomainReader() { + } + + public CrawledDomain read(Path path) throws IOException { + try (var br = new BufferedReader(new InputStreamReader(new ZstdInputStream(new BufferedInputStream(new FileInputStream(path.toFile())))))) { + return gson.fromJson(br, CrawledDomain.class); + } + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawledDomainWriter.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawledDomainWriter.java new file mode 100644 index 00000000..ce0c7216 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawledDomainWriter.java @@ -0,0 +1,66 @@ +package nu.marginalia.wmsa.edge.crawling; + +import com.github.luben.zstd.ZstdOutputStream; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import nu.marginalia.wmsa.edge.crawling.model.CrawledDomain; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.file.Files; +import java.nio.file.Path; + +public class CrawledDomainWriter { + private final Path outputDir; + private final Gson gson = new GsonBuilder().create(); + private static final Logger logger = LoggerFactory.getLogger(CrawledDomainWriter.class); + + public CrawledDomainWriter(Path outputDir) { + this.outputDir = outputDir; + + if (!Files.isDirectory(outputDir)) { + throw new IllegalArgumentException("Output dir " + outputDir + " does not exist"); + } + } + + public String accept(CrawledDomain domainData) throws IOException { + Path outputFile = getOutputFile(domainData.id, domainData.domain); + + try (var outputStream = new OutputStreamWriter(new ZstdOutputStream(new BufferedOutputStream(new FileOutputStream(outputFile.toFile()))))) { + logger.info("Writing {} - {}", domainData.id, domainData.domain); + + gson.toJson(domainData, outputStream); + } + + return outputFile.getFileName().toString(); + } + + private Path getOutputFile(String id, String name) throws IOException { + String first = id.substring(0, 2); + String second = id.substring(2, 4); + + Path destDir = outputDir.resolve(first).resolve(second); + if (!Files.exists(destDir)) { + Files.createDirectories(destDir); + } + return destDir.resolve(id + "-" + filesystemSafeName(name) + ".zstd"); + } + + private String filesystemSafeName(String name) { + StringBuilder nameSaneBuilder = new StringBuilder(); + + name.chars() + .map(Character::toLowerCase) + .map(c -> (c & ~0x7F) == 0 ? c : 'X') + .map(c -> (Character.isDigit(c) || Character.isAlphabetic(c) || c == '.') ? c : 'X') + .limit(128) + .forEach(c -> nameSaneBuilder.append((char) c)); + + return nameSaneBuilder.toString(); + + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlerMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlerMain.java new file mode 100644 index 00000000..ea62e742 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlerMain.java @@ -0,0 +1,131 @@ +package nu.marginalia.wmsa.edge.crawling; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import nu.marginalia.wmsa.edge.crawling.model.CrawledDomain; +import nu.marginalia.wmsa.edge.crawling.model.CrawlingSpecification; +import nu.marginalia.wmsa.edge.crawling.retreival.CrawlerRetreiver; +import nu.marginalia.wmsa.edge.crawling.retreival.HttpFetcher; +import nu.marginalia.util.ParallelPipe; +import nu.marginalia.wmsa.edge.model.EdgeCrawlPlan; +import okhttp3.Dispatcher; +import okhttp3.internal.Util; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.nio.file.Path; +import java.util.concurrent.Semaphore; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class CrawlerMain implements AutoCloseable { + public static Gson gson = new GsonBuilder().create(); + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final Path inputSpec; + + private final WorkLog workLog; + private final CrawledDomainWriter domainWriter; + + private final int numberOfThreads; + private final ParallelPipe pipe; + private final Dispatcher dispatcher = new Dispatcher(new ThreadPoolExecutor(0, Integer.MAX_VALUE, 5, TimeUnit.SECONDS, + new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", true))); + + + public CrawlerMain(EdgeCrawlPlan plan) throws Exception { + this.inputSpec = plan.getJobSpec(); + this.numberOfThreads = 512; + + workLog = new WorkLog(plan.crawl.getLogFile()); + domainWriter = new CrawledDomainWriter(plan.crawl.getDir()); + + Semaphore sem = new Semaphore(250_000); + + pipe = new ParallelPipe<>("Crawler", numberOfThreads, 2, 1) { + @Override + protected CrawledDomain onProcess(CrawlingSpecification crawlingSpecification) throws Exception { + int toAcquire = crawlingSpecification.urls.size(); + sem.acquire(toAcquire); + try { + return fetchDomain(crawlingSpecification); + } + finally { + sem.release(toAcquire); + } + } + + @Override + protected void onReceive(CrawledDomain crawledDomain) throws IOException { + writeDomain(crawledDomain); + } + }; + } + + public static void main(String... args) throws Exception { + if (!AbortMonitor.getInstance().isAlive()) { + System.err.println("Remove abort file first"); + return; + } + + if (args.length != 1) { + System.err.println("Arguments: crawl-plan.yaml"); + System.exit(0); + } + var plan = new CrawlPlanLoader().load(Path.of(args[0])); + + try (var crawler = new CrawlerMain(plan)) { + crawler.run(); + } + } + + private CrawledDomain fetchDomain(CrawlingSpecification specification) { + if (workLog.isJobFinished(specification.id)) + return null; + + var fetcher = new HttpFetcher("search.marginalia.nu", dispatcher); + + try { + var retreiver = new CrawlerRetreiver(fetcher, specification); + + return retreiver.fetch(); + } catch (Exception e) { + logger.error("Error fetching domain", e); + return null; + } + } + + private void writeDomain(CrawledDomain crawledDomain) throws IOException { + String name = domainWriter.accept(crawledDomain); + workLog.setJobToFinished(crawledDomain.id, name, crawledDomain.size()); + } + + public void run() throws InterruptedException { + // First a validation run to ensure the file is all good to parse + + logger.info("Validating JSON"); + CrawlerSpecificationLoader.readInputSpec(inputSpec, spec -> {}); + + logger.info("Starting pipe"); + CrawlerSpecificationLoader.readInputSpec(inputSpec, pipe::accept); + + if (!AbortMonitor.getInstance().isAlive()) { + logger.info("Aborting"); + pipe.clearQueues(); + } + else { + logger.info("All jobs queued, waiting for pipe to finish"); + } + pipe.join(); + + logger.info("All finished"); + } + + public void close() throws Exception { + workLog.close(); + dispatcher.executorService().shutdownNow(); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlerSpecificationLoader.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlerSpecificationLoader.java new file mode 100644 index 00000000..c9e6afc1 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/CrawlerSpecificationLoader.java @@ -0,0 +1,32 @@ +package nu.marginalia.wmsa.edge.crawling; + +import com.github.luben.zstd.ZstdInputStream; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import nu.marginalia.wmsa.edge.crawling.model.CrawlingSpecification; +import org.apache.logging.log4j.util.Strings; + +import java.io.*; +import java.nio.file.Path; +import java.util.function.Consumer; + +public class CrawlerSpecificationLoader { + private final static Gson gson = new GsonBuilder().create(); + + public static void readInputSpec(Path inputSpec, Consumer consumer) { + try (var inputStream = new BufferedReader(new InputStreamReader(new ZstdInputStream(new FileInputStream(inputSpec.toFile()))))) { + + for (;;) { + var line = inputStream.readLine(); + if (line == null || !AbortMonitor.getInstance().isAlive()) + break; + + if (Strings.isNotBlank(line)) { + consumer.accept(gson.fromJson(line, CrawlingSpecification.class)); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/WorkLog.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/WorkLog.java new file mode 100644 index 00000000..276d3651 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/WorkLog.java @@ -0,0 +1,86 @@ +package nu.marginalia.wmsa.edge.crawling; + +import nu.marginalia.wmsa.edge.crawling.model.CrawlLogEntry; +import org.apache.logging.log4j.util.Strings; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +public class WorkLog implements AutoCloseable { + private final Set finishedJobs = new HashSet<>(); + private final FileOutputStream logWriter; + + public WorkLog(Path logFile) throws IOException { + loadLog(logFile); + + logWriter = new FileOutputStream(logFile.toFile(), true); + writeLogEntry("# Starting WorkLog @ " + LocalDateTime.now()); + } + + public static void readLog(Path logFile, Consumer entryConsumer) { + if (!Files.exists(logFile)) { + return; + } + + try (var lines = Files.lines(logFile)) { + lines.filter(WorkLog::isJobId).map(line -> { + String[] parts = line.split("\\s+"); + return new CrawlLogEntry(parts[0], parts[1], parts[2], Integer.parseInt(parts[3])); + }).forEach(entryConsumer); + } catch (IOException e) { + e.printStackTrace(); + } + } + private void loadLog(Path logFile) throws IOException { + if (!Files.exists(logFile)) { + return; + } + + try (var lines = Files.lines(logFile)) { + lines.filter(WorkLog::isJobId).map(this::getJobIdFromWrittenString).forEach(finishedJobs::add); + } + } + + private static boolean isJobId(String s) { + return Strings.isNotBlank(s) && !s.startsWith("#"); + } + + private static final Pattern splitPattern = Pattern.compile("\\s+"); + + private String getJobIdFromWrittenString(String s) { + return splitPattern.split(s, 2)[0]; + } + + public synchronized boolean isJobFinished(String id) { + return finishedJobs.contains(id); + } + + // Use synchro over concurrent set to avoid competing writes + // - correct is better than fast here, it's sketchy enough to use + // a PrintWriter + + public synchronized void setJobToFinished(String id, String where, int size) throws IOException { + finishedJobs.add(id); + + writeLogEntry(String.format("%s\t%s\t%s\t%d",id, LocalDateTime.now(), where, size)); + } + + private void writeLogEntry(String entry) throws IOException { + logWriter.write(entry.getBytes(StandardCharsets.UTF_8)); + logWriter.write("\n".getBytes(StandardCharsets.UTF_8)); + logWriter.flush(); + } + + @Override + public void close() throws Exception { + logWriter.flush(); + logWriter.close(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawlLogEntry.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawlLogEntry.java new file mode 100644 index 00000000..cdad9a70 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawlLogEntry.java @@ -0,0 +1,4 @@ +package nu.marginalia.wmsa.edge.crawling.model; + +public record CrawlLogEntry(String id, String ts, String path, int cnt) { +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawledDocument.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawledDocument.java new file mode 100644 index 00000000..5d8d7e54 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawledDocument.java @@ -0,0 +1,25 @@ +package nu.marginalia.wmsa.edge.crawling.model; + +import lombok.Builder; + +@Builder +public class CrawledDocument { + public String crawlId; + + public String url; + public String contentType; + + public String timestamp; + public int httpStatus; + + public String crawlerStatus; + public String crawlerStatusDesc; + + public String headers; + public String documentBody; + + public String documentBodyHash; + + public String canonicalUrl; + public String redirectUrl; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawledDomain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawledDomain.java new file mode 100644 index 00000000..1a0a3f46 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawledDomain.java @@ -0,0 +1,27 @@ +package nu.marginalia.wmsa.edge.crawling.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@AllArgsConstructor @Data @Builder +public class CrawledDomain { + public String id; + public String domain; + + public String redirectDomain; + + public String crawlerStatus; + public String crawlerStatusDesc; + public String ip; + + public List doc; + public List cookies; + + public int size() { + if (doc == null) return 0; + return doc.size(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawlerDocumentStatus.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawlerDocumentStatus.java new file mode 100644 index 00000000..38f17abd --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawlerDocumentStatus.java @@ -0,0 +1,10 @@ +package nu.marginalia.wmsa.edge.crawling.model; + +public enum CrawlerDocumentStatus { + OK, + BAD_CONTENT_TYPE, + BAD_CHARSET, + REDIRECT, + ROBOTS_TXT, + ERROR +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawlerDomainStatus.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawlerDomainStatus.java new file mode 100644 index 00000000..1c22067c --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawlerDomainStatus.java @@ -0,0 +1,5 @@ +package nu.marginalia.wmsa.edge.crawling.model; + +public enum CrawlerDomainStatus { + OK, ERROR, BLOCKED, REDIRECT +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawlingSpecification.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawlingSpecification.java new file mode 100644 index 00000000..d55cd2bb --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/model/CrawlingSpecification.java @@ -0,0 +1,13 @@ +package nu.marginalia.wmsa.edge.crawling.model; + +import java.util.List; + +public class CrawlingSpecification { + public String id; + + public int crawlDepth; + + // Don't make this EdgeUrl, EdgeDomain etc. -- we want this plastic to change! + public String domain; + public List urls; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/retreival/CrawlerRetreiver.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/retreival/CrawlerRetreiver.java new file mode 100644 index 00000000..467376f5 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/retreival/CrawlerRetreiver.java @@ -0,0 +1,297 @@ +package nu.marginalia.wmsa.edge.crawling.retreival; + +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.crawler.domain.LinkParser; +import nu.marginalia.wmsa.edge.crawler.worker.GeoIpBlocklist; +import nu.marginalia.wmsa.edge.crawler.worker.IpBlockList; +import nu.marginalia.wmsa.edge.crawler.worker.UrlBlocklist; +import nu.marginalia.wmsa.edge.crawling.model.*; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.LocalDateTime; +import java.util.*; + +public class CrawlerRetreiver { + private static final long DEFAULT_CRAWL_DELAY_MS = 1000; + private final LinkedList queue = new LinkedList<>(); + private final HttpFetcher fetcher; + private final HashSet visited; + private final HashSet known; + + private final int depth; + private final String id; + private final String domain; + + private static final LinkParser linkParser = new LinkParser(); + private static final Logger logger = LoggerFactory.getLogger(CrawlerRetreiver.class); + + private static final HashFunction hashMethod = Hashing.murmur3_128(0); + private static final IpBlockList ipBlocklist; + private static final UrlBlocklist urlBlocklist = new UrlBlocklist(); + + static { + try { + ipBlocklist = new IpBlockList(new GeoIpBlocklist()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public CrawlerRetreiver(HttpFetcher fetcher, CrawlingSpecification specs) { + this.fetcher = fetcher; + visited = new HashSet<>((int)(specs.urls.size() * 1.5)); + known = new HashSet<>(specs.urls.size() * 10); + + depth = specs.crawlDepth; + id = specs.id; + domain = specs.domain; + + specs.urls.stream() + .map(this::parseUrl) + .filter(Optional::isPresent) + .map(Optional::get) + .filter(known::add) + .forEach(queue::addLast); + + if (queue.peek() != null) { + var fst = queue.peek(); + var root = new EdgeUrl(fst.proto, fst.domain, fst.port, "/"); + if (known.add(root)) + queue.addFirst(root); + } + } + + private Optional parseUrl(String str) { + try { + return Optional.of(new EdgeUrl(str)); + } + catch (Exception ex) { + return Optional.empty(); + } + } + + public CrawledDomain fetch() { + logger.info("Fetching {}", domain); + + Optional probeResult = probeDomainForProblems(domain); + + return probeResult.orElseGet(this::crawlDomain); + } + + private Optional probeDomainForProblems(String domain) { + EdgeUrl fst = queue.peek(); + + + if (fst == null) { + logger.warn("No URLs for domain {}", domain); + + return Optional.of(CrawledDomain.builder() + .crawlerStatus(CrawlerDomainStatus.ERROR.name()) + .crawlerStatusDesc("No known URLs") + .id(id) + .domain(domain) + .build()); + } + + if (!ipBlocklist.isAllowed(fst.domain)) { + return Optional.of(CrawledDomain.builder() + .crawlerStatus(CrawlerDomainStatus.BLOCKED.name()) + .id(id) + .domain(domain) + .ip(findIp(domain)) + .build()); + } + + var fetchResult = fetcher.probeDomain(new EdgeUrl(fst.proto, fst.domain, fst.port, "/")); + if (!fetchResult.ok()) { + logger.debug("Bad status on {}", domain); + return Optional.of(createErrorPostFromStatus(fetchResult)); + } + return Optional.empty(); + } + + private CrawledDomain crawlDomain() { + String ip = findIp(domain); + + var robotsRules = fetcher.fetchRobotRules(queue.peek().domain); + long crawlDelay = robotsRules.getCrawlDelay(); + + List docs = new ArrayList<>(depth); + CrawledDomain ret = new CrawledDomain(id, domain, null, CrawlerDomainStatus.OK.name(), null, ip, docs, null); + + int visitedCount = 0; + while (!queue.isEmpty() && visitedCount < depth) { + var top = queue.removeFirst(); + + if (!robotsRules.isAllowed(top.toString())) { + ret.doc.add(createRobotsError(top)); + continue; + } + + if (urlBlocklist.isUrlBlocked(top)) + continue; + if (top.toString().length() > 255) + continue; + + if (!visited.add(top)) { + continue; + } + + logger.debug("Fetching {}", top); + long startTime = System.currentTimeMillis(); + + fetchUrl(top).ifPresent(ret.doc::add); + + long crawledTime = System.currentTimeMillis() - startTime; + delay(crawlDelay, crawledTime); + + visitedCount ++; + } + + ret.cookies = fetcher.getCookies(); + + return ret; + } + + private Optional fetchUrl(EdgeUrl top) { + try { + + var doc = fetcher.fetchContent(top); + + if (doc.documentBody != null) { + + doc.documentBodyHash = createHash(doc.documentBody); + + Optional parsedDoc = parseDoc(doc); + EdgeUrl url = new EdgeUrl(doc.url); + + parsedDoc.ifPresent(parsed -> findLinks(url, parsed)); + parsedDoc.flatMap(parsed -> findCanonicalUrl(url, parsed)) + .ifPresent(canonicalLink -> doc.canonicalUrl = canonicalLink.toString()); + } + + return Optional.of(doc); + } + catch (Exception ex) { + logger.warn("Failed to process document {}", top); + } + + return Optional.empty(); + + } + + private String createHash(String documentBodyHash) { + return hashMethod.hashUnencodedChars(documentBodyHash).toString(); + } + + private Optional parseDoc(CrawledDocument doc) { + if (doc.documentBody == null) + return Optional.empty(); + return Optional.of(Jsoup.parse(doc.documentBody)); + } + + public boolean isSameDomain(EdgeUrl url) { + return domain.equals(url.domain.toString().toLowerCase()); + } + + private void findLinks(EdgeUrl url, Document parsed) { + + for (var link : parsed.getElementsByTag("a")) { + linkParser.parseLink(url, link) + .filter(this::isSameDomain) + .filter(u -> !urlBlocklist.isUrlBlocked(u)) + .filter(u -> !urlBlocklist.isForumLink(u)) + .filter(known::add) + .ifPresent(queue::addLast); + } + for (var link : parsed.getElementsByTag("frame")) { + linkParser.parseFrame(url, link) + .filter(this::isSameDomain) + .filter(u -> !urlBlocklist.isUrlBlocked(u)) + .filter(u -> !urlBlocklist.isForumLink(u)) + .filter(known::add) + .ifPresent(queue::addLast); + } + for (var link : parsed.getElementsByTag("iframe")) { + linkParser.parseFrame(url, link) + .filter(this::isSameDomain) + .filter(u -> !urlBlocklist.isUrlBlocked(u)) + .filter(u -> !urlBlocklist.isForumLink(u)) + .filter(known::add) + .ifPresent(queue::addLast); + } + } + + private Optional findCanonicalUrl(EdgeUrl url, Document parsed) { + + for (var link : parsed.select("link[rel=canonical]")) { + return linkParser.parseLink(url, link); + } + + return Optional.empty(); + } + + private String findIp(String domain) { + try { + return InetAddress.getByName(domain).getHostAddress(); + } catch (UnknownHostException e) { + return ""; + } + } + + @SneakyThrows + private void delay(long crawlDelay, long timeParsed) { + if (crawlDelay >= 1) { + if (timeParsed/1000 > crawlDelay) + return; + + Thread.sleep(Math.min(1000*crawlDelay-timeParsed, 5000)); + } + else { + if (timeParsed > DEFAULT_CRAWL_DELAY_MS) + return; + + Thread.sleep(DEFAULT_CRAWL_DELAY_MS - timeParsed); + } + } + + private CrawledDocument createRobotsError(EdgeUrl url) { + return CrawledDocument.builder() + .url(url.toString()) + .timestamp(LocalDateTime.now().toString()) + .httpStatus(-1) + .crawlerStatus(CrawlerDocumentStatus.ROBOTS_TXT.name()) + .build(); + } + + private CrawledDomain createErrorPostFromStatus(HttpFetcher.FetchResult ret) { + String ip = findIp(domain); + + if (ret.state == HttpFetcher.FetchResultState.ERROR) { + return CrawledDomain.builder() + .crawlerStatus(CrawlerDomainStatus.ERROR.name()) + .id(id).domain(domain) + .ip(ip) + .build(); + } + if (ret.state == HttpFetcher.FetchResultState.REDIRECT) { + return CrawledDomain.builder() + .crawlerStatus(CrawlerDomainStatus.REDIRECT.name()) + .id(id) + .domain(domain) + .redirectDomain(ret.domain.toString()) + .ip(ip) + .build(); + } + throw new AssertionError("Unexpected case"); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/retreival/HttpFetcher.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/retreival/HttpFetcher.java new file mode 100644 index 00000000..e3260608 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/crawling/retreival/HttpFetcher.java @@ -0,0 +1,305 @@ +package nu.marginalia.wmsa.edge.crawling.retreival; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import crawlercommons.robots.SimpleRobotRules; +import crawlercommons.robots.SimpleRobotRulesParser; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import lombok.ToString; +import nu.marginalia.wmsa.edge.crawler.domain.LinkParser; +import nu.marginalia.wmsa.edge.crawler.fetcher.ContentTypeParser; +import nu.marginalia.wmsa.edge.crawler.fetcher.Cookies; +import nu.marginalia.wmsa.edge.crawler.fetcher.NoSecuritySSL; +import nu.marginalia.wmsa.edge.crawling.model.CrawledDocument; +import nu.marginalia.wmsa.edge.crawling.model.CrawlerDocumentStatus; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import okhttp3.Dispatcher; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.apache.commons.io.input.BOMInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.X509TrustManager; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.zip.GZIPInputStream; + +public class HttpFetcher { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final String userAgent; + private final int maxFetchSize = 1024*512; + private Cookies cookies = new Cookies(); + + private static final SimpleRobotRulesParser robotsParser = new SimpleRobotRulesParser(); + + private final LinkParser linkParser = new LinkParser(); + + public void setAllowAllContentTypes(boolean allowAllContentTypes) { + this.allowAllContentTypes = allowAllContentTypes; + } + + private boolean allowAllContentTypes = false; + + private final OkHttpClient client; + + public enum FetchResultState { + OK, + REDIRECT, + ERROR; + }; + + @AllArgsConstructor @ToString + public static class FetchResult { + public final FetchResultState state; + public final EdgeDomain domain; + + public boolean ok() { + return state == FetchResultState.OK; + } + }; + + @SneakyThrows + private OkHttpClient createClient(Dispatcher dispatcher) { + return new OkHttpClient.Builder() + .dispatcher(dispatcher) + .sslSocketFactory(NoSecuritySSL.buildSocketFactory(), (X509TrustManager) NoSecuritySSL.trustAllCerts[0]) + .hostnameVerifier(NoSecuritySSL.buildHostnameVerifyer()) + .cookieJar(cookies.getJar()) + .followRedirects(true) + .followSslRedirects(true) + .connectTimeout(8, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .build(); + } + + public List getCookies() { + return cookies.getCookies(); + } + + public void clearCookies() { + cookies.clear(); + } + + @Inject + public HttpFetcher(@Named("user-agent") String userAgent, Dispatcher dispatcher) { + this.client = createClient(dispatcher); + this.userAgent = userAgent; + } + + @SneakyThrows + public FetchResult probeDomain(EdgeUrl url) { + var head = new Request.Builder().head().addHeader("User-agent", userAgent) + .url(new EdgeUrl(url.proto, url.domain, url.port, "/").toString()) + .build(); + + var call = client.newCall(head); + + try (var rsp = call.execute()) { + var requestUrl = rsp.request().url().toString(); + EdgeDomain requestDomain = new EdgeUrl(requestUrl).domain; + + if (!Objects.equals(requestDomain, url.domain)) { + return new FetchResult(FetchResultState.REDIRECT, requestDomain); + } + return new FetchResult(FetchResultState.OK, requestDomain); + } + catch (Exception ex) { + return new FetchResult(FetchResultState.ERROR, url.domain); + } + } + + private Request createHeadRequest(EdgeUrl url) { + return new Request.Builder().head().addHeader("User-agent", userAgent) + .url(url.toString()) + .addHeader("Accept-Encoding", "gzip") + .build(); + } + + private Request createGetRequest(EdgeUrl url) { + return new Request.Builder().get().addHeader("User-agent", userAgent) + .url(url.toString()) + .addHeader("Accept-Encoding", "gzip") + .build(); + + } + + @SneakyThrows + public CrawledDocument fetchContent(EdgeUrl url) { + if (isUrlLikeBinary(url)) { + + logger.debug("Probing suspected binary {}", url); + + var head = createHeadRequest(url); + var call = client.newCall(head); + + try (var rsp = call.execute()) { + var contentTypeHeader = rsp.header("Content-type"); + if (contentTypeHeader != null && !isAllowableContentType(contentTypeHeader)) { + return createErrorResponse(url, rsp, CrawlerDocumentStatus.BAD_CONTENT_TYPE, "Early probe failed"); + } + } + catch (Exception ex) { + return createHardErrorRsp(url, ex); + } + } + + var get = createGetRequest(url); + var call = client.newCall(get); + + + + + try (var rsp = call.execute()) { + return extractBody(url, rsp); + } + catch (Exception ex) { + return createHardErrorRsp(url, ex); + } + } + + private CrawledDocument createHardErrorRsp(EdgeUrl url, Exception why) { + return CrawledDocument.builder() + .crawlerStatus(CrawlerDocumentStatus.ERROR.toString()) + .crawlerStatusDesc(why.getClass().getSimpleName() + ": " + why.getMessage()) + .timestamp(LocalDateTime.now().toString()) + .url(url.toString()) + .build(); + } + + private CrawledDocument createErrorResponse(EdgeUrl url, Response rsp, CrawlerDocumentStatus status, String why) { + return CrawledDocument.builder() + .crawlerStatus(status.toString()) + .crawlerStatusDesc(why) + .headers(rsp.headers().toString()) + .contentType(rsp.header("Content-type")) + .timestamp(LocalDateTime.now().toString()) + .httpStatus(rsp.code()) + .url(url.toString()) + .build(); + } + + private CrawledDocument extractBody(EdgeUrl url, Response rsp) throws IOException, URISyntaxException { + + var responseUrl = new EdgeUrl(rsp.request().url().toString()); + if (!responseUrl.equals(url)) { + return createRedirectResponse(url, rsp, responseUrl); + } + + var body = rsp.body(); + if (null == body) { + return createErrorResponse(url, rsp, CrawlerDocumentStatus.ERROR, "No body"); + } + + var byteStream = body.byteStream(); + if (null == byteStream) { + return createErrorResponse(url, rsp, CrawlerDocumentStatus.ERROR, "No body"); + } + if ("gzip".equals(rsp.header("Content-encoding"))) { + byteStream = new GZIPInputStream(byteStream); + } + byteStream = new BOMInputStream(byteStream); + + var contentTypeHeader = rsp.header("Content-type"); + if (contentTypeHeader != null && !isAllowableContentType(contentTypeHeader)) { + return createErrorResponse(url, rsp, CrawlerDocumentStatus.BAD_CONTENT_TYPE, ""); + } + + byte[] data = byteStream.readNBytes(maxFetchSize); + + var contentType = ContentTypeParser.parse(contentTypeHeader, data); + if (!isAllowableContentType(contentType.contentType)) { + return createErrorResponse(url, rsp, CrawlerDocumentStatus.BAD_CONTENT_TYPE, ""); + } + + if ("Shift_JIS".equalsIgnoreCase(contentType.charset)) { + return createErrorResponse(url, rsp, CrawlerDocumentStatus.BAD_CHARSET, ""); + } + + var strData = new String(data, Charset.forName(contentType.charset)); + var canonical = rsp.header("rel=canonical", ""); + + return CrawledDocument.builder() + .crawlerStatus(CrawlerDocumentStatus.OK.name()) + .headers(rsp.headers().toString()) + .contentType(rsp.header("Content-type")) + .timestamp(LocalDateTime.now().toString()) + .canonicalUrl(canonical) + .httpStatus(rsp.code()) + .url(url.toString()) + .documentBody(strData) + .build(); + } + + private CrawledDocument createRedirectResponse(EdgeUrl url, Response rsp, EdgeUrl responseUrl) { + + return CrawledDocument.builder() + .crawlerStatus(CrawlerDocumentStatus.REDIRECT.name()) + .redirectUrl(responseUrl.toString()) + .headers(rsp.headers().toString()) + .contentType(rsp.header("Content-type")) + .timestamp(LocalDateTime.now().toString()) + .httpStatus(rsp.code()) + .url(url.toString()) + .build(); + + } + + + private final Predicate probableHtmlPattern = Pattern.compile("^.*\\.(htm|html|php|txt)(\\?.*)?$").asPredicate(); + private final Predicate probableBinaryPattern = Pattern.compile("^.*\\.[a-z]+$").asPredicate(); + + public boolean isUrlLikeBinary(EdgeUrl url) { + String urlString = url.toString().toLowerCase(); + + return (!probableHtmlPattern.test(urlString) && probableBinaryPattern.test(urlString)); + } + + private boolean isAllowableContentType(String contentType) { + return allowAllContentTypes || contentType.startsWith("text") + || contentType.startsWith("application/xhtml") + || contentType.startsWith("application/xml") + || contentType.startsWith("application/atom+xml") + || contentType.startsWith("application/rss+xml") + || contentType.startsWith("application/x-rss+xml") + || contentType.startsWith("application/rdf+xml") + || contentType.startsWith("x-rss+xml"); + } + + public SimpleRobotRules fetchRobotRules(EdgeDomain domain) { + return fetchRobotsForProto("https", domain) + .or(() -> fetchRobotsForProto("http", domain)) + .orElseGet(() -> new SimpleRobotRules(SimpleRobotRules.RobotRulesMode.ALLOW_ALL)); + } + + private Optional fetchRobotsForProto(String proto, EdgeDomain domain) { + try { + var url = new EdgeUrl(proto, domain, null, "/robots.txt"); + return Optional.of(parseRobotsTxt(fetchContent(url))); + } + catch (Exception ex) { + return Optional.empty(); + } + } + + private SimpleRobotRules parseRobotsTxt(CrawledDocument doc) { + return robotsParser.parseContent(doc.url, + doc.documentBody.getBytes(StandardCharsets.UTF_8), + doc.contentType, + userAgent); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/EdgeDataStoreDao.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/EdgeDataStoreDao.java new file mode 100644 index 00000000..9b1ea05e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/EdgeDataStoreDao.java @@ -0,0 +1,65 @@ +package nu.marginalia.wmsa.edge.data.dao; + +import com.google.inject.ImplementedBy; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDomainBlacklist; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainLink; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlVisit; +import nu.marginalia.wmsa.edge.model.search.EdgeUrlDetails; +import nu.marginalia.wmsa.edge.search.BrowseResult; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +@ImplementedBy(EdgeDataStoreDaoImpl.class) +public interface EdgeDataStoreDao { + void putUrl(double quality, EdgeUrl... url); + void putFeeds(EdgeUrl... url); + + void putUrlVisited(EdgeUrlVisit... urls); + + void putLink(boolean wipeExisting, EdgeDomainLink... links); + boolean isBlacklisted(EdgeDomain domain); + + EdgeId getDomainId(EdgeDomain domain); + EdgeId getUrlId(EdgeUrl domain); + EdgeUrl getUrl(EdgeId id); + EdgeUrlDetails getUrlDetails(EdgeId id); + + List getDomainNeighbors(EdgeId domainId, EdgeDomainBlacklist backlist, int count); + List getDomainNeighborsAdjacent(EdgeId domainId, EdgeDomainBlacklist backlist, int count); + List getRandomDomains(int count, EdgeDomainBlacklist backlist); + List getUrlDetailsMulti(List> ids); + List> getDomainIdsFromUrlIds(Collection> urlIds); + + + EdgeDomain getDomain(EdgeId id); + + List> inboudUrls(EdgeId id, int limit); + List> outboundUrls(EdgeId id, int limit); + + Optional> resolveAmbiguousDomain(String name); + + void putDomainAlias(EdgeDomain src, EdgeDomain dst); + + int getPagesKnown(EdgeId domainId); + int getPagesVisited(EdgeId domainId); + int getPagesIndexed(EdgeId domainId); + + int getIncomingLinks(EdgeId domainId); + int getOutboundLinks(EdgeId domainId); + + double getDomainQuality(EdgeId domainId); + + EdgeDomainIndexingState getDomainState(EdgeId domainId); + + List getLinkingDomains(EdgeId domainId); + + List getNewUrls(EdgeId domainId, Collection links); + + double getRank(EdgeId domainId); + + void updateDomainIndexTimestamp(EdgeDomain domain, EdgeDomainIndexingState state, EdgeDomain alias, int minIndexed); +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/EdgeDataStoreDaoImpl.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/EdgeDataStoreDaoImpl.java new file mode 100644 index 00000000..58870ffa --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/EdgeDataStoreDaoImpl.java @@ -0,0 +1,1180 @@ +package nu.marginalia.wmsa.edge.data.dao; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.util.concurrent.UncheckedExecutionException; +import com.google.inject.Inject; +import com.zaxxer.hikari.HikariDataSource; +import edu.stanford.nlp.parser.lexparser.Edge; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.crawler.domain.UrlsCache; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDomainBlacklist; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainLink; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlVisit; +import nu.marginalia.wmsa.edge.model.search.EdgePageScoreAdjustment; +import nu.marginalia.wmsa.edge.model.search.EdgeUrlDetails; +import nu.marginalia.wmsa.edge.search.BrowseResult; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.*; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static nu.marginalia.wmsa.edge.crawler.worker.UploaderWorker.QUALITY_LOWER_BOUND_CUTOFF; + +public class EdgeDataStoreDaoImpl implements EdgeDataStoreDao { + private static final int DB_LOCK_RETRIES = 3; + + private final HikariDataSource dataSource; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final Cache> urlIdCache = CacheBuilder.newBuilder().maximumSize(100_000).build(); + private final Cache> domainIdCache = CacheBuilder.newBuilder().maximumSize(10_000).build(); + + private final UrlsCache URLS_INSERTED_CACHE = new UrlsCache<>(); + private final UrlsCache DOMAINS_INSERTED_CACHE = new UrlsCache<>(); + + private static final String DEFAULT_PROTOCOL = "http"; + + @Inject + public EdgeDataStoreDaoImpl(HikariDataSource dataSource) + { + this.dataSource = dataSource; + } + + + public synchronized void clearCaches() + { + urlIdCache.invalidateAll(); + domainIdCache.invalidateAll(); + URLS_INSERTED_CACHE.clear(); + DOMAINS_INSERTED_CACHE.clear(); + } + + @Override + @SneakyThrows + public void putUrl(double quality, EdgeUrl... urls) { + if (quality > 0.5) { + logger.warn("Put URL q={} {}", quality, urls); + } + + if (urls.length == 0) { + return; + } + + try (var connection = dataSource.getConnection()) { + + connection.setAutoCommit(false); + + for (int i = 0; i < DB_LOCK_RETRIES; i++) { + try { + var domains = Arrays.stream(urls) + .map(EdgeUrl::getDomain) + .distinct().toArray(EdgeDomain[]::new); + + insert(connection, domains, quality); + insert(connection, urls); + connection.commit(); + break; + } catch (Exception ex) { + logger.error("DB error", ex); + connection.rollback(); + } finally { + connection.setAutoCommit(true); + } + } + } + } + + + @Override + @SneakyThrows + public void putFeeds(EdgeUrl... urls) { + if (urls.length == 0) { + return; + } + + try (var connection = dataSource.getConnection()) { + connection.setAutoCommit(false); + + for (int i = 0; i < DB_LOCK_RETRIES; i++) { + try { + insertFeed(connection, urls); + connection.commit(); + break; + } catch (Exception ex) { + logger.error("DB error", ex); + connection.rollback(); + } + finally { + connection.setAutoCommit(true); + } + } + } + } + + @Override + @SneakyThrows + public void putUrlVisited(EdgeUrlVisit... urls) { + if (urls.length == 0) { + return; + } + + try (var connection = dataSource.getConnection()) { + + connection.setAutoCommit(false); + + for (int i = 0; i < DB_LOCK_RETRIES; i++) { + try { + insert(connection, Arrays.stream(urls).map(url -> url.getUrl().domain).toArray(EdgeDomain[]::new), Optional.ofNullable(urls[0].quality).orElse(-2.)); + visited(connection, urls); + connection.commit(); + break; + } catch (Exception ex) { + logger.error("DB error", ex); + connection.rollback(); + } finally { + connection.setAutoCommit(true); + } + } + } + } + + @SneakyThrows + private void insert(Connection connection, EdgeUrl[] urls) { + + EdgeUrl[] toCommitUrls = Arrays + .stream(urls) + .filter(URLS_INSERTED_CACHE::isMissing) + .sorted(Comparator.comparing(url -> url.toString().length())) + .distinct() + .toArray(EdgeUrl[]::new); + + int size = 0; + try (var stmt = + connection.prepareStatement("INSERT IGNORE INTO EC_URL (URL, DOMAIN_ID, PROTO, PORT) SELECT ? AS URL, ID, ?, ? AS DOMAIN_ID FROM EC_DOMAIN WHERE URL_PART=?")) { + for (var url : toCommitUrls) { + logger.trace("insert({})", url); + + if (url.path.length() > 255) { + logger.warn("(insert) URL too long: {}", url); + continue; + } + + stmt.setString(1, url.path); + stmt.setString(2, url.proto); + if (url.port != null) { + stmt.setInt(3, url.port); + } + else { + stmt.setNull(3, Types.INTEGER); + } + stmt.setString(4, url.domain.toString()); + stmt.addBatch(); + if ((++size % 100) == 0) { + int[] status = stmt.executeBatch(); + checkExecuteStatus("insert", status); + } + } + int[] status = stmt.executeBatch(); + checkExecuteStatus("insert", status); + + URLS_INSERTED_CACHE.addAll(toCommitUrls); + } + + } + + @SneakyThrows + private void visited(Connection connection, EdgeUrlVisit[] visits) { + int size = 0; + + try (var stmt = + connection.prepareStatement( + "UPDATE EC_URL INNER JOIN EC_DOMAIN ON DOMAIN_ID=EC_DOMAIN.ID " + + "SET QUALITY_MEASURE=?, DATA_HASH=?, IP=?, EC_URL.STATE=?, VISITED=TRUE " + + " WHERE URL_PART=? AND URL=?")) { + for (var visit : visits) { + logger.trace("(visit) insert({})", visit); + + if (visit.url.path.length() > 255) { + logger.warn("URL too long: {}", visit.url); + continue; + } + + if (visit.quality != null) { + stmt.setDouble(1, visit.quality); + } else { + stmt.setNull(1, Types.DOUBLE); + } + + if (visit.data_hash_code != null) { + stmt.setInt(2, visit.data_hash_code); + } else { + stmt.setNull(2, Types.INTEGER); + } + + stmt.setString(3, visit.ipAddress); + stmt.setString(4, visit.urlState.toString()); + stmt.setString(5, visit.url.domain.toString()); + stmt.setString(6, visit.url.path); + stmt.addBatch(); + if ((++size % 100) == 0) { + int[] status = stmt.executeBatch(); + checkExecuteStatus("set-visited", status); + } + } + var status = stmt.executeBatch(); + + checkExecuteStatus("set-visited", status); + } + + try (var stmt = + connection.prepareStatement("REPLACE INTO EC_PAGE_DATA (ID, TITLE, DESCRIPTION, WORDS_DISTINCT, WORDS_TOTAL, FORMAT, FEATURES) SELECT ID, ?,?,?,?,?,? FROM EC_URL_VIEW WHERE URL_DOMAIN=? AND URL_PATH=? AND URL_PROTO=? AND IFNULL(URL_PORT,-1)=IFNULL(?,-1)")) { + for (var visit : visits) { + + + if (visit.title != null) { + stmt.setString(1, StringUtils.truncate(visit.title, 255)); + } + else { + stmt.setNull(1, Types.VARCHAR); + } + + if (visit.description != null) { + stmt.setString(2, StringUtils.truncate(visit.description, 255)); + } + else { + stmt.setNull(2, Types.VARCHAR); + } + + stmt.setInt(3, visit.wordCountDistinct); + stmt.setInt(4, visit.wordCountTotal); + stmt.setString(5, visit.format); + stmt.setInt(6, visit.features); + stmt.setString(7, visit.url.domain.toString()); + stmt.setString(8, visit.url.path); + stmt.setString(9, visit.url.proto); + if (visit.url.port == null) { + stmt.setNull(10, Types.INTEGER); + } else { + stmt.setInt(10, visit.url.port); + } + stmt.addBatch(); + } + var status = stmt.executeBatch(); + checkExecuteStatus("set-visited2", status); + + } + } + + private void checkExecuteStatus(String operation, int[] status) { + } + + @SneakyThrows + private void insertFeed(Connection connection, EdgeUrl[] urls) { + + int size = 0; + try (var stmt = + connection.prepareStatement("INSERT IGNORE INTO EC_FEED_URL (URL, DOMAIN_ID, PROTO, PORT) SELECT ? AS URL, ID, ?, ? AS DOMAIN_ID FROM EC_DOMAIN WHERE URL_PART=?")) { + for (var url : urls) { + logger.trace("insert({})", url); + + if (url.path.length() > 255) { + logger.warn("(insert) URL too long: {}", url); + continue; + } + + stmt.setString(1, url.path); + stmt.setString(2, url.proto); + if (url.port != null) { + stmt.setInt(3, url.port); + } + else { + stmt.setNull(3, Types.INTEGER); + } + stmt.setString(4, url.domain.toString()); + stmt.addBatch(); + if ((++size % 100) == 0) { + int[] status = stmt.executeBatch(); + checkExecuteStatus("insert", status); + } + } + int[] status = stmt.executeBatch(); + checkExecuteStatus("insert", status); + } + + } + @SneakyThrows + private void insert(Connection connection, EdgeDomain[] domains, double quality) { + EdgeDomain[] toCommitDomains = Arrays.stream(domains).filter(DOMAINS_INSERTED_CACHE::isMissing).distinct().toArray(EdgeDomain[]::new); + + try (var stmt = + connection.prepareStatement("INSERT IGNORE INTO EC_TOP_DOMAIN (URL_PART) VALUES (?)")) { + for (var domain : toCommitDomains) { + stmt.setString(1, domain.getDomain()); + stmt.addBatch(); + } + var status = stmt.executeBatch(); + checkExecuteStatus("insert", status); + } + + int size = 0; + + if (quality > 0.5) { + logger.warn("1 quality insert? {}", quality); + } + + try (var stmt = + connection.prepareStatement("INSERT IGNORE INTO EC_DOMAIN (URL_PART, QUALITY, QUALITY_ORIGINAL, URL_TOP_DOMAIN_ID, URL_SUBDOMAIN, RANK) SELECT ?, IFNULL(EC_DOMAIN_HISTORY.QUALITY_MEASURE*IFNULL(EC_DOMAIN_HISTORY.RANK, 1), ?), ?, EC_TOP_DOMAIN.ID, ?, IFNULL(EC_DOMAIN_HISTORY.RANK,1) FROM EC_TOP_DOMAIN LEFT JOIN EC_DOMAIN_HISTORY ON EC_DOMAIN_HISTORY.URL_PART=? WHERE EC_TOP_DOMAIN.URL_PART=?")) { + for (var domain : toCommitDomains) { + logger.trace("insert({})", domain); + stmt.setString(1, domain.toString()); + stmt.setDouble(2, quality); + stmt.setDouble(3, quality); + + stmt.setString(4, domain.subDomain); + + stmt.setString(5, domain.toString()); + stmt.setString(6, domain.domain); + + stmt.addBatch(); + + if ((++size % 100) == 0) { + int[] status = stmt.executeBatch(); + checkExecuteStatus("insert", status); + } + } + var status = stmt.executeBatch(); + checkExecuteStatus("insert", status); + + DOMAINS_INSERTED_CACHE.addAll(toCommitDomains); + } + + } + + @Override + @SneakyThrows + public void putLink(boolean wipeExisting, EdgeDomainLink... links) { + if (links.length == 0) { + return; + } + + try (var connection = dataSource.getConnection()) { + connection.setAutoCommit(false); + + var domains = Arrays.stream(links).flatMap(link -> + Stream.concat(Stream.of(link.destination), + Stream.of(link.source))) + .distinct().toArray(EdgeDomain[]::new); + + for (int i = 0; i < DB_LOCK_RETRIES; i++) { + try { + insert(connection, domains, -5); + insert(connection, links, wipeExisting); + connection.commit(); + break; + + } catch (Exception ex) { + logger.error("DB error", ex); + connection.rollback(); + } finally { + connection.setAutoCommit(true); + } + } + } + } + + @SneakyThrows + @Override + public boolean isBlacklisted(EdgeDomain domain) { + + try (var connection = dataSource.getConnection()) { + try (var stmt = connection.prepareStatement("SELECT ID FROM EC_DOMAIN_BLACKLIST WHERE URL_DOMAIN=?")) { + stmt.setString(1, domain.domain); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return true; + } else { + return false; + } + } + } + } + + @SneakyThrows + private void insert(Connection connection, EdgeDomainLink[] links, boolean wipeExisting) { + + int size = 0; + if (wipeExisting) { + try (var stmt = connection.prepareStatement("DELETE FROM EC_DOMAIN_LINK WHERE SOURCE_DOMAIN_ID=?")) { + EdgeDomain[] sources = Arrays.stream(links).map(EdgeDomainLink::getSource).distinct().toArray(EdgeDomain[]::new); + for (var source : sources) { + stmt.setInt(1, getDomainId(source).getId()); + stmt.executeUpdate(); + } + } + } + + try (var stmt = + connection.prepareStatement( + "INSERT IGNORE INTO EC_DOMAIN_LINK (SOURCE_DOMAIN_ID, DEST_DOMAIN_ID) SELECT SRC.DOMAIN_ID, DEST.DOMAIN_ID FROM (SELECT EC_DOMAIN.ID AS DOMAIN_ID FROM EC_DOMAIN WHERE EC_DOMAIN.URL_PART=?) AS SRC, (SELECT EC_DOMAIN.ID AS DOMAIN_ID FROM EC_DOMAIN WHERE EC_DOMAIN.URL_PART=?) AS DEST")) { + + for (EdgeDomainLink link : links) { + stmt.setString(1, link.source.toString()); + stmt.setString(2, link.destination.toString()); + + stmt.addBatch(); + + if ((++size % 100) == 0) { + int[] status = stmt.executeBatch(); + checkExecuteStatus("insert", status); + } + } + + var status = stmt.executeBatch(); + checkExecuteStatus("insert", status); + } + } + + @SneakyThrows + @Override + public EdgeId getDomainId(EdgeDomain domain) { + try (var connection = dataSource.getConnection()) { + + return domainIdCache.get(domain, () -> { + try (var stmt = connection.prepareStatement("SELECT ID FROM EC_DOMAIN WHERE URL_PART=?")) { + stmt.setString(1, domain.toString()); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return new EdgeId<>(rsp.getInt(1)); + } + } + throw new NoSuchElementException(); + }); + } + catch (UncheckedExecutionException ex) { + throw ex.getCause(); + } + } + + @Override + @SneakyThrows + public EdgeId getUrlId(EdgeUrl url) { + try (var connection = dataSource.getConnection()) { + + return urlIdCache.get(url, () -> { + try (var stmt = connection.prepareStatement("SELECT ID FROM EC_URL_VIEW WHERE URL_PATH=? AND URL_DOMAIN=? AND URL_PROTO=?")) { + stmt.setString(1, url.path); + stmt.setString(2, url.domain.toString()); + stmt.setString(3, url.proto); + + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return new EdgeId<>(rsp.getInt(1)); + } + } + // Lenient mode for http->https upgrades etc + try (var stmt = connection.prepareStatement("SELECT ID FROM EC_URL_VIEW WHERE URL_PATH=? AND URL_DOMAIN=?")) { + stmt.setString(1, url.path); + stmt.setString(2, url.domain.toString()); + + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return new EdgeId<>(rsp.getInt(1)); + } + } + throw new NoSuchElementException(url.toString()); + }); + } + catch (UncheckedExecutionException ex) { + throw ex.getCause(); + } + } + + + @SneakyThrows + @Override + public List> getDomainIdsFromUrlIds(Collection> urlIds) { + List> results = new ArrayList<>(urlIds.size()); + + if (urlIds.isEmpty()) + return results; + + try (var connection = dataSource.getConnection()) { + + try (var stmt = connection.prepareStatement("SELECT DOMAIN_ID FROM EC_URL WHERE ID IN " + urlIds + .stream() + .map(EdgeId::getId) + .map(Object::toString) + .collect(Collectors.joining(",", "(", ")")))) + { + var rsp = stmt.executeQuery(); + while (rsp.next()) { + results.add(new EdgeId<>(rsp.getInt(1))); + } + + } + } + + return results; + } + + static Pattern badChars = Pattern.compile("[';\\\\]"); + private String saneString(String s) { + return "\'"+badChars.matcher(s).replaceAll("?")+"\'"; + } + @SneakyThrows + @Override + public EdgeUrl getUrl(EdgeId id) { + try (var connection = dataSource.getConnection()) { + + try (var stmt = connection.createStatement()) { + var rsp = stmt.executeQuery("SELECT URL_PROTO, URL_DOMAIN,URL_PORT,URL_PATH FROM EC_URL_VIEW WHERE ID=" + id.getId()); + if (rsp.next()) { + return new EdgeUrl(rsp.getString(1), new EdgeDomain(rsp.getString(2)), rsp.getInt(3), rsp.getString(4)); + } + throw new NoSuchElementException(); + } + } + } + + @SneakyThrows + @Override + public EdgeUrlDetails getUrlDetails(EdgeId id) { + try (var connection = dataSource.getConnection()) { + + try (var stmt = connection.createStatement()) { + var rsp = stmt.executeQuery("SELECT ID,URL_PROTO,URL_DOMAIN,URL_PORT,URL_PATH,TITLE,DESCRIPTION,URL_QUALITY_MEASURE,DOMAIN_QUALITY_MEASURE,IFNULL(EC_DOMAIN_LINK_AGGREGATE.LINKS,1),WORDS_TOTAL,FORMAT,FEATURES,\"\",QUALITY_RAW,DOMAIN_STATE,DATA_HASH FROM EC_URL_VIEW LEFT JOIN EC_DOMAIN_LINK_AGGREGATE ON EC_DOMAIN_LINK_AGGREGATE.DOMAIN_ID=EC_URL_VIEW.DOMAIN_ID WHERE ID=" + id.getId()); + if (rsp.next()) { + EdgeUrl url = new EdgeUrl(rsp.getString(2), new EdgeDomain(rsp.getString(3)), rsp.getInt(4), rsp.getString(5)); + return new EdgeUrlDetails(rsp.getInt(1), url, rsp.getString(6), rsp.getString(7), rsp.getDouble(8), rsp.getDouble(15), rsp.getDouble(9), rsp.getInt(10), rsp.getInt(11), rsp.getString(12), rsp.getInt(13), EdgePageScoreAdjustment.zero(), Integer.MAX_VALUE, Double.MAX_VALUE, rsp.getString(14), rsp.getInt(16), 0, rsp.getInt(17)); + } + throw new NoSuchElementException(); + } + } + } + + + @SneakyThrows + @Override + public List getUrlDetailsMulti(List> ids) { + if (ids.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList<>(ids.size()); + + try (var connection = dataSource.getConnection()) { + // This is SQL-injection safe, the IDs are of type int + String idString = ids.stream().map(EdgeId::getId).map(Objects::toString).collect(Collectors.joining(",", "(", ")")); + + try (var stmt = connection.prepareStatement("SELECT ID,URL_PROTO,URL_DOMAIN,URL_PORT,URL_PATH,TITLE,DESCRIPTION,URL_QUALITY_MEASURE,DOMAIN_QUALITY_MEASURE,IFNULL(EC_DOMAIN_LINK_AGGREGATE.LINKS,1),WORDS_TOTAL,FORMAT,FEATURES,\"\",QUALITY_RAW,DOMAIN_STATE,DATA_HASH FROM EC_URL_VIEW LEFT JOIN EC_DOMAIN_LINK_AGGREGATE ON EC_DOMAIN_LINK_AGGREGATE.DOMAIN_ID=EC_URL_VIEW.DOMAIN_ID WHERE ID IN " + idString)) { + stmt.setFetchSize(ids.size()); + + var rsp = stmt.executeQuery(); + while (rsp.next()) { + EdgeUrl url = new EdgeUrl(rsp.getString(2), new EdgeDomain(rsp.getString(3)), rsp.getInt(4), rsp.getString(5)); + var val = new EdgeUrlDetails(rsp.getInt(1), url, rsp.getString(6), rsp.getString(7), rsp.getDouble(8), rsp.getDouble(15), rsp.getDouble(9), rsp.getInt(10), rsp.getInt(11), rsp.getString(12), rsp.getInt(13), EdgePageScoreAdjustment.zero(), Integer.MAX_VALUE, Double.MAX_VALUE, rsp.getString(14), rsp.getInt(16), 0, rsp.getInt(17)); + if (val.urlQuality >= QUALITY_LOWER_BOUND_CUTOFF) { + result.add(val); + } + + } + } + } + + return result; + } + + @Override + public List getDomainNeighbors(EdgeId domainId, EdgeDomainBlacklist blacklist, int count) { + final Set domains = new HashSet<>(count*3); + + final String q = "SELECT EC_DOMAIN.ID AS NEIGHBOR_ID, URL_PART from EC_DOMAIN_NEIGHBORS INNER JOIN EC_DOMAIN ON NEIGHBOR_ID=EC_DOMAIN.ID WHERE STATE<2 AND DOMAIN_ALIAS IS NULL AND EC_DOMAIN_NEIGHBORS.DOMAIN_ID = ? ORDER BY ADJ_IDX LIMIT ?"; + + try (var connection = dataSource.getConnection()) { + try (var stmt = connection.prepareStatement(q)) { + stmt.setFetchSize(count); + stmt.setInt(1, domainId.getId()); + stmt.setInt(2, count); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + int id = rsp.getInt(1); + String domain = rsp.getString(2); + + if (!blacklist.isBlacklisted(id)) { + var url = new EdgeUrl(DEFAULT_PROTOCOL, new EdgeDomain(domain), null, "/"); + + domains.add(new BrowseResult(url, id)); + } + } + } + + final String q2 = "SELECT EC_DOMAIN.ID, URL_PART FROM EC_DOMAIN_LINK INNER JOIN EC_DOMAIN ON DEST_DOMAIN_ID=EC_DOMAIN.ID WHERE SOURCE_DOMAIN_ID=? AND STATE<2 AND DOMAIN_ALIAS IS NULL GROUP BY EC_DOMAIN.ID ORDER BY RANK ASC LIMIT ?"; + try (var stmt = connection.prepareStatement(q2)) { + + stmt.setFetchSize(count); + stmt.setInt(1, domainId.getId()); + stmt.setInt(2, count); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + int id = rsp.getInt(1); + String domain = rsp.getString(2); + + if (!blacklist.isBlacklisted(id)) { + var url = new EdgeUrl(DEFAULT_PROTOCOL, new EdgeDomain(domain), null, "/"); + + domains.add(new BrowseResult(url, id)); + } + } + } + + final String q3 = "SELECT EC_DOMAIN.ID, URL_PART FROM EC_DOMAIN_LINK INNER JOIN EC_DOMAIN ON DEST_DOMAIN_ID=EC_DOMAIN.ID WHERE DEST_DOMAIN_ID=? AND STATE<2 AND DOMAIN_ALIAS IS NULL GROUP BY EC_DOMAIN.ID ORDER BY RANK ASC LIMIT ?"; + try (var stmt = connection.prepareStatement(q3)) { + stmt.setFetchSize(count); + stmt.setInt(1, domainId.getId()); + stmt.setInt(2, count); + + var rsp = stmt.executeQuery(); + while (rsp.next()) { + int id = rsp.getInt(1); + String domain = rsp.getString(2); + + if (!blacklist.isBlacklisted(id)) { + var url = new EdgeUrl(DEFAULT_PROTOCOL, new EdgeDomain(domain), null, "/"); + + domains.add(new BrowseResult(url, id)); + } + } + } + } catch (SQLException throwables) { + throwables.printStackTrace(); + } + + + return new ArrayList<>(domains); + } + + + @Override + public List getDomainNeighborsAdjacent(EdgeId domainId, EdgeDomainBlacklist blacklist, int count) { + final Set domains = new HashSet<>(count*3); + + final String q = """ + SELECT EC_DOMAIN.ID AS NEIGHBOR_ID, URL_PART, COUNT(*) AS CNT + FROM EC_DOMAIN_NEIGHBORS + INNER JOIN EC_DOMAIN ON NEIGHBOR_ID=EC_DOMAIN.ID + INNER JOIN DOMAIN_METADATA ON EC_DOMAIN.ID=DOMAIN_METADATA.ID + INNER JOIN EC_DOMAIN_LINK ON DEST_DOMAIN_ID=EC_DOMAIN.ID + WHERE + STATE<2 + AND KNOWN_URLS<1000 + AND DOMAIN_ALIAS IS NULL + AND EC_DOMAIN_NEIGHBORS.DOMAIN_ID = ? + GROUP BY EC_DOMAIN.ID + HAVING CNT < 100 + ORDER BY ADJ_IDX + LIMIT ? + """; + + try (var connection = dataSource.getConnection()) { + try (var stmt = connection.prepareStatement(q)) { + stmt.setFetchSize(count); + stmt.setInt(1, domainId.getId()); + stmt.setInt(2, count); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + int id = rsp.getInt(1); + String domain = rsp.getString(2); + + if (!blacklist.isBlacklisted(id)) { + var url = new EdgeUrl(DEFAULT_PROTOCOL, new EdgeDomain(domain), null, "/"); + + domains.add(new BrowseResult(url, id)); + } + } + } + + if (domains.size() < count/2) { + final String q2 = """ + SELECT EC_DOMAIN.ID, URL_PART + FROM EC_DOMAIN + INNER JOIN DOMAIN_METADATA ON EC_DOMAIN.ID=DOMAIN_METADATA.ID + INNER JOIN EC_DOMAIN_LINK B ON DEST_DOMAIN_ID=EC_DOMAIN.ID + INNER JOIN EC_DOMAIN_LINK O ON O.DEST_DOMAIN_ID=EC_DOMAIN.ID + WHERE B.SOURCE_DOMAIN_ID=? + AND STATE<2 + AND KNOWN_URLS<1000 + AND DOMAIN_ALIAS IS NULL + GROUP BY EC_DOMAIN.ID + HAVING COUNT(*) < 100 ORDER BY RANK ASC LIMIT ?"""; + try (var stmt = connection.prepareStatement(q2)) { + + stmt.setFetchSize(count/2); + stmt.setInt(1, domainId.getId()); + stmt.setInt(2, count/2 - domains.size()); + var rsp = stmt.executeQuery(); + while (rsp.next() && domains.size() < count/2) { + int id = rsp.getInt(1); + String domain = rsp.getString(2); + + if (!blacklist.isBlacklisted(id)) { + var url = new EdgeUrl(DEFAULT_PROTOCOL, new EdgeDomain(domain), null, "/"); + + domains.add(new BrowseResult(url, id)); + } + } + } + } + + if (domains.size() < count/2) { + final String q3 = """ + SELECT EC_DOMAIN.ID, URL_PART + FROM EC_DOMAIN + INNER JOIN DOMAIN_METADATA ON EC_DOMAIN.ID=DOMAIN_METADATA.ID + INNER JOIN EC_DOMAIN_LINK B ON B.SOURCE_DOMAIN_ID=EC_DOMAIN.ID + INNER JOIN EC_DOMAIN_LINK O ON O.DEST_DOMAIN_ID=EC_DOMAIN.ID + WHERE B.DEST_DOMAIN_ID=? + AND STATE<2 + AND KNOWN_URLS<1000 + AND DOMAIN_ALIAS IS NULL + GROUP BY EC_DOMAIN.ID + HAVING COUNT(*) < 100 + ORDER BY RANK ASC + LIMIT ?"""; + try (var stmt = connection.prepareStatement(q3)) { + stmt.setFetchSize(count/2); + stmt.setInt(1, domainId.getId()); + stmt.setInt(2, count/2 - domains.size()); + + var rsp = stmt.executeQuery(); + while (rsp.next() && domains.size() < count/2) { + int id = rsp.getInt(1); + String domain = rsp.getString(2); + + if (!blacklist.isBlacklisted(id)) { + var url = new EdgeUrl(DEFAULT_PROTOCOL, new EdgeDomain(domain), null, "/"); + + domains.add(new BrowseResult(url, id)); + } + } + } + } + } catch (SQLException throwables) { + throwables.printStackTrace(); + } + + + return new ArrayList<>(domains); + } + + @Override + public List getRandomDomains(int count, EdgeDomainBlacklist blacklist) { + + final String q = "SELECT DOMAIN_ID,URL_PART FROM EC_RANDOM_DOMAINS INNER JOIN EC_DOMAIN ON EC_DOMAIN.ID=DOMAIN_ID WHERE STATE<2 AND DOMAIN_ALIAS IS NULL ORDER BY RAND() LIMIT ?"; + List domains = new ArrayList<>(count); + try (var conn = dataSource.getConnection()) { + try (var stmt = conn.prepareStatement(q)) { + stmt.setInt(1, count); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + int id = rsp.getInt(1); + String domain = rsp.getString(2); + + if (!blacklist.isBlacklisted(id)) { + var url = new EdgeUrl(DEFAULT_PROTOCOL, new EdgeDomain(domain), null, "/"); + + domains.add(new BrowseResult(url, id)); + } + } + } + } + catch (SQLException ex) { + logger.error("SQL error", ex); + } + return domains; + } + + @Override + @SneakyThrows + public EdgeDomain getDomain(EdgeId id) { + try (var connection = dataSource.getConnection()) { + + try (var stmt = connection.prepareStatement("SELECT URL_PART FROM EC_DOMAIN WHERE ID=?")) { + stmt.setInt(1, id.getId()); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return new EdgeDomain(rsp.getString(1)); + } + throw new NoSuchElementException(); + } + } + } + + @Override @SneakyThrows + public List> inboudUrls(EdgeId id, int limit) { + + List> ret = new ArrayList<>(); + try (var connection = dataSource.getConnection()) { + + try (var stmt = + connection.prepareStatement("SELECT SRC_URL_ID FROM EC_RELATED_LINKS_IN WHERE DEST_URL_ID=? ORDER BY SRC_URL_QUALITY DESC LIMIT ?")) { + stmt.setFetchSize(limit); + stmt.setInt(1, id.getId()); + stmt.setInt(2, limit); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + ret.add(new EdgeId<>(rsp.getInt(1))); + } + } + + } + + return ret; + } + + + @Override @SneakyThrows + public List> outboundUrls(EdgeId id, int limit) { + + List> ret = new ArrayList<>(); + try (var connection = dataSource.getConnection()) { + + try (var stmt = + connection.prepareStatement("SELECT DEST_URL_ID FROM EC_RELATED_LINKS_IN WHERE SRC_URL_ID=? ORDER BY SRC_URL_QUALITY DESC LIMIT ?")) { + stmt.setFetchSize(limit); + stmt.setInt(1, id.getId()); + stmt.setInt(2, limit); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + ret.add(new EdgeId<>(rsp.getInt(1))); + } + } + + } + + return ret; + } + + @Override + public Optional> resolveAmbiguousDomain(String name) { + try (var connection = dataSource.getConnection()) { + try (var stmt = connection.prepareStatement("SELECT IFNULL(DOMAIN_ALIAS,ID) FROM EC_DOMAIN WHERE URL_PART=?")) { + stmt.setString(1, name); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return Optional.of(new EdgeId<>(rsp.getInt(1))); + } + } + + try (var stmt = connection.prepareStatement("SELECT IFNULL(DOMAIN_ALIAS,ID) FROM EC_DOMAIN WHERE URL_PART=?")) { + stmt.setString(1, "https://"+name); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return Optional.of(new EdgeId<>(rsp.getInt(1))); + } + } + + try (var stmt = connection.prepareStatement("SELECT IFNULL(DOMAIN_ALIAS,ID) FROM EC_DOMAIN WHERE URL_PART=?")) { + stmt.setString(1, "http://"+name); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return Optional.of(new EdgeId<>(rsp.getInt(1))); + } + } + + try (var stmt = connection.prepareStatement("SELECT IFNULL(DOMAIN_ALIAS,ID) FROM EC_DOMAIN WHERE URL_PART=?")) { + stmt.setString(1, "https://www."+name); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return Optional.of(new EdgeId<>(rsp.getInt(1))); + } + } + + try (var stmt = connection.prepareStatement("SELECT IFNULL(DOMAIN_ALIAS,ID) FROM EC_DOMAIN WHERE URL_PART=?")) { + stmt.setString(1, "http://www."+name); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return Optional.of(new EdgeId<>(rsp.getInt(1))); + } + } + + } catch (SQLException throwables) { + logger.info("Could not resolve domain id for {}", name); + } + + return Optional.empty(); + } + + @SneakyThrows + @Override + public void putDomainAlias(EdgeDomain src, EdgeDomain dst) { + try (var connection = dataSource.getConnection()) { + + for (int i = 0; i < DB_LOCK_RETRIES; i++) { + connection.setAutoCommit(false); + + if (!DOMAINS_INSERTED_CACHE.contains(dst)) { + insert(connection, new EdgeDomain[] { dst }, getDomainQuality(connection, src)); + } + + try (var stmt = connection.prepareStatement("UPDATE EC_DOMAIN AS D, EC_DOMAIN AS S SET S.DOMAIN_ALIAS=D.ID WHERE S.URL_PART=? AND D.URL_PART=?")) { + stmt.setString(1, src.toString()); + stmt.setString(2, dst.toString()); + stmt.executeUpdate(); + connection.commit(); + break; + } catch (SQLException ex) { + logger.error("DB Error", ex); + connection.rollback(); + } + finally { + connection.setAutoCommit(true); + } + } + + } + + } + + @SneakyThrows + @Override + public int getPagesKnown(EdgeId domainId) { + try (var connection = dataSource.getConnection()) { + + try (var stmt = connection.prepareStatement("SELECT KNOWN_URLS FROM DOMAIN_METADATA WHERE ID=?")) { + stmt.setInt(1, domainId.getId()); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return rsp.getInt(1); + } + } catch (Exception ex) { + logger.error("DB error", ex); + } + return 0; + } + } + + @SneakyThrows + @Override + public int getPagesVisited(EdgeId domainId) { + try (var connection = dataSource.getConnection()) { + + try (var stmt = connection.prepareStatement("SELECT VISITED_URLS FROM DOMAIN_METADATA WHERE ID=?")) { + stmt.setInt(1, domainId.getId()); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return rsp.getInt(1); + } + } catch (Exception ex) { + logger.error("DB error", ex); + } + return 0; + } + } + + + @SneakyThrows + @Override + public int getPagesIndexed(EdgeId domainId) { + try (var connection = dataSource.getConnection()) { + + try (var stmt = connection.prepareStatement("SELECT GOOD_URLS FROM DOMAIN_METADATA WHERE ID=?")) { + stmt.setInt(1, domainId.getId()); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return rsp.getInt(1); + } + } catch (Exception ex) { + logger.error("DB error", ex); + } + return 0; + } + } + + @SneakyThrows + @Override + public int getIncomingLinks(EdgeId domainId) { + try (var connection = dataSource.getConnection()) { + + try (var stmt = connection.prepareStatement("SELECT COUNT(ID) FROM EC_DOMAIN_LINK WHERE DEST_DOMAIN_ID=?")) { + stmt.setInt(1, domainId.getId()); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return rsp.getInt(1); + } + } catch (Exception ex) { + logger.error("DB error", ex); + } + return 0; + } + } + @SneakyThrows + @Override + public int getOutboundLinks(EdgeId domainId) { + try (var connection = dataSource.getConnection()) { + + try (var stmt = connection.prepareStatement("SELECT COUNT(ID) FROM EC_DOMAIN_LINK WHERE SOURCE_DOMAIN_ID=?")) { + stmt.setInt(1, domainId.getId()); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return rsp.getInt(1); + } + } catch (Exception ex) { + logger.error("DB error", ex); + } + return 0; + } + } + + @SneakyThrows + @Override + public double getDomainQuality(EdgeId domainId) { + try (var connection = dataSource.getConnection()) { + + try (var stmt = connection.prepareStatement("SELECT QUALITY FROM EC_DOMAIN WHERE ID=?")) { + stmt.setInt(1, domainId.getId()); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return rsp.getDouble(1); + } + } catch (Exception ex) { + logger.error("DB error", ex); + } + return -5; + } + } + + @Override + public EdgeDomainIndexingState getDomainState(EdgeId domainId) { + try (var connection = dataSource.getConnection()) { + + try (var stmt = connection.prepareStatement("SELECT STATE FROM EC_DOMAIN WHERE ID=?")) { + stmt.setInt(1, domainId.getId()); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return EdgeDomainIndexingState.fromCode(rsp.getInt(1)); + } + } catch (Exception ex) { + logger.error("DB error", ex); + } + } catch (SQLException throwables) { + throwables.printStackTrace(); + } + return EdgeDomainIndexingState.ERROR; + } + + @Override + public List getLinkingDomains(EdgeId domainId) { + try (var connection = dataSource.getConnection()) { + List results = new ArrayList<>(25); + try (var stmt = connection.prepareStatement("SELECT SOURCE_URL FROM EC_RELATED_LINKS_VIEW WHERE DEST_DOMAIN_ID=? ORDER BY SOURCE_DOMAIN_ID LIMIT 25")) { + stmt.setInt(1, domainId.getId()); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + results.add(new EdgeDomain(rsp.getString(1))); + } + return results; + } catch (Exception ex) { + logger.error("DB error", ex); + } + + } catch (SQLException throwables) { + throwables.printStackTrace(); + } + return Collections.emptyList(); + } + + @Override + public List getNewUrls(EdgeId domainId, Collection links) { + Map edgeUrlByPath = links.stream().collect(Collectors.toMap(EdgeUrl::getPath, Function.identity(), (a,b)->a)); + + try (var connection = dataSource.getConnection()) { + try (var stmt = connection.prepareStatement("SELECT URL FROM EC_URL WHERE DOMAIN_ID=?")) { + stmt.setFetchSize(500); + stmt.setInt(1, domainId.getId()); + var rs = stmt.executeQuery(); + while (rs.next()) { + edgeUrlByPath.remove(rs.getString(1)); + } + } + } + catch (Exception ex) { + return Collections.emptyList(); + } + return new ArrayList<>(edgeUrlByPath.values()); + + } + + @Override + public double getRank(EdgeId domainId) { + try (var connection = dataSource.getConnection()) { + + try (var stmt = connection.prepareStatement("SELECT IFNULL(RANK, 1) FROM EC_DOMAIN WHERE ID=?")) { + stmt.setInt(1, domainId.getId()); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return rsp.getDouble(1); + } + } catch (Exception ex) { + logger.error("DB error", ex); + } + } catch (SQLException throwables) { + throwables.printStackTrace(); + } + return 1; + } + + @Override + public void updateDomainIndexTimestamp(EdgeDomain domain, EdgeDomainIndexingState state, EdgeDomain alias, int minIndexed) { + try (var connection = dataSource.getConnection(); + var stmt = connection.prepareStatement("UPDATE EC_DOMAIN SET INDEX_DATE=NOW(), STATE=?, DOMAIN_ALIAS=?, INDEXED=GREATEST(INDEXED,?) WHERE ID=?")) { + stmt.setInt(1, state.code); + if (null == alias) { + stmt.setNull(2, Types.INTEGER); + } + else { + stmt.setInt(2, getDomainId(alias).getId()); + } + + stmt.setInt(3, minIndexed); + stmt.setInt(4, getDomainId(domain).getId()); + stmt.executeUpdate(); + connection.commit(); + } + catch (SQLException throwables) { + logger.error("SQL error", throwables); + } + } + + @SneakyThrows + private double getDomainQuality(Connection connection, EdgeDomain src) { + try (var stmt = connection.prepareStatement("SELECT QUALITY_RAW FROM EC_DOMAIN WHERE URL_PART=?")) { + stmt.setString(1, src.toString()); + var res = stmt.executeQuery(); + + if (res.next()) { + var q = res.getDouble(1); + if (q > 0.5) { + logger.warn("gDQ({}) -> 1", src); + } + return 0; + } + } + catch (SQLException ex) { + logger.error("DB error", ex); + } + + return -5; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/EdgeDataStoreTaskDao.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/EdgeDataStoreTaskDao.java new file mode 100644 index 00000000..3bcfc79f --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/EdgeDataStoreTaskDao.java @@ -0,0 +1,18 @@ +package nu.marginalia.wmsa.edge.data.dao; + +import com.google.inject.ImplementedBy; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDataStoreTaskDaoImpl; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.crawl.EdgeIndexTask; + + +@ImplementedBy(EdgeDataStoreTaskDaoImpl.class) +public interface EdgeDataStoreTaskDao { + EdgeIndexTask getIndexTask(int pass, int limit); + EdgeIndexTask getDiscoverTask(); + void finishIndexTask(EdgeDomain domain, double quality, EdgeDomainIndexingState state); + void finishBadIndexTask(EdgeDomain domain, EdgeDomainIndexingState state); + void flushOngoingJobs(); + boolean isBlocked(); +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDataStoreTaskDaoImpl.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDataStoreTaskDaoImpl.java new file mode 100644 index 00000000..e1bd5f6d --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDataStoreTaskDaoImpl.java @@ -0,0 +1,496 @@ +package nu.marginalia.wmsa.edge.data.dao.task; + +import com.google.inject.Inject; +import com.zaxxer.hikari.HikariDataSource; +import io.prometheus.client.Gauge; +import io.reactivex.rxjava3.schedulers.Schedulers; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDaoImpl; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreTaskDao; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.crawl.EdgeIndexTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import static java.sql.Connection.TRANSACTION_READ_COMMITTED; +import static java.sql.Connection.TRANSACTION_READ_UNCOMMITTED; + +public class EdgeDataStoreTaskDaoImpl implements EdgeDataStoreTaskDao { + + private final HikariDataSource dataSource; + private final Logger logger = LoggerFactory.getLogger(getClass()); + private static final Gauge wmsa_index_task_quality = Gauge.build("wmsa_discover_index_task_quality", "wmsa_discover_index_task_quality").labelNames("depth").register(); + + private final LinkedBlockingQueue discoverDomainQueue = new LinkedBlockingQueue<>(200); + private final LinkedBlockingQueue indexDomainQueue1 = new LinkedBlockingQueue<>(200); + + private final EdgeDomainBlacklist blacklist; + private final EdgeDataStoreTaskTuner taskQueryTuner; + private final EdgeDataStoreDaoImpl baseDao; + private final EdgeDataStoreTaskOngoingJobs ongoingJobs; + private final EdgeFinishTasksQueue finishTasksQueue; + private final Initialization initialization; + private final Semaphore taskFetchSem = new Semaphore(3, true); + private final LinkedBlockingDeque blockingJobs = new LinkedBlockingDeque<>(); + + + @Inject + public EdgeDataStoreTaskDaoImpl(HikariDataSource dataSource, + EdgeDomainBlacklist blacklist, + EdgeDataStoreTaskTuner taskQueryTuner, + EdgeDataStoreTaskOngoingJobs ongoingJobs, + EdgeFinishTasksQueue finishTasksQueue, + Initialization initialization) + { + this.dataSource = dataSource; + baseDao = new EdgeDataStoreDaoImpl(dataSource); + this.blacklist = blacklist; + this.taskQueryTuner = taskQueryTuner; + this.ongoingJobs = ongoingJobs; + this.finishTasksQueue = finishTasksQueue; + this.initialization = initialization; + + Schedulers.io().schedulePeriodicallyDirect(this::repopulateUrlLinkDensity, 7, 360, TimeUnit.MINUTES); +// Schedulers.io().schedulePeriodicallyDirect(this::blacklistLinkfarms, 60, 600, TimeUnit.SECONDS); + + var updateDiscoverQueue = new Thread(this::updateDiscoverQueue, "UpdateDiscoverQueue"); + updateDiscoverQueue.setDaemon(true); + updateDiscoverQueue.start(); + + var updateIndexQueue = new Thread(this::updateIndexQueue, "UpdateIndexQueue"); + updateIndexQueue.setDaemon(true); + updateIndexQueue.start(); + + } + + @Override + public boolean isBlocked() { + return !blockingJobs.isEmpty(); + } + + + @SneakyThrows + private void blacklistLinkfarms() { + try (var connection = dataSource.getConnection()) { + connection.setAutoCommit(false); + + List ids = new ArrayList<>(1000); + + try (var stmt = connection.prepareStatement("SELECT SQL_BUFFER_RESULT URL_TOP_DOMAIN_ID from EC_DOMAIN USE INDEX(EC_DOMAIN_ID_INDEXED_INDEX) WHERE INDEXED>=1 GROUP BY URL_TOP_DOMAIN_ID HAVING COUNT(ID)>100")) { + connection.setTransactionIsolation(TRANSACTION_READ_UNCOMMITTED); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + ids.add(rsp.getInt(1)); + } + } + catch (Exception ex) { + logger.error("DB error", ex); + return; + } + finally { + connection.setTransactionIsolation(TRANSACTION_READ_COMMITTED); + } + + try (var stmt = connection.prepareStatement("UPDATE EC_TOP_DOMAIN SET ALIVE=0 WHERE ID=?")) { + for (int id : ids) { + stmt.setInt(1, id); + stmt.addBatch(); + } + stmt.executeBatch(); + connection.commit(); + } + catch (Exception ex) { + logger.error("DB error", ex); + connection.rollback(); + } + } + } + + public synchronized void clearCaches() + { + ongoingJobs.clear(); + } + + + @SneakyThrows + private EdgeIndexTask updateIndexQueue() { + + List fetchedDomains = new ArrayList<>(100); + + initialization.waitReady(); + + for (;;) { + if (!blockingJobs.isEmpty()) { + Thread.sleep(1000); + continue; + } + + try (var connection = dataSource.getConnection()) { + try (var stmt = + connection.prepareStatement("SELECT ID,URL_PART,IFNULL(RANK,1) FROM EC_DOMAIN USE INDEX(EC_DOMAIN_TRIO) WHERE DOMAIN_ALIAS IS NULL AND STATE=0 AND QUALITY > ? AND INDEXED = 1 ORDER BY QUALITY DESC LIMIT 100")) { + + stmt.setDouble(1, taskQueryTuner.getIndexQualityLimit()); + stmt.setFetchSize(100); + + var rsp = stmt.executeQuery(); + + while (rsp.next()) { + int domainId = rsp.getInt(1); + var domain = new EdgeDomain(rsp.getString(2)); + + if (blacklist.isBlacklisted(domainId)) { + finishBadIndexTask(domain, EdgeDomainIndexingState.BLOCKED); + continue; + } + + if (!ongoingJobs.isOngoing(domain)) { + fetchedDomains.add(new EdgeDomainDiscoverTask(domain, rsp.getInt(1), rsp.getDouble(3))); + } + } + } catch (Exception ex) { + logger.error("DB error", ex); + } + } + + indexDomainQueue1.removeIf(d -> ongoingJobs.isOngoing(d.domain)); + + for (var d : fetchedDomains) { + if (!blacklist.isBlacklisted(d.id)) { + if (!indexDomainQueue1.contains(d) && !ongoingJobs.isOngoing(d.domain)) { + indexDomainQueue1.put(d); + } + } + else { + finishBadIndexTask(d.domain, EdgeDomainIndexingState.BLOCKED); + } + } + fetchedDomains.clear(); + } + } + + + @Override + @SneakyThrows + public EdgeIndexTask getIndexTask(int pass, int limit) { + + if (!blockingJobs.isEmpty()) { + return new EdgeIndexTask(null, 0, limit, 1.); + } + boolean acquired = taskFetchSem.tryAcquire(); + if (!acquired) { + return new EdgeIndexTask(null, 0, 1, 1.); + } + + try { + + if (pass == 1) { + var task = tryGetIndexTask(0, pass, limit); + if (task.isPresent()) { + return task.get(); + } + } + + try (var connection = dataSource.getConnection()) { + + for (double adj = 1; adj < 10; adj *= 1.5) { + try (var stmt = + connection.prepareStatement("SELECT ID,URL_PART,INDEXED,QUALITY,IFNULL(RANK,1) FROM EC_DOMAIN USE INDEX(EC_DOMAIN_TRIO) WHERE DOMAIN_ALIAS IS NULL AND STATE=0 AND QUALITY > ? AND INDEXED > ? AND INDEXED <= ? ORDER BY QUALITY DESC LIMIT 100")) { + stmt.setFetchSize(100); + stmt.setDouble(1, taskQueryTuner.getIndexQualityLimit() - (adj - 1) - Math.random()); + stmt.setInt(2, pass / 10); + stmt.setInt(3, pass); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + var domain = new EdgeDomain(rsp.getString(2)); + int domainId = rsp.getInt(1); + + if (blacklist.isBlacklisted(domainId)) { + finishBadIndexTask(domain, EdgeDomainIndexingState.BLOCKED); + continue; + } + + if (ongoingJobs.add(domain)) { + var task = getUrlsForIndexTask(domain, domainId, rsp.getInt(3), limit, rsp.getDouble(5)); + + if (task.isEmpty()) { + finishBadIndexTask(domain, EdgeDomainIndexingState.EXHAUSTED); + } else { + wmsa_index_task_quality + .labels(String.format("%02d", pass)) + .set(rsp.getDouble(4)); + + return task; + } + } + } + } catch (Exception ex) { + logger.error("DB error", ex); + } + } + + return new EdgeIndexTask(null, 0, limit, 1.); + } + } + finally { + taskFetchSem.release(); + } + } + + private Optional tryGetIndexTask(int attempt, int pass, int limit) { + if (attempt > 5) { + return Optional.of(new EdgeIndexTask(null, 1, limit, 1.)); + } + + var t = indexDomainQueue1.poll(); + + if (t != null) { + if (!validateIndexedState(t.id, 1)) { + return tryGetIndexTask(attempt + 1, pass, limit); + } + if (!ongoingJobs.add(t.domain)) { + return tryGetIndexTask(attempt + 1, pass, limit); + } + + var task = getUrlsForIndexTask(t.domain, t.id, pass, limit, t.rank); + + if (task.isEmpty()) { + finishBadIndexTask(t.domain, EdgeDomainIndexingState.EXHAUSTED); + return tryGetIndexTask(attempt + 1, pass, limit); + } + return Optional.of(task); + } + return Optional.of(new EdgeIndexTask(null, 1, limit, 1.)); + } + + + @SneakyThrows + private EdgeIndexTask updateDiscoverQueue() { + + List fetchedDomains = new ArrayList<>(100); + + initialization.waitReady(); + + for (;;) { + if (!blockingJobs.isEmpty()) { + Thread.sleep(1000); + continue; + } + + try (var connection = dataSource.getConnection()) { + try (var stmt = + connection.prepareStatement("SELECT EC_DOMAIN.ID,EC_DOMAIN.URL_PART,IFNULL(RANK, 1) FROM EC_DOMAIN USE INDEX(EC_DOMAIN_TRIO) INNER JOIN EC_TOP_DOMAIN ON EC_TOP_DOMAIN.ID=URL_TOP_DOMAIN_ID WHERE DOMAIN_ALIAS IS NULL AND STATE=0 AND QUALITY > ? AND INDEXED = 0 AND ALIVE = 1 ORDER BY QUALITY DESC LIMIT 100")) { + + stmt.setDouble(1, taskQueryTuner.getDiscoverQualityLimit()); + stmt.setFetchSize(100); + + var rsp = stmt.executeQuery(); + + while (rsp.next()) { + var domain = new EdgeDomain(rsp.getString(2)); + + if (!ongoingJobs.isOngoing(domain)) { + fetchedDomains.add(new EdgeDomainDiscoverTask(domain, rsp.getInt(1), rsp.getDouble(3))); + } + } + } catch (Exception ex) { + logger.error("DB error", ex); + } + } + + discoverDomainQueue.removeIf(d -> ongoingJobs.isOngoing(d.domain)); + + for (var d : fetchedDomains) { + if (!blacklist.isBlacklisted(d.id)) { + if (!discoverDomainQueue.contains(d) && !ongoingJobs.isOngoing(d.domain)) { + discoverDomainQueue.put(d); + } + } + else { + finishIndexTask(d.domain, -1000, EdgeDomainIndexingState.BLOCKED); + } + } + fetchedDomains.clear(); + } + } + + @Override + @SneakyThrows + public EdgeIndexTask getDiscoverTask() { + boolean acquired = taskFetchSem.tryAcquire(); + if (!acquired) { + return new EdgeIndexTask(null, 0, 1, 1.); + } + + try { + + if (!blockingJobs.isEmpty()) { + return new EdgeIndexTask(null, 0, 1, 1.); + } + + return tryGetDiscoverTask(0) + .orElseGet(() -> new EdgeIndexTask(null, 0, 1, 1.)); + } + finally { + taskFetchSem.release(); + } + } + + @SneakyThrows + private Optional tryGetDiscoverTask(int attempt) { + if (attempt > 5) { + return Optional.empty(); + } + var t = discoverDomainQueue.poll(50, TimeUnit.MILLISECONDS); + + if (t != null) { + if (!validateIndexedState(t.id, 0)) { + return tryGetDiscoverTask(attempt+1); + } + if (!ongoingJobs.add(t.domain)) { + return tryGetDiscoverTask(attempt+1); + } + + var task = getUrlsForIndexTask(t.domain, t.id, 0, 10, t.rank); + if (task.isEmpty()) { + if (task.visited.isEmpty()) { + logger.warn("No url for {}", t.domain); + var rootUrl = new EdgeUrl("https", t.domain, null, "/"); + baseDao.putUrl(-5, rootUrl); + + task.urls.add(rootUrl); + } else { + ongoingJobs.remove(t.domain); + return tryGetDiscoverTask(attempt+1); + } + } + + return Optional.of(task); + } + return Optional.of(new EdgeIndexTask(null, 0, 1, 1.)); + } + + @Override + @SneakyThrows + public void finishIndexTask(EdgeDomain domain, double quality, EdgeDomainIndexingState state) { + finishTasksQueue.add(domain, quality, state); + } + @SneakyThrows + public void finishBadIndexTask(EdgeDomain domain, EdgeDomainIndexingState state) { + finishTasksQueue.addError(domain, state); + } + + @Override + public void flushOngoingJobs() { + ongoingJobs.clear(); + } + + private void repopulateUrlLinkDensity() { + try (var connection = dataSource.getConnection(); + var stmt = connection.createStatement() + ) { + blockingJobs.push("Repopulate URL Link Density"); + logger.info("Starting link details sync"); + stmt.executeUpdate("INSERT INTO EC_DOMAIN_LINK_AGGREGATE(DOMAIN_ID,LINKS) SELECT DEST_DOMAIN_ID AS ID, 100*SUM(EXP(EC_DOMAIN.QUALITY_RAW))/SQRT(GREATEST(1,COUNT(EC_DOMAIN.ID))) AS LINKS FROM EC_DOMAIN_LINK INNER JOIN EC_DOMAIN ON EC_DOMAIN.ID=EC_DOMAIN_LINK.SOURCE_DOMAIN_ID GROUP BY EC_DOMAIN_LINK.DEST_DOMAIN_ID ON DUPLICATE KEY UPDATE LINKS=VALUES(LINKS)"); + logger.info("Finished link details sync"); + } + catch (Exception ex) { + logger.error("DB error", ex); + } + finally { + blockingJobs.pop(); + } + } + + private boolean validateIndexedState(int domainId, int expected) { + try (var connection = dataSource.getConnection()) { + + try (var stmt = + connection.prepareStatement("select INDEXED from EC_DOMAIN WHERE ID=?")) { + stmt.setInt(1, domainId); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + return rsp.getInt(1) == expected; + } + else { + return false; + } + } catch (Exception ex) { + logger.error("DB error", ex); + } + } + catch (Exception ex) { + logger.error("DB error", ex); + } + return false; + + } + + @SneakyThrows + private EdgeIndexTask getUrlsForIndexTask(EdgeDomain domain, int domainId, int pass, int limit, double rank) { + try (var connection = dataSource.getConnection()) { + + EdgeIndexTask indexTask = new EdgeIndexTask(domain, pass, limit, rank); + + try (var stmt = + connection.prepareStatement("select SQL_BUFFER_RESULT proto,url,port,visited from EC_URL USE INDEX (EC_URL_DOMAIN_ID) WHERE DOMAIN_ID=?")) { + stmt.setFetchSize(limit); + stmt.setInt(1, domainId); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + var url = new EdgeUrl(rsp.getString(1), + domain, + rsp.getInt(3), + rsp.getString(2) + ); + + if (rsp.getBoolean(4)) { + indexTask.visited.add(url.hashCode()); + } else if (indexTask.urls.size() < limit) { + indexTask.urls.add(url); + } + } + } catch (Exception ex) { + logger.error("DB error", ex); + } + return indexTask; + } + } + + +} + +@AllArgsConstructor +class EdgeDomainDiscoverTask { + public final EdgeDomain domain; + public final int id; + public final double rank; + + @Override + public int hashCode() { + return domain.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (other instanceof EdgeDomainDiscoverTask) { + EdgeDomainDiscoverTask o = (EdgeDomainDiscoverTask)other; + return id == o.id; + } + return false; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDataStoreTaskOngoingJobs.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDataStoreTaskOngoingJobs.java new file mode 100644 index 00000000..ad3d4cfe --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDataStoreTaskOngoingJobs.java @@ -0,0 +1,49 @@ +package nu.marginalia.wmsa.edge.data.dao.task; + +import com.google.inject.Singleton; +import io.prometheus.client.Gauge; +import io.reactivex.rxjava3.schedulers.Schedulers; +import nu.marginalia.wmsa.edge.model.EdgeDomain; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +@Singleton +public class EdgeDataStoreTaskOngoingJobs { + private final ConcurrentHashMap indexingDomains = new ConcurrentHashMap<>(); + private static final long STALE_JOB_TIMEOUT = 30*60; + + private static final Gauge wmsa_director_ongoing_jobs = Gauge.build("wmsa_director_ongoing_jobs", + "wmsa_director_ongoing_jobs").register(); + + public EdgeDataStoreTaskOngoingJobs() { + Schedulers.computation().schedulePeriodicallyDirect(this::purgeOngoingJobs, 60, 60, TimeUnit.SECONDS); + } + + + private void purgeOngoingJobs() { + final long now = System.currentTimeMillis(); + + indexingDomains + .entrySet() + .removeIf(e -> (now - e.getValue()) / 1000 >= STALE_JOB_TIMEOUT); + + wmsa_director_ongoing_jobs.set(indexingDomains.size()); + } + + public boolean isOngoing(EdgeDomain domain) { + return indexingDomains.containsKey(domain.getDomainKey()); + } + + public boolean add(EdgeDomain job) { + return indexingDomains.putIfAbsent(job.getDomainKey(), System.currentTimeMillis()) == null; + } + + public void clear() { + indexingDomains.clear(); + } + + public void remove(EdgeDomain domain) { + indexingDomains.remove(domain.getDomainKey()); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDataStoreTaskTuner.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDataStoreTaskTuner.java new file mode 100644 index 00000000..1e7da96d --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDataStoreTaskTuner.java @@ -0,0 +1,142 @@ +package nu.marginalia.wmsa.edge.data.dao.task; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.zaxxer.hikari.HikariDataSource; +import io.prometheus.client.Gauge; +import io.prometheus.client.Histogram; +import io.reactivex.rxjava3.schedulers.Schedulers; +import lombok.SneakyThrows; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.PreparedStatement; +import java.util.concurrent.TimeUnit; + +@Singleton +public class EdgeDataStoreTaskTuner { + + private static final Gauge wmsa_discover_queue_discover_quality_limit = Gauge.build("wmsa_discover_queue_discover_quality_limit", + "wmsa_discover_queue_discover_quality_limit").register(); + private static final Gauge wmsa_discover_queue_index_quality_limit = Gauge.build("wmsa_discover_queue_index_quality_limit", + "wmsa_discover_queue_index_quality_limit").register(); + private static final Gauge wmsa_discover_queue_discover_quality_pool_size = Gauge.build("wmsa_discover_queue_discover_quality_pool_size", + "wmsa_discover_queue_discover_quality_pool_size").register(); + private static final Gauge wmsa_discover_queue_index_quality_pool_size = Gauge.build("wmsa_discover_queue_index_quality_pool_size", + "wmsa_discover_queue_index_quality_pool_size").register(); + private static final Histogram wmsa_discover_queue_tune_time = Histogram.build("wmsa_discover_queue_tune_time", + "wmsa_discover_queue_tune_time").register(); + private static final int INDEX_TARGET = 50; + private static final int DISCOVER_TARGET = 100; + + + private volatile double discoverQualityLimit = -2.; + private volatile double indexQualityLimit = -2.; + + private final HikariDataSource dataSource; + private Logger logger = LoggerFactory.getLogger(getClass()); + + @Inject + public EdgeDataStoreTaskTuner(HikariDataSource dataSource) { + this.dataSource = dataSource; + + Schedulers.io().schedulePeriodicallyDirect(this::tuneDiscoverQualityLimit, 1, 5, TimeUnit.SECONDS); + } + + public double getIndexQualityLimit() { + return indexQualityLimit; + } + public double getDiscoverQualityLimit() { + return discoverQualityLimit; + } + + @SneakyThrows + private double linearSearchBounds(PreparedStatement stmt, + int target, + double delta, + double min, + double max) { + for (double i = max; i >= min; i-=delta) { + int cnt; + stmt.setDouble(1, i); + try (var rsp = stmt.executeQuery()) { + rsp.next(); + cnt = rsp.getInt(1); + } + if (cnt >= target) { + return i; + } + } + + return min; + } + @SneakyThrows + private double binarySearchBounds(PreparedStatement stmt, + int target, + double eps, + double min, + double max) { + while (max - min >= eps) { + double v = (max + min)/2; + stmt.setDouble(1, v); + int cnt; + try (var rsp = stmt.executeQuery()) { + rsp.next(); + cnt = rsp.getInt(1); + } + + if (cnt == target) { + return v; + } else if (cnt > target) { + min = v; + } else { + max = v; + } + } + return min; + } + + @SneakyThrows + private void tuneDiscoverQualityLimit() { + var timer = wmsa_discover_queue_tune_time.startTimer(); + + + try (var connection = dataSource.getConnection()) { + + double delta = 0.1; + double epsilon = 0.000001; + try (var stmt = + connection.prepareStatement("SELECT COUNT(EC_DOMAIN.ID) FROM EC_DOMAIN USE INDEX(EC_DOMAIN_TRIO) WHERE DOMAIN_ALIAS IS NULL AND STATE = 0 AND QUALITY > ? AND INDEXED = 1")) { + + double lower = linearSearchBounds(stmt, INDEX_TARGET, delta, -100, 0); + indexQualityLimit = binarySearchBounds(stmt, INDEX_TARGET, epsilon, lower, lower+delta); + wmsa_discover_queue_index_quality_limit.set(indexQualityLimit); + + var rsp = stmt.executeQuery(); + rsp.next(); + wmsa_discover_queue_index_quality_pool_size.set(rsp.getInt(1)); + } + + + try (var stmt = + connection.prepareStatement("SELECT COUNT(EC_DOMAIN.ID) FROM EC_DOMAIN USE INDEX(EC_DOMAIN_TRIO) INNER JOIN EC_TOP_DOMAIN ON EC_TOP_DOMAIN.ID=URL_TOP_DOMAIN_ID WHERE ALIVE = 1 AND DOMAIN_ALIAS IS NULL AND STATE = 0 AND QUALITY > ? AND INDEXED = 0")) { + + double lower = linearSearchBounds(stmt, DISCOVER_TARGET, delta, -100, 0); + discoverQualityLimit = binarySearchBounds(stmt, DISCOVER_TARGET, epsilon, lower, lower+delta); + + wmsa_discover_queue_discover_quality_limit.set(discoverQualityLimit); + + var rsp = stmt.executeQuery(); + rsp.next(); + wmsa_discover_queue_discover_quality_pool_size.set(rsp.getInt(1)); + } + + } + catch (Exception ex) { + logger.error("Failed to tune quality limits", ex); + } + + timer.observeDuration(); + + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDomainBlacklist.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDomainBlacklist.java new file mode 100644 index 00000000..fa1899b1 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDomainBlacklist.java @@ -0,0 +1,17 @@ +package nu.marginalia.wmsa.edge.data.dao.task; + +import com.google.inject.ImplementedBy; +import gnu.trove.set.hash.TIntHashSet; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeId; + +@ImplementedBy(EdgeDomainBlacklistImpl.class) +public interface EdgeDomainBlacklist { + boolean isBlacklisted(int domainId); + default boolean isBlacklisted(EdgeId domainId) { + return isBlacklisted(domainId.getId()); + } + default TIntHashSet getSpamDomains() { + return new TIntHashSet(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDomainBlacklistImpl.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDomainBlacklistImpl.java new file mode 100644 index 00000000..ea89e7fb --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeDomainBlacklistImpl.java @@ -0,0 +1,77 @@ +package nu.marginalia.wmsa.edge.data.dao.task; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.zaxxer.hikari.HikariDataSource; +import gnu.trove.set.hash.TIntHashSet; +import io.prometheus.client.Counter; +import io.reactivex.rxjava3.schedulers.Schedulers; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +@Singleton +public class EdgeDomainBlacklistImpl implements EdgeDomainBlacklist { + private volatile TIntHashSet spamDomainSet = new TIntHashSet(); + private final HikariDataSource dataSource; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private static final Counter wmsa_blacklist_intercept = Counter.build("wmsa_blacklist_intercept", + "wmsa_blacklist_intercept").register(); + @Inject + public EdgeDomainBlacklistImpl(HikariDataSource dataSource) { + this.dataSource = dataSource; + + Schedulers.io().schedulePeriodicallyDirect(this::updateSpamList, 5, 600, TimeUnit.SECONDS); + + updateSpamList(); + } + + private void updateSpamList() { + try { + int oldSetSize = spamDomainSet.size(); + + spamDomainSet = getSpamDomains(); + + if (oldSetSize == 0) { + logger.info("Synchronized {} spam domains", spamDomainSet.size()); + } + } + catch (Exception ex) { + logger.error("Failed to synchronize spam domains", ex); + } + } + + + @SneakyThrows + public TIntHashSet getSpamDomains() { + final TIntHashSet result = new TIntHashSet(1_000_000); + + try (var connection = dataSource.getConnection()) { + try (var stmt = connection.prepareStatement("SELECT EC_DOMAIN.ID FROM EC_DOMAIN INNER JOIN EC_TOP_DOMAIN ON EC_DOMAIN.URL_TOP_DOMAIN_ID = EC_TOP_DOMAIN.ID INNER JOIN EC_DOMAIN_BLACKLIST ON EC_DOMAIN_BLACKLIST.URL_DOMAIN = EC_TOP_DOMAIN.URL_PART")) { + stmt.setFetchSize(1000); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + result.add(rsp.getInt(1)); + } + } + } + + return result; + } + + @Override + public boolean isBlacklisted(int domainId) { + if (spamDomainSet.contains(domainId)) { + wmsa_blacklist_intercept.inc(); + return true; + } + + return false; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeFinishTasksQueue.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeFinishTasksQueue.java new file mode 100644 index 00000000..d374eea4 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/data/dao/task/EdgeFinishTasksQueue.java @@ -0,0 +1,91 @@ +package nu.marginalia.wmsa.edge.data.dao.task; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.zaxxer.hikari.HikariDataSource; +import lombok.AllArgsConstructor; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.LinkedBlockingQueue; + +@Singleton +public class EdgeFinishTasksQueue { + private final HikariDataSource dataSource; + private final EdgeDataStoreTaskOngoingJobs ongoingJobs; + private final LinkedBlockingQueue finishTasksQueue = new LinkedBlockingQueue<>(10); + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Inject + public EdgeFinishTasksQueue(HikariDataSource dataSource, EdgeDataStoreTaskOngoingJobs ongoingJobs) { + this.dataSource = dataSource; + this.ongoingJobs = ongoingJobs; + + var finishIndexTasks = new Thread(this::finishIndexTasks, "FinishIndexTasks"); + finishIndexTasks.setDaemon(true); + finishIndexTasks.start(); + } + + + private void finishIndexTasks() { + for (;;) { + try (var connection = dataSource.getConnection()) { + + var task = finishTasksQueue.take(); + + connection.setAutoCommit(false); + + if (task.quality != null) { + + try (var stmt = + connection.prepareStatement("UPDATE EC_DOMAIN LEFT JOIN (SELECT DOMAIN_ID, AVG(QUALITY_MEASURE) AVGQ FROM EC_URL GROUP BY DOMAIN_ID) QUALITY ON EC_DOMAIN.ID = QUALITY.DOMAIN_ID SET INDEXED=INDEXED+1, INDEX_DATE=NOW(), QUALITY=IFNULL(AVGQ,?-INDEXED/2)*IFNULL(RANK,1), QUALITY_RAW=IFNULL(AVGQ,-5), STATE=? WHERE EC_DOMAIN.URL_PART=?")) { + stmt.setDouble(1, task.quality); + stmt.setInt(2, task.state.code); + stmt.setString(3, task.domain.toString()); + stmt.execute(); + + connection.commit(); + ongoingJobs.remove(task.domain); + } catch (Exception ex) { + logger.error("DB error", ex); + connection.rollback(); + } + } + else { + try (var stmt = + connection.prepareStatement("UPDATE EC_DOMAIN SET STATE=? WHERE URL_PART=?")) { + stmt.setInt(1, task.state.code); + stmt.setString(2, task.domain.toString()); + stmt.execute(); + + connection.commit(); + ongoingJobs.remove(task.domain); + } catch (Exception ex) { + logger.error("DB error", ex); + connection.rollback(); + } + } + } + catch (Exception ex) { + logger.error("DB error", ex); + } + } + } + + public void add(EdgeDomain domain, double quality, EdgeDomainIndexingState state) throws InterruptedException { + finishTasksQueue.put(new EdgeFinishTaskSpecs(domain, quality, state)); + } + public void addError(EdgeDomain domain, EdgeDomainIndexingState state) throws InterruptedException { + finishTasksQueue.put(new EdgeFinishTaskSpecs(domain, null, state)); + } + @AllArgsConstructor + private static class EdgeFinishTaskSpecs { + public final EdgeDomain domain; + public final Double quality; + public final EdgeDomainIndexingState state; + } + +} + diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/dating/DatingMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/dating/DatingMain.java new file mode 100644 index 00000000..1405f981 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/dating/DatingMain.java @@ -0,0 +1,37 @@ +package nu.marginalia.wmsa.edge.dating; + +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import nu.marginalia.wmsa.configuration.MainClass; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.module.ConfigurationModule; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.configuration.server.Initialization; +import spark.Spark; + +import java.io.IOException; + +public class DatingMain extends MainClass { + DatingService service; + + @Inject + public DatingMain(DatingService service) throws IOException { + this.service = service; + } + + public static void main(String... args) { + init(ServiceDescriptor.DATING, args); + + Spark.staticFileLocation("/static/dating/"); + + Injector injector = Guice.createInjector( + new DatingModule(), + new ConfigurationModule(), + new DatabaseModule() + ); + + injector.getInstance(DatingMain.class); + injector.getInstance(Initialization.class).setReady(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/dating/DatingModule.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/dating/DatingModule.java new file mode 100644 index 00000000..b92f67ba --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/dating/DatingModule.java @@ -0,0 +1,6 @@ +package nu.marginalia.wmsa.edge.dating; + +import com.google.inject.AbstractModule; + +public class DatingModule extends AbstractModule { +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/dating/DatingService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/dating/DatingService.java new file mode 100644 index 00000000..406a7dfd --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/dating/DatingService.java @@ -0,0 +1,184 @@ +package nu.marginalia.wmsa.edge.dating; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.configuration.server.MetricsServer; +import nu.marginalia.wmsa.configuration.server.Service; +import nu.marginalia.wmsa.edge.assistant.screenshot.ScreenshotService; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDao; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDomainBlacklist; +import nu.marginalia.wmsa.edge.model.EdgeId; +import nu.marginalia.wmsa.edge.search.BrowseResult; +import nu.marginalia.wmsa.renderer.mustache.MustacheRenderer; +import nu.marginalia.wmsa.renderer.mustache.RendererFactory; +import org.jetbrains.annotations.NotNull; +import spark.Request; +import spark.Response; +import spark.Spark; +import spark.resource.ClassPathResource; + +import java.io.FileNotFoundException; +import java.util.Map; +import java.util.Optional; + +public class DatingService extends Service { + private final EdgeDataStoreDao edgeDataStoreDao; + private final EdgeDomainBlacklist blacklist; + private final MustacheRenderer datingRenderer; + private final ScreenshotService screenshotService; + private final String SESSION_OBJECT_NAME = "so"; + @SneakyThrows + @Inject + public DatingService(@Named("service-host") String ip, + @Named("service-port") Integer port, + EdgeDataStoreDao edgeDataStoreDao, + RendererFactory rendererFactory, + Initialization initialization, + MetricsServer metricsServer, + EdgeDomainBlacklist blacklist, + ScreenshotService screenshotService) { + + super(ip, port, initialization, metricsServer); + + this.edgeDataStoreDao = edgeDataStoreDao; + this.blacklist = blacklist; + + datingRenderer = rendererFactory.renderer("dating/dating-view"); + this.screenshotService = screenshotService; + + Spark.get("/public/reset", this::getReset); + Spark.get("/public/", this::serveIndex); + Spark.get("/public/view", this::getCurrent); + Spark.get("/public/next", this::getNext); + Spark.get("/public/similar/:id", this::getSimilar); + Spark.get("/public/rewind", this::getRewind); + Spark.get("/public/init", this::getInitSession); + } + + @SneakyThrows + private Object serveIndex(Request request, Response response) { + try { + ClassPathResource resource = new ClassPathResource("static/dating/index.html"); + resource.getInputStream().transferTo(response.raw().getOutputStream()); + } + catch (IllegalArgumentException| FileNotFoundException ex) { + return false; + } + return ""; + } + + private Object getInitSession(Request request, Response response) { + var sessionObjectOpt = getSession(request); + if (sessionObjectOpt.isEmpty()) { + request.session(true).attribute(SESSION_OBJECT_NAME, new DatingSessionObject()); + } + response.redirect("https://explore.marginalia.nu/view"); + return ""; + } + + private String getReset(Request request, Response response) { + var sessionObjectOpt = getSession(request); + if (sessionObjectOpt.isEmpty()) { + response.redirect("https://explore.marginalia.nu/"); + return ""; + } + var session = sessionObjectOpt.get(); + session.resetQueue(); + + return getNext(request, response); + } + + private String getCurrent(Request request, Response response) { + var sessionObjectOpt = getSession(request); + if (sessionObjectOpt.isEmpty()) { + response.redirect("https://explore.marginalia.nu/"); + return ""; + } + var session = sessionObjectOpt.get(); + + var current = session.getCurrent(); + if (current == null) { + BrowseResult res = session.next(edgeDataStoreDao, blacklist); + res = findViableDomain(session, res); + session.browseForward(res); + current = session.getCurrent(); + } + + return datingRenderer.render(current, Map.of("back", session.hasHistory())); + } + + private String getNext(Request request, Response response) { + var sessionObjectOpt = getSession(request); + if (sessionObjectOpt.isEmpty()) { + response.redirect("https://explore.marginalia.nu/"); + return ""; + } + var session = sessionObjectOpt.get(); + + BrowseResult res = session.next(edgeDataStoreDao, blacklist); + + res = findViableDomain(session, res); + + session.browseForward(res); + + response.redirect("https://explore.marginalia.nu/view"); + return ""; + } + + private String getRewind(Request request, Response response) { + var sessionObjectOpt = getSession(request); + if (sessionObjectOpt.isEmpty()) { + response.redirect("https://explore.marginalia.nu/"); + return ""; + } + var session = sessionObjectOpt.get(); + + BrowseResult res = session.takeFromHistory(); + if (res == null) { + Spark.halt(404); + return ""; + } + + session.browseBackward(res); + + response.redirect("https://explore.marginalia.nu/view"); + return ""; + } + + + private String getSimilar(Request request, Response response) { + var sessionObjectOpt = getSession(request); + if (sessionObjectOpt.isEmpty()) { + response.redirect("https://explore.marginalia.nu/"); + return ""; + } + var session = sessionObjectOpt.get(); + + int id = Integer.parseInt(request.params("id")); + BrowseResult res = session.nextSimilar(new EdgeId<>(id), edgeDataStoreDao, blacklist); + + res = findViableDomain(session, res); + + session.browseForward(res); + + response.redirect("https://explore.marginalia.nu/view"); + return ""; + } + + @NotNull + private BrowseResult findViableDomain(DatingSessionObject session, BrowseResult res) { + while (!screenshotService.hasScreenshot(new EdgeId<>(res.domainId)) || session.isRecent(res)) { + res = session.next(edgeDataStoreDao, blacklist); + } + return res; + } + + + private Optional getSession(Request request) { + return Optional.ofNullable(request.session(false)) + .map(s -> s.attribute(SESSION_OBJECT_NAME)) + .map(DatingSessionObject.class::cast); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/dating/DatingSessionObject.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/dating/DatingSessionObject.java new file mode 100644 index 00000000..c67cb51d --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/dating/DatingSessionObject.java @@ -0,0 +1,89 @@ +package nu.marginalia.wmsa.edge.dating; + +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDao; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDomainBlacklist; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeId; +import nu.marginalia.wmsa.edge.search.BrowseResult; + +import java.util.LinkedList; + +public class DatingSessionObject { + public final LinkedList queue = new LinkedList<>(); + public final LinkedList recentlyViewed = new LinkedList<>(); + private BrowseResult current; + + private static final int MAX_HISTORY_SIZE = 100; + private static final int MAX_QUEUE_SIZE = 100; + + public BrowseResult setCurrent(BrowseResult result) { + current = result; + return current; + } + + public BrowseResult next(EdgeDataStoreDao dao, EdgeDomainBlacklist blacklist) { + if (queue.isEmpty()) { + dao.getRandomDomains(25, blacklist).forEach(queue::addLast); + } + return queue.pollFirst(); + } + + public BrowseResult nextSimilar(EdgeId id, EdgeDataStoreDao dao, EdgeDomainBlacklist blacklist) { + dao.getDomainNeighborsAdjacent(id, blacklist, 25).forEach(queue::addFirst); + + while (queue.size() > MAX_QUEUE_SIZE) { + queue.removeLast(); + } + + return queue.pollFirst(); + } + + public void browseForward(BrowseResult res) { + if (current != null) { + addToHistory(current); + } + setCurrent(res); + } + + public void browseBackward(BrowseResult res) { + if (current != null) { + addToQueue(current); + } + setCurrent(res); + } + + public BrowseResult addToHistory(BrowseResult res) { + recentlyViewed.addFirst(res); + while (recentlyViewed.size() > MAX_HISTORY_SIZE) { + recentlyViewed.removeLast(); + } + return res; + } + + public BrowseResult addToQueue(BrowseResult res) { + queue.addFirst(res); + while (queue.size() > MAX_QUEUE_SIZE) { + queue.removeLast(); + } + return res; + } + + public BrowseResult takeFromHistory() { + return recentlyViewed.pollFirst(); + } + + public boolean hasHistory() { + return !recentlyViewed.isEmpty(); + } + + public boolean isRecent(BrowseResult res) { + return recentlyViewed.contains(res) || res.equals(current); + } + public void resetQueue() { + queue.clear(); + } + + public BrowseResult getCurrent() { + return current; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/director/EdgeDirectorMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/director/EdgeDirectorMain.java new file mode 100644 index 00000000..66b3e998 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/director/EdgeDirectorMain.java @@ -0,0 +1,35 @@ +package nu.marginalia.wmsa.edge.director; + +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import nu.marginalia.wmsa.configuration.MainClass; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.module.ConfigurationModule; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.configuration.server.Initialization; + +import java.io.IOException; + +public class EdgeDirectorMain extends MainClass { + private EdgeDirectorService service; + + @Inject + public EdgeDirectorMain(EdgeDirectorService service) throws IOException { + this.service = service; + } + + public static void main(String... args) { + init(ServiceDescriptor.EDGE_DIRECTOR, args); + + Injector injector = Guice.createInjector( + new DatabaseModule(), + new EdgeDirectorModule(), + new ConfigurationModule() + ); + + injector.getInstance(EdgeDirectorMain.class); + injector.getInstance(Initialization.class).setReady(); + + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/director/EdgeDirectorModule.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/director/EdgeDirectorModule.java new file mode 100644 index 00000000..c0271b07 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/director/EdgeDirectorModule.java @@ -0,0 +1,6 @@ +package nu.marginalia.wmsa.edge.director; + +import com.google.inject.AbstractModule; + +public class EdgeDirectorModule extends AbstractModule { +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/director/EdgeDirectorService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/director/EdgeDirectorService.java new file mode 100644 index 00000000..1edfd17e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/director/EdgeDirectorService.java @@ -0,0 +1,109 @@ +package nu.marginalia.wmsa.edge.director; + + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.inject.Inject; +import com.google.inject.name.Named; +import io.prometheus.client.Histogram; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.configuration.server.MetricsServer; +import nu.marginalia.wmsa.configuration.server.Service; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreTaskDao; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import org.eclipse.jetty.util.UrlEncoded; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Request; +import spark.Response; +import spark.Spark; + +import static spark.Spark.*; + +public class EdgeDirectorService extends Service { + private final Gson gson = new GsonBuilder().create(); + private final EdgeDataStoreTaskDao taskDao; + + static final Histogram request_time_metrics + = Histogram.build("wmsa_edge_director_request_time", "DB Request Time") + .labelNames("request") + .register(); + private Logger logger = LoggerFactory.getLogger(getClass()); + + @Inject + public EdgeDirectorService(@Named("service-host") String ip, + @Named("service-port") Integer port, + Initialization init, + EdgeDataStoreTaskDao taskDao, + MetricsServer metricsServer) + { + super(ip, port, init, metricsServer); + this.taskDao = taskDao; + + + Spark.path("edge", () -> { + get("/task/index/:pass", this::getIndexTask, this::convertToJson); + get("/task/discover/", this::getDiscoverTask, this::convertToJson); + delete("/task/*", this::finishTask, this::convertToJson); + get("/task/blocked", this::isBlocked, this::convertToJson); + post("/task/flush", this::flushTasks, this::convertToJson); + }); + + } + + private Object flushTasks(Request request, Response response) { + logger.info("Flushing ongoing jobs"); + taskDao.flushOngoingJobs(); + return "Ok"; + } + + private Object isBlocked(Request request, Response response) { + return taskDao.isBlocked(); + } + + public Object getIndexTask(Request request, Response response) { + final long start = System.currentTimeMillis(); + + response.header("Content-Encoding", "gzip"); + var ret = taskDao.getIndexTask(Integer.parseInt(request.params("pass")), Integer.parseInt(request.queryParams("limit"))); + + request_time_metrics.labels("get_index_task").observe(System.currentTimeMillis() - start); + + return ret; + } + + public Object getDiscoverTask(Request request, Response response) { + final long start = System.currentTimeMillis(); + + response.header("Content-Encoding", "gzip"); + var ret = taskDao.getDiscoverTask(); + + request_time_metrics.labels("get_discover_task").observe(System.currentTimeMillis() - start); + + return ret; + } + + public Object finishTask(Request request, Response response) { + final long start = System.currentTimeMillis(); + + var domain = UrlEncoded.decodeString(request.splat()[0]); + EdgeDomainIndexingState state = EdgeDomainIndexingState.valueOf(request.queryParams("state")); + + if (state.code < 0) { + taskDao.finishBadIndexTask(new EdgeDomain(domain), state); + } + else { + double quality = Double.parseDouble(request.queryParams("quality")); + taskDao.finishIndexTask(new EdgeDomain(domain), quality, state); + } + + request_time_metrics.labels("finish_task").observe(System.currentTimeMillis() - start); + return null; + } + + + private String convertToJson(Object o) { + return gson.toJson(o); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/director/client/EdgeDirectorClient.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/director/client/EdgeDirectorClient.java new file mode 100644 index 00000000..3ff70623 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/director/client/EdgeDirectorClient.java @@ -0,0 +1,49 @@ +package nu.marginalia.wmsa.edge.director.client; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import io.reactivex.rxjava3.core.Observable; +import nu.marginalia.wmsa.client.AbstractDynamicClient; +import nu.marginalia.wmsa.client.HttpStatusCode; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.crawl.EdgeIndexTask; +import org.eclipse.jetty.util.UrlEncoded; + +import javax.annotation.CheckReturnValue; + +@Singleton +public class EdgeDirectorClient extends AbstractDynamicClient { + + @Inject + public EdgeDirectorClient() { + super(ServiceDescriptor.EDGE_DIRECTOR); + } + + @CheckReturnValue + public Observable getIndexTask(Context ctx, int pass, int limit) { + return super.get(ctx, "/edge/task/index/"+pass+"?limit="+limit, EdgeIndexTask.class); + } + @CheckReturnValue + public Observable getDiscoverTask(Context ctx) { + return super.get(ctx, "/edge/task/discover/", EdgeIndexTask.class); + } + + @CheckReturnValue + public Observable finishTask(Context ctx, EdgeDomain domain, double quality, EdgeDomainIndexingState state) { + return super.delete(ctx, "/edge/task/"+ UrlEncoded.encodeString(domain.toString())+"?quality="+quality+"&state="+state.toString()); + } + + @CheckReturnValue + public Observable isBlocked(Context ctx) { + return super.get(ctx, "/edge/task/blocked", Boolean.class); + } + + @CheckReturnValue + public void flushOngoingJobs(Context ctx) { + super.post(ctx, "/edge/task/flush", new Object()).blockingSubscribe(); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeIndexControl.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeIndexControl.java new file mode 100644 index 00000000..3c65464e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeIndexControl.java @@ -0,0 +1,40 @@ +package nu.marginalia.wmsa.edge.index; + + +import com.google.inject.Inject; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; + + +public class EdgeIndexControl { + + private final IndexServicesFactory servicesFactory; + + @Inject + public EdgeIndexControl(IndexServicesFactory servicesFactory) { + this.servicesFactory = servicesFactory; + } + + public void regenerateIndex(int id) { + System.runFinalization(); + System.gc(); + + for (IndexBlock block : IndexBlock.values()) { + + servicesFactory.getIndexConverter(id, block); + + System.runFinalization(); + System.gc(); + } + + System.runFinalization(); + System.gc(); + } + + public long wordCount(int id) { + return servicesFactory.wordCount(id); + } + + public void switchIndexFiles(int id) throws Exception { + servicesFactory.switchFilesJob(id).call(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeIndexMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeIndexMain.java new file mode 100644 index 00000000..61b57dbb --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeIndexMain.java @@ -0,0 +1,36 @@ +package nu.marginalia.wmsa.edge.index; + +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import nu.marginalia.wmsa.configuration.MainClass; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.module.ConfigurationModule; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.configuration.server.Initialization; + +import java.io.IOException; + +public class EdgeIndexMain extends MainClass { + private EdgeIndexService service; + + @Inject + public EdgeIndexMain(EdgeIndexService service) throws IOException { + this.service = service; + } + + public static void main(String... args) { + init(ServiceDescriptor.EDGE_INDEX, args); + + Injector injector = Guice.createInjector( + new EdgeTablesModule(), + new EdgeIndexModule(), + new DatabaseModule(), + new ConfigurationModule() + ); + + injector.getInstance(EdgeIndexMain.class); + injector.getInstance(Initialization.class).setReady(); + + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeIndexModule.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeIndexModule.java new file mode 100644 index 00000000..f12212ec --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeIndexModule.java @@ -0,0 +1,12 @@ +package nu.marginalia.wmsa.edge.index; + +import com.google.inject.AbstractModule; +import com.google.inject.name.Names; + +public class EdgeIndexModule extends AbstractModule { + + public void configure() { + bind(Long.class).annotatedWith(Names.named("edge-dictionary-hash-map-size")).toInstance(1L << 31); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeIndexService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeIndexService.java new file mode 100644 index 00000000..14ce0673 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeIndexService.java @@ -0,0 +1,506 @@ +package nu.marginalia.wmsa.edge.index; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.inject.Inject; +import com.google.inject.name.Named; +import gnu.trove.map.TLongIntMap; +import gnu.trove.map.hash.TIntIntHashMap; +import gnu.trove.map.hash.TLongIntHashMap; +import gnu.trove.set.hash.TIntHashSet; +import io.prometheus.client.Counter; +import io.prometheus.client.Histogram; +import io.reactivex.rxjava3.schedulers.Schedulers; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.configuration.server.MetricsServer; +import nu.marginalia.wmsa.configuration.server.Service; +import nu.marginalia.wmsa.edge.index.model.*; +import nu.marginalia.wmsa.edge.index.service.SearchIndexes; +import nu.marginalia.wmsa.edge.index.service.index.SearchIndexWriterImpl; +import nu.marginalia.wmsa.edge.index.service.query.IndexSearchBudget; +import nu.marginalia.util.dict.DictionaryHashMap; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageWordSet; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageWords; +import nu.marginalia.wmsa.edge.model.search.*; +import org.apache.http.HttpStatus; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.HaltException; +import spark.Request; +import spark.Response; +import spark.Spark; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.function.LongPredicate; +import java.util.stream.LongStream; + +import static spark.Spark.get; +import static spark.Spark.halt; + +public class EdgeIndexService extends Service { + private static final int SEARCH_BUDGET_LIMIT = 1_000_000; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @NotNull + private final Initialization init; + private final SearchIndexes indexes; + + private final Gson gson = new GsonBuilder().create(); + + private static final Histogram wmsa_edge_index_query_time + = Histogram.build().name("wmsa_edge_index_query_time").help("-").register(); + private static final Counter wmsa_edge_index_query_count + = Counter.build().name("wmsa_edge_index_query_count").help("-").register(); + private static final Histogram wmsa_edge_index_put_words_time + = Histogram.build().name("wmsa_edge_index_put_words_time").help("-").register(); + + public static final int DYNAMIC_BUCKET_LENGTH = 7; + + + @Inject + public EdgeIndexService(@Named("service-host") String ip, + @Named("service-port") Integer port, + Initialization init, + MetricsServer metricsServer, + SearchIndexes indexes + ) { + super(ip, port, init, metricsServer); + + this.init = init; + this.indexes = indexes; + + Spark.post("/words/", this::putWords); + Spark.post("/search/", this::search, gson::toJson); + + Spark.post("/dictionary/*", this::getWordId, gson::toJson); + + Spark.post("/ops/repartition", this::repartitionEndpoint); + Spark.post("/ops/preconvert", this::preconvertEndpoint); + Spark.post("/ops/reindex/:id", this::reindexEndpoint); + + get("/is-blocked", this::isBlocked, gson::toJson); + + Schedulers.newThread().scheduleDirect(this::initialize, 5, TimeUnit.SECONDS); + } + + private Object getWordId(Request request, Response response) { + final String word = request.splat()[0]; + + var dr = indexes.getDictionaryReader(); + if (null == dr) { + response.status(HttpStatus.SC_FAILED_DEPENDENCY); + return ""; + } + + final int wordId = dr.get(word); + + if (DictionaryHashMap.NO_VALUE == wordId) { + response.status(404); + return ""; + } + + return wordId; + } + + private Object repartitionEndpoint(Request request, Response response) { + + if (!indexes.repartition()) { + Spark.halt(503, "Operations busy"); + } + return "OK"; + } + + private Object preconvertEndpoint(Request request, Response response) { + if (!indexes.preconvert()) { + Spark.halt(503, "Operations busy"); + } + return "OK"; + } + + private Object reindexEndpoint(Request request, Response response) { + int id = Integer.parseInt(request.params("id")); + + if (!indexes.reindex(id)) { + Spark.halt(503, "Operations busy"); + } + return "OK"; + } + + private Object isBlocked(Request request, Response response) { + return indexes.isBusy() || !initialized; + } + + volatile boolean initialized = false; + public void initialize() { + if (!initialized) { + initialized = true; + } + else { + return; + } + indexes.initialize(init); + } + + private Object putWords(Request request, Response response) { + var putWordsRequest = gson.fromJson(request.body(), EdgePutWordsRequest.class); + + synchronized (this) { + putWords(putWordsRequest.getDomainId(), putWordsRequest.getUrlId(), + putWordsRequest.wordSet, putWordsRequest.getIndex()); + } + + response.status(HttpStatus.SC_ACCEPTED); + return ""; + } + + public void putWords(EdgeId domainId, EdgeId urlId, + EdgePageWordSet wordSet, int idx + ) { + + wmsa_edge_index_put_words_time.time(() -> { + for (EdgePageWords words : wordSet.values()) { + putWords(domainId, urlId, words, idx); + } + }); + + } + + public void putWords(EdgeId domainId, EdgeId urlId, + EdgePageWords words, int idx + ) { + SearchIndexWriterImpl indexWriter = indexes.getIndexWriter(idx); + + if (!words.words.isEmpty()) { + if (words.size() < 1000) { + indexWriter.put(domainId, urlId, words.block, words.words); + } else { + chunks(words.words, 1000).forEach(chunk -> { + indexWriter.put(domainId, urlId, words.block, chunk); + }); + } + } + } + + + private List> chunks(Collection coll, int size) { + List> ret = new ArrayList<>(); + List data = List.copyOf(coll); + + for (int i = 0; i < data.size(); i+=size) { + ret.add(data.subList(i, Math.min(data.size(), i+size))); + } + + return ret; + } + + private Object search(Request request, Response response) { + if (indexes.getDictionaryReader() == null) { + halt(HttpStatus.SC_SERVICE_UNAVAILABLE, "Come back in a few minutes"); + } + + String json = request.body(); + EdgeSearchSpecification specsSet = gson.fromJson(json, EdgeSearchSpecification.class); + + long start = System.currentTimeMillis(); + + + try { + if (specsSet.isStagger()) { + return new EdgeSearchResultSet(searchStaggered(specsSet)); + } + else { + return new EdgeSearchResultSet(searchStraight(specsSet)); + } + } + catch (HaltException ex) { + logger.warn("Halt", ex); + throw ex; + } + catch (Exception ex) { + logger.info("Error during search {}({}) (query: {})", ex.getClass().getSimpleName(), ex.getMessage(), json); + logger.info("Error", ex); + Spark.halt(500, "Error"); + return null; + } + finally { + wmsa_edge_index_query_time.observe(System.currentTimeMillis() - start); + wmsa_edge_index_query_count.inc(); + } + } + + private Map> searchStaggered(EdgeSearchSpecification specsSet) { + int count = 0; + + final Map> results = new HashMap<>(); + final TIntHashSet seenResults = new TIntHashSet(); + + final DomainResultCountFilter[] domainCountFilter = new DomainResultCountFilter[] { + new DomainResultCountFilter(specsSet.limitByDomain), + new DomainResultCountFilter(specsSet.limitByDomain) + }; + + final IndexSearchBudget budget = new IndexSearchBudget(SEARCH_BUDGET_LIMIT); + final TIntIntHashMap limitsPerBucketRemaining = new TIntIntHashMap(6, 0.7f, 0, specsSet.limitByBucket); + + for (int i = 0; i < specsSet.buckets.size(); i+=2) { + for (var sq : specsSet.subqueries) { + for (int j = 0; j < 2 && i + j < specsSet.buckets.size(); j++) { + Optional searchTerms = getSearchTerms(sq); + + if (searchTerms.isEmpty()) + continue; + + var result = performSearch(searchTerms.get(), + budget, + seenResults, + domainCountFilter[j], + sq, + List.of(specsSet.buckets.get(i+j)), + specsSet, + Math.min(limitsPerBucketRemaining.get(i+j), specsSet.limitTotal - count) + ); + + if (logger.isDebugEnabled()) { + logger.debug("{} -> {} {} {}", sq.block, specsSet.buckets.get(i+j), sq.searchTermsInclude, result.results.values().stream().mapToInt(List::size).sum()); + } + + int sz = result.size(); + count += sz; + limitsPerBucketRemaining.adjustOrPutValue(i+j, -sz, specsSet.limitByBucket-sz); + + if (sz > 0) { + results.computeIfAbsent(sq.block, s -> new ArrayList<>()).add(result); + } + } + } + } + + if (budget.used() > 0) { + logger.debug("Query used ${}", budget.used()); + } + + return results; + } + + @NotNull + private Map> searchStraight(EdgeSearchSpecification specsSet) { + Map> results = new HashMap<>(); + int count = 0; + TIntHashSet seenResults = new TIntHashSet(); + + final DomainResultCountFilter domainCountFilter = new DomainResultCountFilter(specsSet.limitByDomain); + + IndexSearchBudget budget = new IndexSearchBudget(SEARCH_BUDGET_LIMIT); + for (var sq : specsSet.subqueries) { + Optional searchTerms = getSearchTerms(sq); + + if (searchTerms.isEmpty()) + continue; + + var result = performSearch(searchTerms.get(), + budget, seenResults, domainCountFilter, + sq, specsSet.buckets, specsSet, + specsSet.limitTotal - count); + + if (logger.isDebugEnabled()) { + logger.debug("{} -> {} {}", sq.block, sq.searchTermsInclude, result.size()); + } + + count += result.size(); + if (result.size() > 0) { + results.computeIfAbsent(sq.block, s -> new ArrayList<>()).add(result); + } + } + + if (budget.used() > 0) { + logger.debug("Query used ${}", budget.used()); + } + + return results; + } + + private EdgeSearchResults performSearch(EdgeIndexSearchTerms searchTerms, + IndexSearchBudget budget, + TIntHashSet seenResults, + DomainResultCountFilter domainCountFilter, + EdgeSearchSubquery sq, + List specBuckets, + EdgeSearchSpecification specs, + int limit) + { + if (limit <= 0) { + return new EdgeSearchResults(); + } + + final Map> results = new HashMap<>(); + final DomainResultCountFilter localFilter = new DomainResultCountFilter(specs.limitByDomain); + + boolean debug = sq.searchTermsExclude.contains("special:debug"); + + for (int i : specBuckets) { + int foundResultsCount = results.values().stream().mapToInt(List::size).sum(); + + if (foundResultsCount >= specs.limitTotal || foundResultsCount >= limit) + break; + + List resultsForBucket = new ArrayList<>(specs.limitByBucket); + + if (debug) { + getQuery(i, budget, sq.block, lv -> localFilter.filterRawValue(i, lv), searchTerms) + .peek(l -> logger.info("Considering {}", Long.toHexString(l))) + .mapToObj(id -> new EdgeSearchResultItem(i, sq.termSize(), id)) + .filter(ri -> { + if (seenResults.contains(ri.url.getId())) { + logger.info("Seen before: {}", Integer.toHexString(ri.url.getId())); + return false; + } + else if (!localFilter.test(i, domainCountFilter, ri)) { + logger.info("DCF: {} - {}:{}", ri.blockId, Integer.toHexString(ri.domain.getId()), Integer.toHexString(ri.url.getId())); + return false; + } + return true; + }) + .limit(specs.limitTotal * 3L) + .distinct() + .limit(Math.min(specs.limitByBucket + - results.values().stream().mapToInt(Collection::size).sum(), limit - foundResultsCount)) + .forEach(resultsForBucket::add); + } + else { + getQuery(i, budget, sq.block, lv -> localFilter.filterRawValue(i, lv), searchTerms) + .mapToObj(id -> new EdgeSearchResultItem(i, sq.termSize(), id)) + .filter(ri -> !seenResults.contains(ri.url.getId()) && localFilter.test(i, domainCountFilter, ri)) + .limit(specs.limitTotal * 3L) + .distinct() + .limit(Math.min(specs.limitByBucket + - results.values().stream().mapToInt(Collection::size).sum(), limit - foundResultsCount)) + .forEach(resultsForBucket::add); + } + + for (var result : resultsForBucket) { + seenResults.add(result.url.getId()); + } + for (var result : resultsForBucket) { + for (var searchTerm : sq.searchTermsInclude) { + result.scores.add(getSearchTermScore(i, searchTerm, result.getCombinedId())); + } + } + + domainCountFilter.addAll(i, resultsForBucket); + + if (!resultsForBucket.isEmpty()) { + results.put(i, resultsForBucket); + } + } + + return new EdgeSearchResults(results); + } + + private EdgeSearchResultKeywordScore getSearchTermScore(int bucketId, String term, long urlId) { + final int termId = indexes.getDictionaryReader().get(term); + + var bucket = indexes.getBucket(bucketId); + + return new EdgeSearchResultKeywordScore(term, + bucket.getTermScore(termId, urlId), + bucket.isTermInBucket(IndexBlock.Title, termId, urlId), + bucket.isTermInBucket(IndexBlock.Link, termId, urlId) + ); + + } + + private LongStream getQuery(int bucket, IndexSearchBudget budget, IndexBlock block, + LongPredicate filter, EdgeIndexSearchTerms searchTerms) { + if (!indexes.isValidBucket(bucket)) { + logger.warn("Invalid bucket {}", bucket); + return LongStream.empty(); + } + return indexes.getBucket(bucket).getQuery(block, filter, budget, searchTerms); + } + + static class DomainResultCountFilter { + final TLongIntMap resultsByDomain = new TLongIntHashMap(200, 0.75f, -1, 0); + final int limitByDomain; + + DomainResultCountFilter(int limitByDomain) { + this.limitByDomain = limitByDomain; + } + + public boolean filterRawValue(int bucket, long value) { + var domain = new EdgeId((int)(value >>> 32)); + + if (domain.getId() == Integer.MAX_VALUE) { + return true; + } + + return resultsByDomain.get(getKey(bucket, domain)) <= limitByDomain; + } + + long getKey(int bucket, EdgeId id) { + return ((long)bucket) << 32 | id.getId(); + } + + public boolean test(int bucket, EdgeSearchResultItem item) { + if (item.domain.getId() == Integer.MAX_VALUE) { + return true; + } + + return resultsByDomain.adjustOrPutValue(getKey(bucket, item.domain), 1, 1) <= limitByDomain; + } + + int getCount(int bucket, EdgeSearchResultItem item) { + return resultsByDomain.get(getKey(bucket, item.domain)); + } + + public void addAll(int bucket, List items) { + items.forEach(item -> { + resultsByDomain.adjustOrPutValue(getKey(bucket, item.domain), 1, 1); + }); + } + + public boolean test(int bucket, DomainResultCountFilter root, EdgeSearchResultItem item) { + if (item.domain.getId() == Integer.MAX_VALUE) { + return true; + } + return root.getCount(bucket, item) + resultsByDomain.adjustOrPutValue(getKey(bucket, item.domain), 1, 1) <= limitByDomain; + } + } + + private Optional getSearchTerms(EdgeSearchSubquery request) { + final List excludes = new ArrayList<>(); + final List includes = new ArrayList<>(); + + for (var include : request.searchTermsInclude) { + var word = lookUpWord(include); + if (word.isEmpty()) { + logger.debug("Unknown search term: " + include); + return Optional.empty(); + } + includes.add(word.getAsInt()); + } + + for (var exclude : request.searchTermsExclude) { + lookUpWord(exclude).ifPresent(excludes::add); + } + + if (includes.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(new EdgeIndexSearchTerms(includes, excludes)); + } + + private OptionalInt lookUpWord(String s) { + int ret = indexes.getDictionaryReader().get(s); + if (ret == DictionaryHashMap.NO_VALUE) { + return OptionalInt.empty(); + } + return OptionalInt.of(ret); + } + +} + diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeTablesModule.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeTablesModule.java new file mode 100644 index 00000000..bc9c2f44 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/EdgeTablesModule.java @@ -0,0 +1,28 @@ +package nu.marginalia.wmsa.edge.index; + +import com.google.inject.AbstractModule; +import com.google.inject.name.Names; + +import java.nio.file.Path; + +public class EdgeTablesModule extends AbstractModule { + + public void configure() { + bind(Path.class).annotatedWith(Names.named("partition-root-slow")).toInstance(Path.of("/var/lib/wmsa/index/write")); + bind(Path.class).annotatedWith(Names.named("partition-root-slow-tmp")).toInstance(Path.of("/backup/work/index-tmp/")); + + bind(Path.class).annotatedWith(Names.named("partition-root-fast")).toInstance(Path.of("/var/lib/wmsa/index/read")); + bind(Path.class).annotatedWith(Names.named("tmp-file-dir")).toInstance(Path.of("/var/lib/wmsa/index/read")); + + bind(String.class).annotatedWith(Names.named("edge-writer-page-index-file")).toInstance("page-index.dat"); + bind(String.class).annotatedWith(Names.named("edge-writer-dictionary-file")).toInstance("dictionary.dat"); + + bind(String.class).annotatedWith(Names.named("edge-index-write-words-file")).toInstance("words.dat.wip"); + bind(String.class).annotatedWith(Names.named("edge-index-write-urls-file")).toInstance("urls.dat.wip"); + + bind(String.class).annotatedWith(Names.named("edge-index-read-words-file")).toInstance("words.dat"); + bind(String.class).annotatedWith(Names.named("edge-index-read-urls-file")).toInstance("urls.dat"); + + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/IndexServicesFactory.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/IndexServicesFactory.java new file mode 100644 index 00000000..9b26989c --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/IndexServicesFactory.java @@ -0,0 +1,223 @@ +package nu.marginalia.wmsa.edge.index; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDomainBlacklist; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.index.radix.EdgeIndexBucket; +import nu.marginalia.wmsa.edge.index.service.dictionary.DictionaryReader; +import nu.marginalia.wmsa.edge.index.service.dictionary.DictionaryWriter; +import nu.marginalia.wmsa.edge.index.service.index.*; +import nu.marginalia.wmsa.edge.index.service.query.SearchIndexPartitioner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.EnumMap; +import java.util.concurrent.Callable; + +import static nu.marginalia.wmsa.edge.index.EdgeIndexService.DYNAMIC_BUCKET_LENGTH; + +@Singleton +public class IndexServicesFactory { + private final Path tmpFileDir; + private final EdgeDomainBlacklist domainBlacklist; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final PartitionedDataFile writerIndexFile; + private final RootDataFile writerDictionaryFile; + private final PartitionedDataFile preconverterOutputFile; + private final DoublePartitionedDataFile indexReadWordsFile; + private final DoublePartitionedDataFile indexReadUrlsFile; + private final DoublePartitionedDataFile indexWriteWordsFile; + private final DoublePartitionedDataFile indexWriteUrlsFile; + private volatile static DictionaryWriter dictionaryWriter; + private final Long dictionaryHashMapSize; + private final SearchIndexPartitioner partitoner; + @Inject + public IndexServicesFactory( + @Named("tmp-file-dir") Path tmpFileDir, + @Named("partition-root-slow") Path partitionRootSlow, + @Named("partition-root-slow-tmp") Path partitionRootSlowTmp, + @Named("partition-root-fast") Path partitionRootFast, + @Named("edge-writer-page-index-file") String writerIndexFile, + @Named("edge-writer-dictionary-file") String writerDictionaryFile, + @Named("edge-index-read-words-file") String indexReadWordsFile, + @Named("edge-index-read-urls-file") String indexReadUrlsFile, + @Named("edge-index-write-words-file") String indexWriteWordsFile, + @Named("edge-index-write-urls-file") String indexWriteUrlsFile, + @Named("edge-dictionary-hash-map-size") Long dictionaryHashMapSize, + EdgeDomainBlacklist domainBlacklist, + SearchIndexPartitioner partitoner + ) { + + this.tmpFileDir = tmpFileDir; + this.dictionaryHashMapSize = dictionaryHashMapSize; + this.domainBlacklist = domainBlacklist; + + this.writerIndexFile = new PartitionedDataFile(partitionRootSlow, writerIndexFile); + this.writerDictionaryFile = new RootDataFile(partitionRootSlow, writerDictionaryFile); + this.indexReadWordsFile = new DoublePartitionedDataFile(partitionRootFast, indexReadWordsFile); + this.indexReadUrlsFile = new DoublePartitionedDataFile(partitionRootFast, indexReadUrlsFile); + this.indexWriteWordsFile = new DoublePartitionedDataFile(partitionRootFast, indexWriteWordsFile); + this.indexWriteUrlsFile = new DoublePartitionedDataFile(partitionRootFast, indexWriteUrlsFile); + this.preconverterOutputFile = new PartitionedDataFile(partitionRootSlowTmp, "preconverted.dat"); + this.partitoner = partitoner; + } + + public SearchIndexWriterImpl getIndexWriter(int idx) { + return new SearchIndexWriterImpl(getDictionaryWriter(), writerIndexFile.get(idx)); + } + + public DictionaryWriter getDictionaryWriter() { + if (dictionaryWriter == null) { + dictionaryWriter = new DictionaryWriter(writerDictionaryFile.get(), dictionaryHashMapSize, true); + } + return dictionaryWriter; + } + + @SneakyThrows + public DictionaryReader getDictionaryReader() { + return new DictionaryReader(getDictionaryWriter()); + + } + @SneakyThrows + public SearchIndexConverter getIndexConverter(int id, IndexBlock block) { + return new SearchIndexConverter(block, id, tmpFileDir, + preconverterOutputFile.get(id), + indexWriteWordsFile.get(id, block.id), + indexWriteUrlsFile.get(id, block.id), + partitoner, + domainBlacklist + ); + } + @SneakyThrows + public SearchIndexPreconverter getIndexPreconverter() { + File[] outputFiles = new File[DYNAMIC_BUCKET_LENGTH+1]; + for (int i = 0; i < outputFiles.length; i++) { + outputFiles[i] = getPreconverterOutputFile(i); + } + return new SearchIndexPreconverter(writerIndexFile.get(0), + outputFiles, + partitoner, + domainBlacklist + ); + } + + private File getPreconverterOutputFile(int i) { + return preconverterOutputFile.get(i); + } + + public long wordCount(int id) { + return SearchIndexConverter.wordCount(writerIndexFile.get(0)); + } + + @SneakyThrows + public SearchIndexReader getIndexReader(int id) { + EnumMap indexMap = new EnumMap<>(IndexBlock.class); + for (IndexBlock block : IndexBlock.values()) { + try { + indexMap.put(block, createSearchIndex(id, block)); + } + catch (Exception ex) { + logger.error("Could not create index {}-{}", id, block); + } + } + return new SearchIndexReader(indexMap); + } + + private SearchIndex createSearchIndex(int bucketId, IndexBlock block) { + try { + return new SearchIndex("IndexReader"+bucketId+":"+ block.name(), + indexReadUrlsFile.get(bucketId, block.id), + indexReadWordsFile.get(bucketId, block.id)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public Callable switchFilesJob(int id) { + return () -> { + for (int block = 0; block < IndexBlock.values().length; block++) { + Files.move( + indexWriteWordsFile.get(id, block).toPath(), + indexReadWordsFile.get(id, block).toPath(), + StandardCopyOption.REPLACE_EXISTING); + Files.move( + indexWriteUrlsFile.get(id, block).toPath(), + indexReadUrlsFile.get(id, block).toPath(), + StandardCopyOption.REPLACE_EXISTING); + } + return true; + }; + } + + public EdgeIndexBucket createIndexBucket(int id) { + return new EdgeIndexBucket(this, new EdgeIndexControl(this), id); + } +} + +class RootDataFile { + private final Path partition; + private final String pattern; + + RootDataFile(Path partition, String pattern) { + this.partition = partition; + this.pattern = pattern; + } + + public File get() { + return partition.resolve(pattern).toFile(); + } +} + + +class PartitionedDataFile { + private final Path partition; + private final String pattern; + + PartitionedDataFile(Path partition, String pattern) { + this.partition = partition; + this.pattern = pattern; + } + + public File get(int id) { + Path partitionDir = partition.resolve(Integer.toString(id)); + if (!partitionDir.toFile().exists()) { + partitionDir.toFile().mkdir(); + } + return partitionDir.resolve(pattern).toFile(); + } +} + +class DoublePartitionedDataFile { + private final Path partition; + private final String pattern; + + DoublePartitionedDataFile(Path partition, String pattern) { + this.partition = partition; + this.pattern = pattern; + } + + public File get(int id, int id2) { + Path partitionDir = partition.resolve(Integer.toString(id)); + + if (!partitionDir.toFile().exists()) { + partitionDir.toFile().mkdir(); + } + partitionDir = partitionDir.resolve(Integer.toString(id2)); + if (!partitionDir.toFile().exists()) { + partitionDir.toFile().mkdir(); + } + + return partitionDir.resolve(pattern).toFile(); + } + +} \ No newline at end of file diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/client/EdgeIndexClient.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/client/EdgeIndexClient.java new file mode 100644 index 00000000..6f64ceae --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/client/EdgeIndexClient.java @@ -0,0 +1,69 @@ +package nu.marginalia.wmsa.edge.index.client; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.inject.Singleton; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import nu.marginalia.wmsa.client.AbstractDynamicClient; +import nu.marginalia.wmsa.client.HttpStatusCode; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.edge.index.model.EdgePutWordsRequest; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageWordSet; +import nu.marginalia.wmsa.edge.model.search.EdgeSearchResultSet; +import nu.marginalia.wmsa.edge.model.search.EdgeSearchSpecification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.CheckReturnValue; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Singleton +public class EdgeIndexClient extends AbstractDynamicClient { + private final Gson gson = new GsonBuilder() + .create(); + private final Logger logger = LoggerFactory.getLogger(getClass()); + + public EdgeIndexClient() { + super(ServiceDescriptor.EDGE_INDEX); + setTimeout(30); + } + + @CheckReturnValue + public Observable putWords(Context ctx, EdgeId domain, EdgeId url, double quality, + EdgePageWordSet wordSet, int writer + ) + { + EdgePutWordsRequest request = new EdgePutWordsRequest(domain, url, quality, wordSet, writer); + + return this.post(ctx, "/words/", request); + } + + + @CheckReturnValue + public EdgeSearchResultSet query(Context ctx, EdgeSearchSpecification specs) { + return this.postGet(ctx, "/search/", specs, EdgeSearchResultSet.class).blockingFirst(); + } + + @CheckReturnValue + public List multiQuery(Context ctx, EdgeSearchSpecification... specs) { + + return Observable.fromArray(specs) + .concatMap(s -> postGet(ctx, "/search/", s, EdgeSearchResultSet.class) + .subscribeOn(Schedulers.io()) + .timeout(1, TimeUnit.SECONDS) + .onErrorComplete()) + .toList() + .blockingGet(); + } + + + @CheckReturnValue + public Observable isBlocked(Context ctx) { + return super.get(ctx, "/is-blocked", Boolean.class); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/model/EdgeIndexSearchTerms.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/model/EdgeIndexSearchTerms.java new file mode 100644 index 00000000..6d4119e1 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/model/EdgeIndexSearchTerms.java @@ -0,0 +1,12 @@ +package nu.marginalia.wmsa.edge.index.model; + +import lombok.AllArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@AllArgsConstructor +public class EdgeIndexSearchTerms { + public List includes = new ArrayList<>(); + public List excludes = new ArrayList<>(); +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/model/EdgePutWordsRequest.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/model/EdgePutWordsRequest.java new file mode 100644 index 00000000..dc541c5b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/model/EdgePutWordsRequest.java @@ -0,0 +1,20 @@ +package nu.marginalia.wmsa.edge.index.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeId; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageWordSet; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +@AllArgsConstructor @Getter +@ToString +public class EdgePutWordsRequest { + public final EdgeId domainId; + public final EdgeId urlId; + public final double quality; + + public final EdgePageWordSet wordSet; + private int index = 0; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/model/IndexBlock.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/model/IndexBlock.java new file mode 100644 index 00000000..a347d2e4 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/model/IndexBlock.java @@ -0,0 +1,34 @@ +package nu.marginalia.wmsa.edge.index.model; + +public enum IndexBlock { + TitleKeywords(0, 0), + Title(1, 1), + Link(2, 1.25), + Top(3, 2), + Middle(4, 3), + Low(5, 4), + Words(6, 6), + Meta(7, 7), + PositionWords(8, 4.5), + NamesWords(9, 5), + TermFreq(10, 10), + Topic(11, 0.5); + + public final int id; + public final double sortOrder; + + IndexBlock(int id, double sortOrder) { + this.sortOrder = sortOrder; + this.id = id; + + } + + public static IndexBlock byId(int id) { + for (IndexBlock block : values()) { + if (id == block.id) { + return block; + } + } + throw new IllegalArgumentException("Bad block id"); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/radix/EdgeIndexBucket.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/radix/EdgeIndexBucket.java new file mode 100644 index 00000000..a257e5f3 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/radix/EdgeIndexBucket.java @@ -0,0 +1,155 @@ +package nu.marginalia.wmsa.edge.index.radix; + +import nu.marginalia.wmsa.edge.index.EdgeIndexControl; +import nu.marginalia.wmsa.edge.index.IndexServicesFactory; +import nu.marginalia.wmsa.edge.index.model.EdgeIndexSearchTerms; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.index.service.index.SearchIndexReader; +import nu.marginalia.wmsa.edge.index.service.index.SearchIndexWriter; +import nu.marginalia.wmsa.edge.index.service.query.IndexSearchBudget; +import nu.marginalia.wmsa.edge.index.service.query.Query; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeId; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.LongPredicate; +import java.util.stream.Collectors; +import java.util.stream.LongStream; + +public class EdgeIndexBucket { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private volatile SearchIndexReader indexReader; + + private final ReadWriteLock indexReplacementLock = new ReentrantReadWriteLock(); + + @NotNull + private final IndexServicesFactory servicesFactory; + private final EdgeIndexControl indexControl; + private final SearchIndexWriter writer; + + private final int id; + + public EdgeIndexBucket(@NotNull IndexServicesFactory servicesFactory, EdgeIndexControl indexControl, int id) { + this.servicesFactory = servicesFactory; + this.indexControl = indexControl; + this.id = id; + + writer = servicesFactory.getIndexWriter(0); + } + + public void init() { + Lock lock = indexReplacementLock.writeLock(); + try { + lock.lock(); + logger.info("Initializing bucket {}", id); + + if (indexReader == null) { + indexReader = servicesFactory.getIndexReader(id); + } + + } + catch (Exception ex) { + logger.error("Uncaught exception", ex); + } + finally { + lock.unlock(); + } + } + + public void preconvert() { + + writer.forceWrite(); + writer.flushWords(); + + servicesFactory.getIndexPreconverter(); + + System.runFinalization(); + System.gc(); + + } + public void switchIndex() { + + indexControl.regenerateIndex(id); + + Lock lock = indexReplacementLock.writeLock(); + try { + lock.lock(); + + indexControl.switchIndexFiles(id); + + if (indexReader != null) { + indexReader.close(); + } + + indexReader = servicesFactory.getIndexReader(id); + + } + catch (Exception ex) { + logger.error("Uncaught exception", ex); + } + finally { + lock.unlock(); + } + } + + + public boolean isAvailable() { + return indexReader != null; + } + + public LongStream getQuery(IndexBlock block, LongPredicate filter, IndexSearchBudget budget, EdgeIndexSearchTerms searchTerms) { + if (null == indexReader) { + logger.warn("Index reader not neady {}", block); + return LongStream.empty(); + } + + var orderedIncludes = searchTerms.includes + .stream() + .sorted(Comparator.comparingLong(i -> indexReader.numHits(block, i))) + .distinct() + .mapToInt(Integer::intValue) + .toArray(); + + + if (logger.isDebugEnabled()) { + logger.debug("Includes: ({}); excludes: ({})", Arrays. + stream(orderedIncludes) + .mapToObj(String::valueOf) + .collect(Collectors.joining(",")), + searchTerms.excludes.stream().map(String::valueOf).collect(Collectors.joining(","))); + } + Query query; + if (orderedIncludes.length == 1) { + query = indexReader.findUnderspecified(block, budget, filter, orderedIncludes[0]); + } + else { + query = indexReader.findWord(block, budget, filter, orderedIncludes[0]); + } + + for (int i = 1; i < orderedIncludes.length; i++) { + query = query.also(orderedIncludes[i]); + } + for (int term : searchTerms.excludes) { + query = query.not(term); + } + return query.stream(); + } + + + public IndexBlock getTermScore(int termId, long urlId) { + return indexReader.getBlockForResult(termId, urlId); + } + + public boolean isTermInBucket(IndexBlock block, int termId, long urlId) { + return indexReader.isTermInBucket(block, termId, urlId); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/SearchEngineRanking.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/SearchEngineRanking.java new file mode 100644 index 00000000..abaced82 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/SearchEngineRanking.java @@ -0,0 +1,56 @@ +package nu.marginalia.wmsa.edge.index.service; + +import gnu.trove.list.TIntList; +import gnu.trove.map.hash.TIntIntHashMap; +import gnu.trove.set.hash.TIntHashSet; + +import static nu.marginalia.wmsa.edge.index.EdgeIndexService.DYNAMIC_BUCKET_LENGTH; + +public class SearchEngineRanking { + + private final TIntIntHashMap domainToId + = new TIntIntHashMap(1_000_000, 0.5f, -1, Integer.MAX_VALUE); + + private final TIntHashSet[] domainToBucket = new TIntHashSet[DYNAMIC_BUCKET_LENGTH+1]; + + private final int offset; + private final double[] limits; + + public SearchEngineRanking(int offset, TIntList domains, double... limits) { + this.offset = offset; + this.limits = limits; + + for (int i = offset; i < offset+limits.length; i++) { + domainToBucket[i] = new TIntHashSet(100, 0.5f, DYNAMIC_BUCKET_LENGTH); + } + + for (int i = 0; i < domains.size(); i++) { + double relPortion = i / (double) domains.size(); + + for (int limit = 0; limit < limits.length; limit++) { + if (relPortion < limits[limit]) { + domainToBucket[limit+offset].add(domains.get(i)); + break; + } + } + + domainToId.put(domains.get(i), i); + } + } + + public boolean ownsBucket(int bucketId) { + return bucketId >= offset && bucketId < offset + limits.length; + } + + public boolean hasBucket(int bucket, int domain) { + var set = domainToBucket[bucket]; + if (set == null) { + return false; + } + return set.contains(domain); + } + + public int translateId(int id) { + return domainToId.get(id); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/SearchIndexDao.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/SearchIndexDao.java new file mode 100644 index 00000000..0ecf8f42 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/SearchIndexDao.java @@ -0,0 +1,113 @@ +package nu.marginalia.wmsa.edge.index.service; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.zaxxer.hikari.HikariDataSource; +import gnu.trove.list.TIntList; +import gnu.trove.list.array.TIntArrayList; +import gnu.trove.set.hash.TIntHashSet; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.edge.index.service.util.ranking.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public class SearchIndexDao { + private final HikariDataSource dataSource; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Inject + public SearchIndexDao(HikariDataSource dataSource) + { + this.dataSource = dataSource; + } + + @SneakyThrows + public TIntHashSet getSpamDomains() { + final TIntHashSet result = new TIntHashSet(1_000_000); + + try (var connection = dataSource.getConnection()) { + try (var stmt = connection.prepareStatement("SELECT EC_DOMAIN.ID FROM EC_DOMAIN INNER JOIN EC_TOP_DOMAIN ON EC_DOMAIN.URL_TOP_DOMAIN_ID = EC_TOP_DOMAIN.ID INNER JOIN EC_DOMAIN_BLACKLIST ON EC_DOMAIN_BLACKLIST.URL_DOMAIN = EC_TOP_DOMAIN.URL_PART")) { + var rsp = stmt.executeQuery(); + while (rsp.next()) { + result.add(rsp.getInt(1)); + } + } + } + + return result; + } + + @SneakyThrows + public TIntHashSet goodUrls() { + TIntHashSet domains = new TIntHashSet(10_000_000, 0.5f, -1); + TIntHashSet urls = new TIntHashSet(100_000_000, 0.5f, -1); + + try (var connection = dataSource.getConnection()) { + try (var stmt = connection.prepareStatement("SELECT ID FROM EC_DOMAIN WHERE DOMAIN_ALIAS IS NULL AND STATE>=0")) { + stmt.setFetchSize(10_000); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + domains.add(rsp.getInt(1)); + } + } + + // For some reason, doing this "INNER JOIN" in Java is significantly faster than doing it in SQL + + try (var stmt = connection.prepareStatement("SELECT ID,DOMAIN_ID FROM EC_URL WHERE VISITED AND EC_URL.STATE='OK'")) { + stmt.setFetchSize(10_000); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + if (domains.contains(rsp.getInt(2))) { + urls.add(rsp.getInt(1)); + } + } + } + + } + + return urls; + } + + @SneakyThrows + public TIntList getDomainsByRealPageRank() { + var spr = new BetterStandardPageRank(dataSource,"www.rep.routledge.com", "www.personal.kent.edu", "xroads.virginia.edu", "classics.mit.edu", "faculty.washington.edu", "monadnock.net", "memex.marginalia.nu", "wiki.xxiivv.com", "bikobatanari.art", "sadgrl.online", "lileks.com"); + return spr.pageRankWithPeripheralNodes(spr.size()/2, false); + } + + @SneakyThrows + public TIntList getSmallWebDomains() { + var rpr = new BetterReversePageRank(new DatabaseModule().provideConnection(), "bikobatanari.art", "sadgrl.online", "wiki.xxiivv.com", "%neocities.org"); + + rpr.setMaxKnownUrls(750); + + return rpr.pageRankWithPeripheralNodes(rpr.size(), false); + } + + @SneakyThrows + public TIntList getAcademiaDomains() { + var spr = new BetterStandardPageRank(new DatabaseModule().provideConnection(), "%edu"); + return spr.pageRankWithPeripheralNodes(spr.size()/2, false); + } + + @SneakyThrows + public TIntList getDomainsByStandardPageRank() { + var spr = new BuggyStandardPageRank(dataSource,"memex.marginalia.nu"); + return spr.pageRankWithPeripheralNodes(spr.size()/2, false); + } + + @SneakyThrows + public TIntList getSpecialDomains() { + TIntArrayList results = new TIntArrayList(); + try (var connection = dataSource.getConnection(); + var stmt = connection.prepareStatement("SELECT ID FROM EC_DOMAIN WHERE STATE=2") + ) { + var rs = stmt.executeQuery(); + while (rs.next()) { + results.add(rs.getInt(1)); + } + } + return results; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/SearchIndexes.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/SearchIndexes.java new file mode 100644 index 00000000..91065101 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/SearchIndexes.java @@ -0,0 +1,153 @@ +package nu.marginalia.wmsa.edge.index.service; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.edge.index.IndexServicesFactory; +import nu.marginalia.wmsa.edge.index.radix.EdgeIndexBucket; +import nu.marginalia.wmsa.edge.index.service.dictionary.DictionaryReader; +import nu.marginalia.wmsa.edge.index.service.index.SearchIndexWriterImpl; +import nu.marginalia.wmsa.edge.index.service.query.SearchIndexPartitioner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.util.concurrent.locks.ReentrantLock; + +import static nu.marginalia.wmsa.edge.index.EdgeIndexService.DYNAMIC_BUCKET_LENGTH; + +@Singleton +public class SearchIndexes { + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final EdgeIndexBucket[] buckets + = new EdgeIndexBucket[DYNAMIC_BUCKET_LENGTH + 1]; + private final IndexServicesFactory servicesFactory; + private final SearchIndexPartitioner partitioner; + + private final ReentrantLock opsLock = new ReentrantLock(false); + + private final SearchIndexWriterImpl primaryIndexWriter; + private final SearchIndexWriterImpl secondaryIndexWriter; + private DictionaryReader dictionaryReader = null; + + @Inject + public SearchIndexes(IndexServicesFactory servicesFactory, SearchIndexPartitioner partitioner) { + this.servicesFactory = servicesFactory; + this.partitioner = partitioner; + + this.primaryIndexWriter = servicesFactory.getIndexWriter(0); + this.secondaryIndexWriter = servicesFactory.getIndexWriter(1); + + for (int i = 0; i < buckets.length; i++) { + buckets[i] = servicesFactory.createIndexBucket(i); + } + } + + public boolean repartition() { + + if (!opsLock.tryLock()) { + return false; + } + try { + partitioner.reloadPartitions(); + } + finally { + opsLock.unlock(); + } + + return true; + } + + public boolean preconvert() { + + if (!opsLock.tryLock()) { + return false; + } + try { + buckets[0].preconvert(); + } + finally { + opsLock.unlock(); + } + + return true; + } + + public boolean reindex(int id) { + + if (!opsLock.tryLock()) { + return false; + } + try { + buckets[id].switchIndex(); + } + finally { + opsLock.unlock(); + } + + return true; + } + + public boolean reindexAll() { + if (!opsLock.tryLock()) { + return false; + } + try { + for (var bucket : buckets) { + bucket.switchIndex(); + } + } finally { + opsLock.unlock(); + } + + return true; + } + + @Nullable + public DictionaryReader getDictionaryReader() { + return dictionaryReader; + } + + + public boolean isBusy() { + return partitioner.isBusy(); + } + + public void initialize(Initialization init) { + + logger.info("Waiting for init"); + init.waitReady(); + + opsLock.lock(); + try { + logger.info("Initializing buckets"); + for (EdgeIndexBucket bucket : buckets) { + bucket.init(); + } + + logger.info("Initializing dictionary reader"); + dictionaryReader = servicesFactory.getDictionaryReader(); + } + finally { + opsLock.unlock(); + } + } + + public SearchIndexWriterImpl getIndexWriter(int idx) { + if (idx == 0) { + return primaryIndexWriter; + } + else { + return secondaryIndexWriter; + } + } + + public EdgeIndexBucket getBucket(int bucketId) { + return buckets[bucketId]; + } + public boolean isValidBucket(int bucketId) { + return bucketId >= 0 && bucketId < buckets.length; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/SearchOrder.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/SearchOrder.java new file mode 100644 index 00000000..d1c9f10a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/SearchOrder.java @@ -0,0 +1,6 @@ +package nu.marginalia.wmsa.edge.index.service; + +public enum SearchOrder { + ASCENDING, + REVERSED +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/dictionary/DictionaryReader.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/dictionary/DictionaryReader.java new file mode 100644 index 00000000..90d270d2 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/dictionary/DictionaryReader.java @@ -0,0 +1,27 @@ +package nu.marginalia.wmsa.edge.index.service.dictionary; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import lombok.SneakyThrows; + +import java.util.concurrent.TimeUnit; + +@Singleton +public class DictionaryReader { + private final DictionaryWriter writer; + + private final Cache cache = CacheBuilder.newBuilder().maximumSize(10_000).expireAfterAccess(60, TimeUnit.SECONDS).build(); + + @SneakyThrows @Inject + public DictionaryReader(DictionaryWriter writer) { + this.writer = writer; + } + + @SneakyThrows + public int get(String word) { + return cache.get(word, () -> writer.getReadOnly(word)); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/dictionary/DictionaryWriter.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/dictionary/DictionaryWriter.java new file mode 100644 index 00000000..c943e50b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/dictionary/DictionaryWriter.java @@ -0,0 +1,376 @@ +package nu.marginalia.wmsa.edge.index.service.dictionary; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import io.prometheus.client.Gauge; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.crawler.domain.language.WordPatterns; +import nu.marginalia.util.dict.DictionaryHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.nio.ByteBuffer; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +@Singleton +public class DictionaryWriter implements AutoCloseable { + private final ArrayList commitQueue = new ArrayList<>(10_000); + + private final DictionaryHashMap reverseIndex; + private boolean prepopulate; + + private final ReadWriteLock memoryLock = new ReentrantReadWriteLock(); + private final ReadWriteLock diskLock = new ReentrantReadWriteLock(); + private final RandomAccessFile raf; + + private final Map stats = new HashMap<>(); + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + static volatile AtomicInteger instances = new AtomicInteger(); + + private final TokenCompressor readOnlyTokenCompressor = new TokenCompressor(this::getReadOnly); + private final TokenCompressor tokenCompressor = new TokenCompressor(this::get); + + private static final Gauge request_time_metrics + = Gauge.build("wmsa_edge_index_dictionary_size", "Dictionary Size") + .register(); + + private volatile boolean running = true; + private final Thread commitToDiskThread; + @SneakyThrows + public long getPos() { + return raf.getFilePointer(); + } + public void printStats() { + stats + .entrySet() + .stream() + .filter(e -> e.getValue() > 10) + .sorted(Map.Entry.comparingByValue()) + .forEach(e -> System.out.println(e.getKey() + " " + e.getValue())); + } + @SneakyThrows @Inject + public DictionaryWriter( + @Named("edge-writer-dictionary-file") File dictionaryFile, + @Named("edge-dictionary-hash-map-size") Long hashMapSize, + boolean prepopulate) { + logger.info("Creating dictionary writer"); + raf = new RandomAccessFile(dictionaryFile, "rw"); + reverseIndex = new DictionaryHashMap(hashMapSize); + this.prepopulate = prepopulate; + + Lock writeLock = diskLock.writeLock(); + try { + writeLock.lock(); + loadFile(dictionaryFile); + } + finally { + writeLock.unlock(); + } + + commitToDiskThread = new Thread(this::commitToDiskRunner, "CommitToDiskThread"); + commitToDiskThread.start(); + + Runtime.getRuntime().addShutdownHook(new Thread(this::commitToDisk)); + + if (!instances.compareAndSet(0, 1)) { + logger.error("MULTIPLE WRITER INSTANCES!"); + } + logger.info("Done creating dictionary writer"); + } + + + public void commitToDiskRunner() { + while (running) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + commitToDisk(); + } + } + + public void prepare() { + if (!prepopulate) + return; + + try (var resource = Objects.requireNonNull(ClassLoader.getSystemResourceAsStream("dictionary/word-frequency"), + "Could not load word frequency table"); + var br = new BufferedReader(new InputStreamReader(resource)) + ) { + for (;;) { + var line = br.readLine(); + if (line == null) { + break; + } + if (WordPatterns.wordPredicateEither.test(line)) { + get(line); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + + } + @SneakyThrows + private void loadFile(File dictionaryFile) { + if (!dictionaryFile.exists()) { + logger.info("File {} does not exist, can't load", dictionaryFile); + return; + } + + logger.info("Reading {}", dictionaryFile); + + long pos; + if (raf.length() < 8) { + pos = 8; + raf.writeLong(pos); + } + else { + pos = raf.readLong(); + } + + logger.info("Length {} ({})", pos, raf.length()); + if (pos == 8) { + logger.info("Empty DB, prepopulating"); + prepare(); + } + + ByteBuffer buffer = ByteBuffer.allocateDirect(8192); + + var channel = raf.getChannel(); + + long cp = channel.position(); + int debugNext = 0; + try { + buffer.limit(0); + long loaded = 0; + + while (cp < pos || buffer.hasRemaining()) { + if (buffer.limit() - buffer.position() < 4) { + buffer.compact(); + + long rb = channel.read(buffer); + if (rb <= 0) { + break; + } + cp += rb; + + buffer.flip(); + } + + int len = buffer.get(); + if (debugNext > 0) { + logger.warn("NextLen: {} ({})", len, (char) len); + } + while (buffer.limit() - buffer.position() < len) { + buffer.compact(); + int rb = channel.read(buffer); + if (rb <= 0) break; + cp += rb; + buffer.flip(); + } + + if (buffer.limit() < len) { + + logger.warn("Partial write at end-of-file!"); + + if (cp >= pos) { + logger.info("... but it's ok"); + } + break; + } + + boolean negativeLen = false; + if (len < 0) { + len = (len&0xFF); + negativeLen = true; + + } + + byte[] data = new byte[len]; + buffer.get(data); + if ((++loaded % 10_000_000) == 0L) { + logger.info("Loaded {} million items", loaded/1_000_000); + } + + if (debugNext > 0) { + logger.warn("Next word {}", new String(data)); + if (--debugNext == 0) { + logger.info(" "); + } + } + if (negativeLen) { + logger.warn("Negative length of word {} {}@{}", len, new String(data), reverseIndex.size()); + debugNext = 10; + } + +// if (reverseIndex.get(data) != DictionaryHashMap.NO_VALUE) { +// logger.error("Duplicate insert"); +// } + reverseIndex.put(data, reverseIndex.size()); + } + } + catch (Exception ex) { + logger.error("IO Exception", ex); + } + + raf.seek(pos); + request_time_metrics.set(reverseIndex.size()); + + logger.info("Initial loading done, dictionary size {}", reverseIndex.size()); + } + + private final ByteBuffer commitBuffer = ByteBuffer.allocateDirect(4096); + public volatile boolean noCommit = false; + @SneakyThrows + public void commitToDisk() { + if (noCommit) return; + + if (!raf.getChannel().isOpen()) { + logger.error("commitToDisk() with closed channel! Cannot commit!"); + return; + } + + Lock memLock = memoryLock.readLock(); + List data; + try { + memLock.lock(); + if (commitQueue.isEmpty()) + return; + data = new ArrayList<>(commitQueue); + commitQueue.clear(); + } + finally { + memLock.unlock(); + } + + var channel = raf.getChannel(); + commitBuffer.clear(); + + Lock writeLock = diskLock.writeLock(); + // Only acquire memory lock if there's a risk of backpressure + if (data.size() < 1000) { + memLock = null; + } + + try { + if (memLock != null) memLock.lock(); + writeLock.lock(); + + long start = System.currentTimeMillis(); + int ct = data.size(); + + for (byte[] item : data) { + commitBuffer.clear(); + commitBuffer.put((byte) item.length); + commitBuffer.put(item); + commitBuffer.flip(); + + while (commitBuffer.position() < commitBuffer.limit()) + channel.write(commitBuffer, channel.size()); + } + + long pos = channel.size(); + commitBuffer.clear(); + commitBuffer.putLong(pos); + commitBuffer.flip(); + channel.write(commitBuffer, 0); + + channel.force(false); + + logger.debug("Comitted {} items in {} ms", ct, System.currentTimeMillis() - start); + } + catch (Exception ex) { + logger.error("Error during dictionary commit!!!", ex); + } + finally { + writeLock.unlock(); + if (memLock != null) { + memLock.unlock(); + } + } + } + + public int get(String macroWord) { + byte[] word = tokenCompressor.getWordBytes(macroWord); + + Lock lock = memoryLock.readLock(); + try { + lock.lock(); + int idx = reverseIndex.get(word); + if (idx >= 0) { + return idx; + } + } + finally { + lock.unlock(); + } + + lock = memoryLock.writeLock(); + try { + lock.lock(); + int idx = reverseIndex.get(word); + if (idx >= 0) { + return idx; + } + + if (!noCommit) { + commitQueue.add(word); + } + + idx = reverseIndex.size(); + + reverseIndex.put(word, idx); + + request_time_metrics.set(reverseIndex.size()); + + return idx; + } + finally { + + lock.unlock(); + } + } + + public int getReadOnly(String word) { + var bytes = readOnlyTokenCompressor.getWordBytes(word); + if (bytes.length == 0) { + return DictionaryHashMap.NO_VALUE; + } + return reverseIndex.get(bytes); + } + + public int size() { + Lock lock = memoryLock.readLock(); + try { + lock.lock(); + return reverseIndex.size(); + } + finally { + lock.unlock(); + } + } + + @Override + public void close() throws Exception { + logger.warn("Closing DictionaryWriter"); + + running = false; + commitToDiskThread.join(); + commitToDisk(); + + raf.close(); + } + +} + diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/dictionary/TokenCompressor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/dictionary/TokenCompressor.java new file mode 100644 index 00000000..9f26fffd --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/dictionary/TokenCompressor.java @@ -0,0 +1,83 @@ +package nu.marginalia.wmsa.edge.index.service.dictionary; + +import nu.marginalia.util.ByteFolder; +import nu.marginalia.util.dict.DictionaryHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.function.Predicate; +import java.util.function.ToIntFunction; +import java.util.regex.Pattern; + +public class TokenCompressor { + private final ToIntFunction mapper; + private final ByteFolder folder = new ByteFolder(); + public static final byte[] EMPTY = new byte[0]; + + private static final Logger logger = LoggerFactory.getLogger(TokenCompressor.class); + + private static final Predicate intPatternMatcher = Pattern.compile("[1-9][0-9]{1,8}").asMatchPredicate(); + + + public TokenCompressor(ToIntFunction mapper) { + this.mapper = mapper; + } + final char[] separators = new char[] { '_', '-', '.', '/' }; + public synchronized byte[] getWordBytes(String macroWord) { + int ui = -1; + + for (char c : separators) { + int ui2 = macroWord.indexOf(c); + if (ui < 0) ui = ui2; + else if (ui2 >= 0) ui = Math.min(ui, ui2); + } + + if (ui <= 0 || ui >= macroWord.length()-1) { + return getByteRepresentation(macroWord); + } + + String car = macroWord.substring(0, ui); + String cdr = macroWord.substring(ui+1); + + int carId = mapper.applyAsInt(car); + int cdrId = mapper.applyAsInt(cdr); + + if (carId == DictionaryHashMap.NO_VALUE || cdrId == DictionaryHashMap.NO_VALUE) { + return EMPTY; + } + + return folder.foldBytes(carId, cdrId); + } + + private byte[] getByteRepresentation(String word) { + if (intPatternMatcher.test(word)) { + long val = Long.parseLong(word); + if (val < 0x100) { + return new byte[] { 'A', (byte) (val & 0xFF)}; + } + else if (val < 0x10000) { + return new byte[] { 'B', (byte)((val & 0xFF00)>>8), (byte) (val & 0xFF)}; + } + else if (val < 0x1000000) { + return new byte[] { 'C', (byte)((val & 0xFF0000)>>12), (byte)((val & 0xFF00)>>8), (byte) (val & 0xFF)}; + } + else if (val < 0x100000000L) { + return new byte[] { 'D', (byte)((val & 0xFF0000)>>16), (byte)((val & 0xFF0000)>>12), (byte)((val & 0xFF00)>>8), (byte) (val & 0xFF)}; + } + } + + var bytes = word.getBytes(); + for (int i = 0; i < bytes.length; i++) { + if (bytes[i] < 32 && (bytes[i] & 0x80) == 0) { + logger.error("Bad byte in {} -> {} ({})", word, bytes[i], (char) bytes[i]); + bytes[i] = '?'; + } + } + if (bytes.length >= Byte.MAX_VALUE) { + return Arrays.copyOf(bytes, Byte.MAX_VALUE); + } + return bytes; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndex.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndex.java new file mode 100644 index 00000000..c25100f4 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndex.java @@ -0,0 +1,115 @@ +package nu.marginalia.wmsa.edge.index.service.index; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import com.upserve.uppend.blobs.NativeIO; +import nu.marginalia.wmsa.edge.index.service.index.wordstable.IndexWordsTable; +import nu.marginalia.util.btree.BTreeReader; +import nu.marginalia.util.multimap.MultimapFileLong; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.stream.LongStream; + +public class SearchIndex implements AutoCloseable { + + private final MultimapFileLong urls; + private final IndexWordsTable words; + private final RandomAccessFile wordsFile; + private final BTreeReader bTreeReader; + private final Logger logger; + + @Inject + public SearchIndex( + String name, + @Named("edge-index-read-urls-file") File inUrls, + @Named("edge-index-read-words-file") File inWords) + throws IOException { + + logger = LoggerFactory.getLogger(name); + wordsFile = new RandomAccessFile(inWords, "r"); + + logger.info("{} : Loading {}", name, inUrls); + logger.info("{} : Loading {}", name, inWords); + + urls = MultimapFileLong.forReading(inUrls.toPath()); + words = IndexWordsTable.ofFile(wordsFile); + + bTreeReader = new BTreeReader(urls, SearchIndexConverter.urlsBTreeContext); + + madvise(urls, bTreeReader); + } + + private void madvise(MultimapFileLong urls, BTreeReader reader) { + + urls.advice(NativeIO.Advice.Sequential); + words.forEachWordsOffset(offset -> { + var h = reader.getHeader(offset); + int length = (int) (h.dataOffsetLongs() - h.indexOffsetLongs()); + + if (length > 0) { + urls.adviceRange(NativeIO.Advice.WillNeed, h.indexOffsetLongs(), length); + urls.pokeRange(h.indexOffsetLongs(), length); + } + }); + } + + + public long numUrls(int wordId) { + int length = words.wordLength(wordId); + if (length < 0) return 0; + if (length > 0) return length; + + var range = rangeForWord(wordId); + if (range.isPresent()) { + return bTreeReader.getHeader(range.dataOffset).numEntries(); + } + return 0; + } + + public UrlIndexTree rangeForWord(int wordId) { + return new UrlIndexTree(words.positionForWord(wordId)); + } + + public boolean hasUrl(long url, UrlIndexTree range) { + if (!range.isPresent()) + return false; + + return bTreeReader.offsetForEntry(bTreeReader.getHeader(range.dataOffset), url) >= 0; + } + + public class UrlIndexTree { + final long dataOffset; + + public UrlIndexTree(long dataOffset) { + this.dataOffset = dataOffset; + } + + public LongStream stream() { + if (dataOffset < 0) { + return LongStream.empty(); + } + var header = bTreeReader.getHeader(dataOffset); + + long urlOffset = header.dataOffsetLongs(); + return LongStream.range(urlOffset, urlOffset + header.numEntries()).map(urls::get); + } + + public boolean isPresent() { + return dataOffset >= 0; + } + } + + + + @Override + public void close() throws Exception { + urls.close(); + words.close(); + + wordsFile.close(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexConverter.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexConverter.java new file mode 100644 index 00000000..95a47a69 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexConverter.java @@ -0,0 +1,383 @@ +package nu.marginalia.wmsa.edge.index.service.index; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import gnu.trove.set.hash.TIntHashSet; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDomainBlacklist; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.index.service.index.wordstable.WordsTableWriter; +import nu.marginalia.wmsa.edge.index.service.query.SearchIndexPartitioner; +import nu.marginalia.util.btree.BTreeWriter; +import nu.marginalia.util.btree.model.BTreeContext; +import nu.marginalia.util.multimap.MultimapFileLong; +import nu.marginalia.util.RandomWriteFunnel; +import nu.marginalia.util.multimap.MultimapSorter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.locks.Lock; + +public class SearchIndexConverter { + private static final long FILE_HEADER_SIZE = 12; + private static final int CHUNK_HEADER_SIZE = 16; + + public static final BTreeContext urlsBTreeContext = new BTreeContext(5, 1, ~0, 8); + + private final long fileLength; + private final long urlsFileSize; + private final FileChannel urlsTmpFileChannel; + private final int wordCount; + private final MultimapFileLong urlsTmpFileMap; + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final IndexBlock block; + private final int bucketId; + @org.jetbrains.annotations.NotNull + private final File urlsFile; + private final SearchIndexPartitioner partitioner; + private final TIntHashSet spamDomains; + private final MultimapSorter urlTmpFileSorter; + + @SneakyThrows + public static long wordCount(File inputFile) { + try (RandomAccessFile raf = new RandomAccessFile(inputFile, "r")) { + raf.readLong(); + return raf.readInt(); + } + } + + @SneakyThrows + @Inject + public SearchIndexConverter(IndexBlock block, + int bucketId, @Named("tmp-file-dir") Path tmpFileDir, + @Named("edge-writer-page-index-file") File inputFile, + @Named("edge-index-write-words-file") File outputFileWords, + @Named("edge-index-write-urls-file") File outputFileUrls, + SearchIndexPartitioner partitioner, + EdgeDomainBlacklist blacklist) + { + this.block = block; + this.bucketId = bucketId; + urlsFile = outputFileUrls; + this.partitioner = partitioner; + this.spamDomains = blacklist.getSpamDomains(); + logger.info("Converting {} ({}) {}", block.id, block, inputFile); + + Files.deleteIfExists(outputFileWords.toPath()); + Files.deleteIfExists(outputFileUrls.toPath()); + + final RandomAccessFile raf = new RandomAccessFile(inputFile, "r"); + + this.fileLength = raf.readLong(); + this.wordCount = raf.readInt(); + + var inputChannel = raf.getChannel(); + + ByteBuffer buffer = ByteBuffer.allocateDirect(10_000); + + urlsFileSize = getUrlsSize(buffer, raf); + + var tmpUrlsFile = Files.createTempFile(tmpFileDir, "urls-sorted", ".dat"); + + var urlsTmpFileRaf = new RandomAccessFile(tmpUrlsFile.toFile(), "rw"); + urlsTmpFileChannel = new RandomAccessFile(tmpUrlsFile.toFile(), "rw").getChannel(); + urlsTmpFileMap = new MultimapFileLong(urlsTmpFileRaf, FileChannel.MapMode.READ_WRITE, urlsFileSize, 8*1024*1024, false); + urlTmpFileSorter = urlsTmpFileMap.createSorter(tmpFileDir, 1024*1024*256); + + logger.info("Creating word index table {} for block {} ({})", outputFileWords, block.id, block); + long[] wordIndexTable = createWordIndexTable(outputFileWords, inputChannel); + + logger.info("Creating word urls table {} for block {} ({})", outputFileUrls, block.id, block); + createUrlTable(tmpFileDir, buffer, raf, wordIndexTable); + + Files.delete(tmpUrlsFile); + raf.close(); + + urlsTmpFileChannel.close(); + urlsTmpFileMap.force(); + + } + + private boolean isUrlAllowed(long url) { + return !spamDomains.contains((int)(url >>> 32)); + } + + public long translateUrl(long url) { + int domainId = partitioner.translateId(bucketId, (int) (url >>> 32)); + return ((long)domainId << 32) | (url & 0xFFFFFFFFL); + } + + + @RequiredArgsConstructor + private class IndexReader { + private final ByteBuffer buffer; + private final FileChannel channel; + public long filtered; + + public void read() throws IOException { + var lock = partitioner.getReadLock(); + try { + lock.lock(); + outer: + while (channel.position() < fileLength) { + buffer.clear(); + buffer.limit(CHUNK_HEADER_SIZE); + channel.read(buffer); + buffer.flip(); + long urlId = buffer.getLong(); + int chunkBlock = buffer.getInt(); + int count = buffer.getInt(); + + if (count > 1000) { + + int tries = 0; + logger.warn("Terminating garbage @{}b, attempting repair", channel.position()); + + for (; ; ) { + tries++; + long p = channel.position(); + buffer.clear(); + buffer.limit(8); + if (channel.read(buffer) != 8) { + break outer; // EOF...? + } + + buffer.flip(); + int pcb = buffer.getInt(); + int pct = buffer.getInt(); + if (pcb == 0 || pcb == 1 && pct >= 0 && pct <= 1000) { + chunkBlock = pcb; + count = pct; + break; + } else { + channel.position(p + 1); + } + } + logger.warn("Skipped {}b", tries); + } + + buffer.clear(); + buffer.limit(count * 4); + + int trb = 0; + while (trb < count * 4) { + int rb = channel.read(buffer); + if (rb <= 0) { + throw new ArrayIndexOutOfBoundsException(trb + " - " + count * 4 + " " + rb); + } + trb += rb; + } + + buffer.flip(); + + if (isUrlAllowed(urlId)) { + if (block.id == chunkBlock) { + eachUrl(lock, count, urlId); + } + } else { + filtered++; + } + } + } + finally { + lock.unlock(); + } + } + + public void eachUrl(Lock lock, int count, long urlId) throws IOException { + for (int i = 0; i < count; i++) { + int wordId = buffer.getInt(); + if (acceptWord(lock, urlId, wordId, i, block.id)) { + eachWord(urlId, wordId); + } + } + } + public void eachWord(long urlId, int wordId) throws IOException { + + } + } + + private long getUrlsSize(ByteBuffer buffer, RandomAccessFile raf) throws IOException { + raf.seek(FILE_HEADER_SIZE); + + var channel = raf.getChannel(); + + var reader = new IndexReader(buffer, channel) { + public long size; + + @Override + public void eachWord(long urlId, int wordId) { + size++; + } + }; + + reader.read(); + + logger.info("Blacklist filtered {} URLs", reader.filtered); + logger.debug("URLs Size {} Mb", channel.position()/(1024*1024)); + + return reader.size; + } + + private void createUrlTable(Path tmpFileDir, ByteBuffer buffer, RandomAccessFile raf, long[] wordIndexTable) throws IOException { + logger.debug("Table size = {}", wordIndexTable.length); + int[] wordIndex = new int[wordIndexTable.length]; + raf.seek(FILE_HEADER_SIZE); + + var channel = raf.getChannel(); + + try (RandomWriteFunnel rwf = new RandomWriteFunnel(tmpFileDir, urlsFileSize, 10_000_000)) { + var reader = new IndexReader(buffer, channel) { + @Override + public void eachWord(long urlId, int wordId) throws IOException { + if (wordId >= wordIndex.length) + return; + + if (wordId != 0) { + if (!(wordIndexTable[wordId - 1] + wordIndex[wordId] <= wordIndexTable[wordId])) { + logger.error("Crazy state: wordId={}, index={}, lower={}, upper={}", + wordId, + wordIndex[wordId], + wordIndexTable[wordId - 1], + wordIndexTable[wordId]); + throw new IllegalStateException(); + } + } + if (wordId > 0) { + rwf.put(wordIndexTable[wordId - 1] + wordIndex[wordId]++, translateUrl(urlId)); + } else { + rwf.put(wordIndex[wordId]++, translateUrl(urlId)); + } + } + }; + + reader.read(); + + rwf.write(urlsTmpFileChannel); + } + + urlsTmpFileChannel.force(false); + + logger.debug("URL TMP Table: {} Mb", channel.position()/(1024*1024)); + + if (wordIndexTable.length > 0) { + logger.debug("Sorting urls table"); + sortUrls(wordIndexTable); + urlsTmpFileMap.force(); + } + else { + logger.warn("urls table empty -- nothing to sort"); + } + + + long idx = 0; + + var copyBuffer = ByteBuffer.allocateDirect(4096); + try (var urlsFileMap = MultimapFileLong.forOutput(urlsFile.toPath(), 1024)) { + var writer = new BTreeWriter(urlsFileMap, urlsBTreeContext); + + if (wordIndexTable[0] != 0) { + int start = 0; + int end = (int) wordIndexTable[0]; + + idx += writer.write(idx, (int) wordIndexTable[0], + offset -> urlsFileMap.transferFromFileChannel(urlsTmpFileChannel, offset, start, end)); + } + + for (int i = 1; i < wordIndexTable.length; i++) { + if (wordIndexTable[i] != wordIndexTable[i - 1]) { + long start = wordIndexTable[i-1]; + long end = wordIndexTable[i]; + + idx += writer.write(idx, (int) (end-start), + offset -> urlsFileMap.transferFromFileChannel(urlsTmpFileChannel, offset, start, end)); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + + logger.warn("BTrees generated"); + } + + public void transfer(ByteBuffer buffer, MultimapFileLong dest, FileChannel sourceChannel, long destOffset, long sourceStart, long sourceEnd) throws IOException { + int tbw = 0; + + buffer.limit(Math.min(buffer.capacity(), (int)(sourceEnd - sourceStart)*8)); + while (sourceEnd - sourceStart - tbw > buffer.limit()/8) { + int bw = 0; + while (buffer.position() < buffer.limit()) { + int r = sourceChannel.read(buffer, sourceStart*8 + bw); + if (r < 0) { + throw new IOException(""); + } + bw += r; + } + buffer.flip(); + dest.write(buffer.asLongBuffer(), destOffset + tbw); + tbw += bw/8; + buffer.clear(); + buffer.limit(Math.min(buffer.capacity(), (int)(sourceEnd*8 - sourceStart*8 - tbw))); + } + buffer.clear(); + buffer.limit((int)(sourceEnd - (sourceStart + tbw))*8); + int bw = 0; + while (bw < buffer.limit()) { + bw += sourceChannel.read(buffer, sourceStart + bw); + } + buffer.flip(); + dest.write(buffer.asLongBuffer(), destOffset + tbw); + } + + @SneakyThrows + private void sortUrls(long[] wordIndices) { + urlTmpFileSorter.sort( 0, (int) wordIndices[0]); + + for (int i = 1; i < wordIndices.length; i++) { + urlTmpFileSorter.sort(wordIndices[i-1], (int) (wordIndices[i] - wordIndices[i-1])); + } + } + + private long[] createWordIndexTable(File outputFileWords, FileChannel inputChannel) throws Exception { + inputChannel.position(FILE_HEADER_SIZE); + + logger.debug("Table size = {}", wordCount); + WordsTableWriter wordsTableWriter = new WordsTableWriter(wordCount); + ByteBuffer buffer = ByteBuffer.allocateDirect(8*SearchIndexWriterImpl.MAX_BLOCK_SIZE); + + logger.debug("Reading words"); + + var reader = new IndexReader(buffer, inputChannel) { + @Override + public void eachWord(long urlId, int wordId) { + wordsTableWriter.acceptWord(wordId); + } + }; + reader.read(); + + logger.debug("Rearranging table"); + + inputChannel.position(FILE_HEADER_SIZE); + + wordsTableWriter.write(outputFileWords); + + return wordsTableWriter.getTable(); + } + + boolean acceptWord(Lock lock, long urlId, int wordId, int wordIdx, int block) { + int domainId = (int) (urlId >>> 32L); + + if (!partitioner.filterUnsafe(lock, domainId, bucketId)) { + return false; + } + + return true; + } +} + diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexPreconverter.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexPreconverter.java new file mode 100644 index 00000000..02197e3d --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexPreconverter.java @@ -0,0 +1,135 @@ +package nu.marginalia.wmsa.edge.index.service.index; + +import com.google.inject.Inject; +import gnu.trove.set.hash.TIntHashSet; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDomainBlacklist; +import nu.marginalia.wmsa.edge.index.service.query.SearchIndexPartitioner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.util.Objects; + +public class SearchIndexPreconverter { + private static final int CHUNK_HEADER_SIZE = 16; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final SearchIndexPartitioner partitioner; + private final TIntHashSet spamDomains; + + @SneakyThrows + public static long wordCount(File inputFile) { + try (RandomAccessFile raf = new RandomAccessFile(inputFile, "r")) { + raf.readLong(); + return raf.readInt(); + } + } + + @SneakyThrows + @Inject + public SearchIndexPreconverter(File inputFile, + File[] outputFiles, + SearchIndexPartitioner partitioner, + EdgeDomainBlacklist blacklist) + { + this.partitioner = partitioner; + this.spamDomains = blacklist.getSpamDomains(); + logger.info("Preconverting {}", inputFile); + + for (File f : outputFiles) { + if (f.exists()) { + Files.deleteIfExists(Objects.requireNonNull(f).toPath()); + } + } + + final RandomAccessFile raf = new RandomAccessFile(inputFile, "r"); + + var fileLength = raf.readLong(); + var wordCount = raf.readInt(); + final int wordCountOriginal = wordCount; + + logger.info("Word Count: {}", wordCount); + logger.info("File Length: {}", fileLength); + + var channel = raf.getChannel(); + + ByteBuffer inByteBuffer = ByteBuffer.allocateDirect(10_000); + + RandomAccessFile[] randomAccessFiles = new RandomAccessFile[outputFiles.length]; + for (int i = 0; i < randomAccessFiles.length; i++) { + randomAccessFiles[i] = new RandomAccessFile(outputFiles[i], "rw"); + randomAccessFiles[i].seek(12); + } + FileChannel[] fileChannels = new FileChannel[outputFiles.length]; + for (int i = 0; i < fileChannels.length; i++) { + fileChannels[i] = randomAccessFiles[i].getChannel(); + } + + + var lock = partitioner.getReadLock(); + try { + lock.lock(); + + while (channel.position() < fileLength) { + inByteBuffer.clear(); + inByteBuffer.limit(CHUNK_HEADER_SIZE); + channel.read(inByteBuffer); + inByteBuffer.flip(); + long urlId = inByteBuffer.getLong(); + int chunkBlock = inByteBuffer.getInt(); + int count = inByteBuffer.getInt(); + // inByteBuffer.clear(); + inByteBuffer.limit(count * 4 + CHUNK_HEADER_SIZE); + channel.read(inByteBuffer); + inByteBuffer.position(CHUNK_HEADER_SIZE); + + for (int i = 0; i < count; i++) { + wordCount = Math.max(wordCount, 1 + inByteBuffer.getInt()); + } + + inByteBuffer.position(count * 4 + CHUNK_HEADER_SIZE); + + + if (isUrlAllowed(urlId)) { + for (int i = 0; i < randomAccessFiles.length; i++) { + if (partitioner.filterUnsafe(lock, (int) (urlId >>> 32L), i)) { + inByteBuffer.flip(); + fileChannels[i].write(inByteBuffer); + } + } + } + } + } + finally { + lock.unlock(); + } + + if (wordCountOriginal < wordCount) { + logger.warn("Raised word count {} => {}", wordCountOriginal, wordCount); + } + + for (int i = 0; i < randomAccessFiles.length; i++) { + long pos = randomAccessFiles[i].getFilePointer(); + randomAccessFiles[i].seek(0); + randomAccessFiles[i].writeLong(pos); + randomAccessFiles[i].writeInt(wordCount); + fileChannels[i].force(true); + fileChannels[i].close(); + randomAccessFiles[i].close(); + } + }; + + private boolean isUrlAllowed(long url) { + int urlId = (int)(url & 0xFFFF_FFFFL); + int domainId = (int)(url >>> 32); + + return partitioner.isGoodUrl(urlId) && !spamDomains.contains(domainId); + } + +} + diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexReader.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexReader.java new file mode 100644 index 00000000..df269034 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexReader.java @@ -0,0 +1,134 @@ +package nu.marginalia.wmsa.edge.index.service.index; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.inject.Inject; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.index.service.query.IndexQueryBuilder; +import nu.marginalia.wmsa.edge.index.service.query.IndexSearchBudget; +import nu.marginalia.wmsa.edge.index.service.query.Query; +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.EnumMap; +import java.util.function.LongPredicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class SearchIndexReader implements AutoCloseable { + + private final EnumMap indices; + + private final EnumMap queryBuilders; + private final EnumMap underspecifiedQueryBuilders; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Cache, Long> numHitsCache = CacheBuilder.newBuilder().maximumSize(1000).build(); + + private static final IndexBlock[] indicesBySearchOrder = new IndexBlock[] { + IndexBlock.Top, + IndexBlock.Middle, + IndexBlock.Low, + IndexBlock.Words, + IndexBlock.NamesWords, + }; + + @Inject + public SearchIndexReader( + EnumMap indices) { + this.indices = indices; + + var lowIndex = indices.get(IndexBlock.Low); + var midIndex = indices.get(IndexBlock.Middle); + var topIndex = indices.get(IndexBlock.Top); + var linkIndex = indices.get(IndexBlock.Link); + var titleIndex = indices.get(IndexBlock.Title); + var namesIndex = indices.get(IndexBlock.NamesWords); + var positionIndex = indices.get(IndexBlock.PositionWords); + var titleKeywordsIndex = indices.get(IndexBlock.TitleKeywords); + var wordsIndex = indices.get(IndexBlock.Words); + var metaIndex = indices.get(IndexBlock.Meta); + var topicIndex = indices.get(IndexBlock.Topic); + + queryBuilders = new EnumMap<>(IndexBlock.class); + underspecifiedQueryBuilders = new EnumMap<>(IndexBlock.class); + + queryBuilders.put(IndexBlock.Words, new IndexQueryBuilder(Stream.of(metaIndex, titleKeywordsIndex, topicIndex, titleIndex, topIndex, midIndex, lowIndex, namesIndex, wordsIndex).collect(Collectors.toList()), wordsIndex)); + queryBuilders.put(IndexBlock.Low, new IndexQueryBuilder(Stream.of(metaIndex, titleKeywordsIndex, topicIndex, titleIndex, topIndex, midIndex, lowIndex, namesIndex).collect(Collectors.toList()), wordsIndex)); + queryBuilders.put(IndexBlock.Middle, new IndexQueryBuilder(Stream.of(metaIndex, titleKeywordsIndex, topicIndex, titleIndex, topIndex, midIndex).collect(Collectors.toList()), wordsIndex)); + queryBuilders.put(IndexBlock.Top, new IndexQueryBuilder(Stream.of(metaIndex, titleKeywordsIndex, topicIndex, titleIndex, topIndex).collect(Collectors.toList()), wordsIndex)); + queryBuilders.put(IndexBlock.PositionWords, new IndexQueryBuilder(Stream.of(metaIndex, titleKeywordsIndex, topicIndex, titleIndex, namesIndex, positionIndex).collect(Collectors.toList()), wordsIndex)); + queryBuilders.put(IndexBlock.NamesWords, new IndexQueryBuilder(Stream.of(metaIndex, titleKeywordsIndex, topicIndex, titleIndex, namesIndex).collect(Collectors.toList()), wordsIndex)); + queryBuilders.put(IndexBlock.Link, new IndexQueryBuilder(Stream.of(metaIndex, titleKeywordsIndex, topicIndex, titleIndex, linkIndex).collect(Collectors.toList()), wordsIndex)); + queryBuilders.put(IndexBlock.Title, new IndexQueryBuilder(Stream.of(metaIndex, titleKeywordsIndex, topicIndex, titleIndex).collect(Collectors.toList()), wordsIndex)); + queryBuilders.put(IndexBlock.TitleKeywords, new IndexQueryBuilder(Stream.of(metaIndex, titleKeywordsIndex).collect(Collectors.toList()), wordsIndex)); + + underspecifiedQueryBuilders.put(IndexBlock.TitleKeywords, new IndexQueryBuilder(Stream.of(titleKeywordsIndex, linkIndex, topicIndex, topIndex, midIndex, lowIndex, namesIndex, positionIndex, metaIndex).collect(Collectors.toList()), wordsIndex)); + underspecifiedQueryBuilders.put(IndexBlock.Link, new IndexQueryBuilder(Stream.of(linkIndex, topicIndex, topIndex, midIndex, lowIndex, namesIndex, positionIndex, metaIndex).collect(Collectors.toList()), wordsIndex)); + } + + public Query findUnderspecified( + IndexBlock block, + IndexSearchBudget budget, + LongPredicate filter, + int wordId) { + var builder = underspecifiedQueryBuilders.get(block); + if (null != builder) { + return builder.buildUnderspecified(budget, filter, wordId); + } + return findWord(block, budget, filter, wordId); + } + + public Query findWord(IndexBlock block, IndexSearchBudget budget, LongPredicate filter, int wordId) { + return queryBuilders.get(block).build(budget, filter, wordId); + } + + @Override + public void close() throws Exception { + for (var idx : indices.values()) { + idx.close(); + } + numHitsCache.invalidateAll(); + numHitsCache.cleanUp(); + } + + @SneakyThrows + public long numHits(IndexBlock block, int word) { + return numHitsCache.get(Pair.of(block, word), + () -> queryBuilders.get(block) + .getIndicies() + .stream() + .mapToLong(idx -> idx.numUrls(word)) + .sum() + ); + + } + + + public IndexBlock getBlockForResult(int searchTerm, long urlId) { + for (var block : indicesBySearchOrder) { + var index = indices.get(block); + + if (null == index) { + continue; + } + + var range = index.rangeForWord(searchTerm); + if (index.hasUrl(urlId, range)) { + return block; + } + } + return IndexBlock.Words; + } + + public boolean isTermInBucket(IndexBlock block, int searchTerm, long urlId) { + final var index = indices.get(block); + if (null == index) return false; + + final var range = index.rangeForWord(searchTerm); + + return index.hasUrl(urlId, range); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexScrubberMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexScrubberMain.java new file mode 100644 index 00000000..7225dbb5 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexScrubberMain.java @@ -0,0 +1,79 @@ +package nu.marginalia.wmsa.edge.index.service.index; + +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Path; + +public class SearchIndexScrubberMain { + public static final Logger logger = LoggerFactory.getLogger(SearchIndexScrubberMain.class); + private static final int CHUNK_HEADER_SIZE = 16; + + public static void main(String... args) throws IOException { + var inputFile = Path.of(args[0]).toFile(); + var outputFile = Path.of(args[1]).toFile(); + + logger.info("Scrubbing {}", inputFile); + + final RandomAccessFile raf = new RandomAccessFile(inputFile, "r"); + + var fileLength = raf.readLong(); + var wordCount = raf.readInt(); + + logger.info("Word Count: {}", wordCount); + logger.info("File Length: {}", fileLength); + + var channel = raf.getChannel(); + + ByteBuffer inByteBuffer = ByteBuffer.allocateDirect(10_000); + + RandomAccessFile[] randomAccessFiles = new RandomAccessFile[1]; + + for (int i = 0; i < randomAccessFiles.length; i++) { + randomAccessFiles[i] = new RandomAccessFile(outputFile, "rw"); + randomAccessFiles[i].seek(12); + } + FileChannel[] fileChannels = new FileChannel[1]; + for (int i = 0; i < fileChannels.length; i++) { + fileChannels[i] = randomAccessFiles[i].getChannel(); + } + + while (channel.position() < fileLength) { + inByteBuffer.clear(); + inByteBuffer.limit(CHUNK_HEADER_SIZE); + channel.read(inByteBuffer); + inByteBuffer.flip(); + long urlId = inByteBuffer.getLong(); + int chunkBlock = inByteBuffer.getInt(); + int count = inByteBuffer.getInt(); + inByteBuffer.clear(); + inByteBuffer.limit(count*4+CHUNK_HEADER_SIZE); + inByteBuffer.putLong(urlId); + inByteBuffer.putInt(chunkBlock); + inByteBuffer.putInt(count); + channel.read(inByteBuffer); + + + if (chunkBlock == IndexBlock.Link.id) { + for (int i = 0; i < randomAccessFiles.length; i++) { + inByteBuffer.flip(); + fileChannels[i].write(inByteBuffer); + } + } + + } + + long size = randomAccessFiles[0].getFilePointer(); + + randomAccessFiles[0].seek(0); + randomAccessFiles[0].writeLong(size); + randomAccessFiles[0].writeInt(wordCount); + + randomAccessFiles[0].close(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexWriter.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexWriter.java new file mode 100644 index 00000000..ca5d70b3 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexWriter.java @@ -0,0 +1,16 @@ +package nu.marginalia.wmsa.edge.index.service.index; + +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeId; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +import java.util.List; + +public interface SearchIndexWriter { + void put(EdgeId domainId, EdgeId urlId, IndexBlock block, List words); + void forceWrite(); + + void flushWords(); + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexWriterImpl.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexWriterImpl.java new file mode 100644 index 00000000..2f482815 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/SearchIndexWriterImpl.java @@ -0,0 +1,121 @@ +package nu.marginalia.wmsa.edge.index.service.index; + +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.index.service.dictionary.DictionaryWriter; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeId; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.EOFException; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class SearchIndexWriterImpl implements SearchIndexWriter { + private final DictionaryWriter dictionaryWriter; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Disposable writerTask; + private RandomAccessFile raf; + private FileChannel channel; + + public static final int MAX_BLOCK_SIZE = 1000*32*8*4; + private final ByteBuffer byteBuffer; + private long pos; + + @SneakyThrows + public SearchIndexWriterImpl(DictionaryWriter dictionaryWriter, File indexFile) { + this.dictionaryWriter = dictionaryWriter; + initializeIndexFile(indexFile); + + byteBuffer = ByteBuffer.allocate(MAX_BLOCK_SIZE); + + writerTask = Schedulers.io().schedulePeriodicallyDirect(this::forceWrite, 1, 1, TimeUnit.SECONDS); + Runtime.getRuntime().addShutdownHook(new Thread(this::forceWrite)); + } + + private void initializeIndexFile(File indexFile) throws IOException { + raf = new RandomAccessFile(indexFile, "rw"); + channel = raf.getChannel(); + + try { + pos = raf.readLong(); + raf.seek(pos); + logger.info("Resuming index file of size {}", pos); + } + catch (EOFException ex) { + logger.info("Clean index file"); + writePositionMarker(); + writePositionMarker(); + } + } + + @Override + @SneakyThrows + public synchronized void put(EdgeId domainId, EdgeId urlId, IndexBlock block, List wordsSuspect) { + int numGoodWords = 0; + for (String word : wordsSuspect) { + if (word.length() < Byte.MAX_VALUE) numGoodWords++; + } + + byteBuffer.clear(); + long url_id = ((long) domainId.getId() << 32) | urlId.getId(); + byteBuffer.putLong(url_id); + byteBuffer.putInt(block.id); + byteBuffer.putInt(numGoodWords); + + for (String word : wordsSuspect) { + if (word.length() < Byte.MAX_VALUE) { + byteBuffer.putInt(dictionaryWriter.get(word)); + } + } + byteBuffer.limit(byteBuffer.position()); + byteBuffer.rewind(); + + while (byteBuffer.position() < byteBuffer.limit()) + channel.write(byteBuffer); + + writePositionMarker(); + } + + @Override + public synchronized void forceWrite() { + try { + channel.force(false); + } + catch (IOException ex) { + logger.error("IO Exception", ex); + } + } + + + @Override + public void flushWords() { + dictionaryWriter.commitToDisk(); + } + + private void writePositionMarker() throws IOException { + var lock = channel.lock(0, 12, false); + pos = channel.size(); + raf.seek(0); + raf.writeLong(pos); + raf.writeInt(dictionaryWriter.size()); + raf.seek(pos); + lock.release(); + } + + public synchronized void close() throws IOException { + writerTask.dispose(); + channel.close(); + raf.close(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/wordstable/BtreeWordsTable.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/wordstable/BtreeWordsTable.java new file mode 100644 index 00000000..0a6a70c0 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/wordstable/BtreeWordsTable.java @@ -0,0 +1,88 @@ +package nu.marginalia.wmsa.edge.index.service.index.wordstable; + +import com.upserve.uppend.blobs.NativeIO; +import nu.marginalia.util.btree.BTreeReader; +import nu.marginalia.util.btree.model.BTreeHeader; +import nu.marginalia.util.multimap.MultimapFileLong; + +import java.util.function.LongConsumer; + +import static nu.marginalia.wmsa.edge.index.service.index.wordstable.WordsTableWriter.wordsBTreeContext; + +public class BtreeWordsTable extends IndexWordsTable{ + private final MultimapFileLong words; + private final BTreeReader reader; + private final BTreeHeader header; + private final int HEADER_OFFSET = 1; + + public BtreeWordsTable(MultimapFileLong words) { + this.words = words; + + + reader = new BTreeReader(words, wordsBTreeContext); + header = reader.getHeader(HEADER_OFFSET); + + madvise(); + } + + private void madvise() { + words.advice(NativeIO.Advice.Random); + words.advice0(NativeIO.Advice.WillNeed); + + var h = reader.getHeader(HEADER_OFFSET); + int length = (int)(h.dataOffsetLongs() - h.indexOffsetLongs()); + words.adviceRange(NativeIO.Advice.WillNeed, h.indexOffsetLongs(), length); + words.pokeRange(h.indexOffsetLongs(), length); + } + + public void forEachWordsOffset(LongConsumer offsetConsumer) { + int n = header.numEntries(); + long offset = header.dataOffsetLongs(); + + for (int i = 0; i < n; i++) { + try { + long posOffset = 2*(offset + i); + if (posOffset * 8 >= words.size()) { + break; + } + + long sz = words.get(posOffset); + if ((sz>> 32) > 0) { + offsetConsumer.accept(words.get(posOffset+1)); + } + } + catch (RuntimeException ex) { + logger.warn("Error @ " + i, ex); + break; + } + } + } + + @Override + public long positionForWord(int wordId) { + + long offset = reader.offsetForEntry(header, wordId); + if (offset < 0) { + return -1L; + } + + return words.get(offset+1); + } + + @Override + public int wordLength(int wordId) { + + long offset = reader.offsetForEntry(header, wordId); + if (offset < 0) { + return -1; + } + + return (int)(words.get(offset) >> 32); + } + + @Override + public void close() throws Exception { + words.close(); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/wordstable/IndexWordsTable.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/wordstable/IndexWordsTable.java new file mode 100644 index 00000000..5b557db1 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/wordstable/IndexWordsTable.java @@ -0,0 +1,48 @@ +package nu.marginalia.wmsa.edge.index.service.index.wordstable; + +import nu.marginalia.util.multimap.MultimapFileLong; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.util.function.LongConsumer; + +public abstract class IndexWordsTable implements AutoCloseable { + final Logger logger = LoggerFactory.getLogger(getClass()); + + private static final int BUFFER_SIZE = 1024*1024*64; + + public static IndexWordsTable ofFile(RandomAccessFile file) throws IOException { + var wordsFile = openWordsFile(file); + long signature = wordsFile.get(0); + + if (signature == Strategy.BTREE.ordinal()) { + return new BtreeWordsTable(wordsFile); + } + throw new IllegalArgumentException("Unknown signature " + signature); + } + + private static MultimapFileLong openWordsFile(RandomAccessFile wordsFile) throws IOException { + return new MultimapFileLong(wordsFile, + FileChannel.MapMode.READ_ONLY, wordsFile.length(), BUFFER_SIZE, false); + } + + public abstract long positionForWord(int wordId); + + public abstract int wordLength(int wordId); + public abstract void forEachWordsOffset(LongConsumer offsetConsumer); + + @Override + public void close() throws Exception { + + } + + public record TableWordRange(long start, long end) {} + + public enum Strategy { + FLAT, HASH, BTREE_OLD, BTREE + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/wordstable/WordsTableWriter.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/wordstable/WordsTableWriter.java new file mode 100644 index 00000000..3097dd47 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/index/wordstable/WordsTableWriter.java @@ -0,0 +1,85 @@ +package nu.marginalia.wmsa.edge.index.service.index.wordstable; + +import nu.marginalia.util.btree.BTreeWriter; +import nu.marginalia.util.btree.model.BTreeContext; +import nu.marginalia.util.multimap.MultimapFileLong; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; + +import static nu.marginalia.wmsa.edge.index.service.index.SearchIndexConverter.urlsBTreeContext; + +public class WordsTableWriter { + private final long[] table; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + public static final BTreeContext wordsBTreeContext = new BTreeContext(7, 2, 0x0000_0000_FFFF_FFFFL, 8); + + public WordsTableWriter(int length) { + table = new long[length]; + } + + public void acceptWord(int wordId) { + if (wordId >= table.length) { + logger.warn("Invalid word-id {}", wordId); + } + else { + table[wordId]++; + } + } + + public long[] getTable() { + return table; + } + public void write(File file) throws Exception { + + int tableSize = 0; + + if (table[0] != 0) tableSize = 1; + + for (int i = 1; i < table.length; i++) { + if (table[i] != 0) { + tableSize++; + } + table[i] += table[i-1]; + } + + logger.info("Writing table {} words {} max", tableSize, table.length); + + writeBtreeWordsFile(file, table, tableSize); + + } + + private void writeBtreeWordsFile(File outputFileWords, long[] table, int tableSize) throws Exception { + try (var mmf = MultimapFileLong.forOutput(outputFileWords.toPath(), tableSize/8L)) { + mmf.put(0, IndexWordsTable.Strategy.BTREE.ordinal()); + long offset = 1; + + var writer = new BTreeWriter(mmf, wordsBTreeContext); + + writer.write(offset, tableSize, (idx) -> { + long urlFileOffset = 0; + + if (table[0] != 0) { + int length = (int) table[0]; + mmf.put(idx++, (long)length<<32); + mmf.put(idx++, 0); + + urlFileOffset += (urlsBTreeContext.calculateSize(length)); + } + + for (int i = 1; i < table.length; i++) { + if (table[i] != table[i - 1]) { + int length = (int)(table[i] - table[i-1]); + mmf.put(idx++, (long)length << 32 | i); + mmf.put(idx++, urlFileOffset); + + urlFileOffset += (urlsBTreeContext.calculateSize(length)); + } + } + }); + } + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/query/IndexQueryBuilder.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/query/IndexQueryBuilder.java new file mode 100644 index 00000000..de3f1435 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/query/IndexQueryBuilder.java @@ -0,0 +1,128 @@ +package nu.marginalia.wmsa.edge.index.service.query; + +import com.google.common.collect.Streams; +import nu.marginalia.wmsa.edge.index.service.index.SearchIndex; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.LongPredicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.LongStream; + +public class IndexQueryBuilder { + private final List requiredIndices; + private final SearchIndex excludeIndex; + + public Collection getIndicies() { + return requiredIndices; + } + + public IndexQueryBuilder(List requiredIndices, SearchIndex excludeIndex) { + this.requiredIndices = requiredIndices.stream().filter(Objects::nonNull).collect(Collectors.toList()); + this.excludeIndex = excludeIndex; + } + + public Query build(IndexSearchBudget budget, + LongPredicate filter, + int wordId) { + return new QueryForIndices(budget, filter, wordId); + } + + public Query buildUnderspecified(IndexSearchBudget budget, LongPredicate filter, int wordId) { + if (requiredIndices.size() == 1) { + return build(budget, filter, wordId); + } + + var ranges = requiredIndices.stream().map(idx -> idx.rangeForWord(wordId)).toArray(SearchIndex.UrlIndexTree[]::new); + var relevantIndices = IntStream.range(0, requiredIndices.size()).filter(i -> ranges[i].isPresent()).toArray(); + + if (relevantIndices.length == 0) { + return new QueryForIndices(budget, LongStream::empty); + } + else if (relevantIndices.length == 1 || relevantIndices[0] != 0) { + return build(budget, filter, wordId); + } + + var fstRange = requiredIndices.get(relevantIndices[0]).rangeForWord(wordId); + + return new QueryForIndices(budget, () -> + Streams.concat(IntStream.range(1, relevantIndices.length) + .mapToObj(i -> underspecifiedPairStream(budget, (int) budget.limit()/(relevantIndices.length*2), relevantIndices[0], relevantIndices[i], wordId)) + .flatMapToLong(Function.identity()), + fstRange.stream().takeWhile(budget::take)) + .filter(filter) + ); + } + + private LongStream underspecifiedPairStream(IndexSearchBudget budget, int limit, int firstIdx, int otherIdx, int wordId) { + SearchIndex first = requiredIndices.get(firstIdx), + second = requiredIndices.get(otherIdx); + + if (first.numUrls(wordId) > second.numUrls(wordId)) { + SearchIndex tmp = first; + first = second; + second = tmp; + } + + SearchIndex fst = first; + SearchIndex snd = second; + + var sndRange = snd.rangeForWord(wordId); + + return fst.rangeForWord(wordId).stream().takeWhile(budget::take).limit(limit).filter( + url -> snd.hasUrl(url, sndRange) + ); + } + + + + private class QueryForIndices implements Query { + private final Supplier supp; + private final IndexSearchBudget budget; + + private QueryForIndices(IndexSearchBudget budget, LongPredicate filter, int wordId) { + this.budget = budget; + supp = () -> + requiredIndices.stream().flatMapToLong(idx -> { + var range = idx.rangeForWord(wordId); + return range.stream().takeWhile(budget::take); + }) + .filter(filter); + } + + private QueryForIndices(IndexSearchBudget budget, Supplier supp) { + this.budget = budget; + this.supp = supp; + } + + @Override + public Query also(int wordId) { + return new QueryForIndices(budget, + () -> requiredIndices.stream().flatMapToLong(idx -> alsoStream(idx, wordId))); + } + + @Override + public Query not(int wordId) { + return new QueryForIndices(budget, () -> notStream(wordId)); + } + + private LongStream alsoStream(SearchIndex idx, int wordId) { + var range = idx.rangeForWord(wordId); + + return stream().filter(url -> idx.hasUrl(url, range)).takeWhile(budget::take); + } + + private LongStream notStream(int wordId) { + var bodyRange = excludeIndex.rangeForWord(wordId); + return stream().filter(url -> !excludeIndex.hasUrl(url, bodyRange)).takeWhile(budget::take); + } + + public LongStream stream() { + return supp.get(); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/query/IndexSearchBudget.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/query/IndexSearchBudget.java new file mode 100644 index 00000000..96940b07 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/query/IndexSearchBudget.java @@ -0,0 +1,21 @@ +package nu.marginalia.wmsa.edge.index.service.query; + +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; + +import java.util.concurrent.atomic.AtomicInteger; + +@RequiredArgsConstructor +public class IndexSearchBudget { + private final long limit; + private long used = 0; + + public boolean take(long unused) { + return used++ < limit; + } + + public long used() { + return used; + } + public long limit() { return limit; } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/query/Query.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/query/Query.java new file mode 100644 index 00000000..09f7701b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/query/Query.java @@ -0,0 +1,10 @@ +package nu.marginalia.wmsa.edge.index.service.query; + +import java.util.stream.LongStream; + +public interface Query { + Query also(int wordId); + Query not(int wordId); + + LongStream stream(); +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/query/SearchIndexPartitioner.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/query/SearchIndexPartitioner.java new file mode 100644 index 00000000..b8c93f24 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/query/SearchIndexPartitioner.java @@ -0,0 +1,168 @@ +package nu.marginalia.wmsa.edge.index.service.query; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import gnu.trove.set.hash.TIntHashSet; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.index.service.SearchEngineRanking; +import nu.marginalia.wmsa.edge.index.service.SearchIndexDao; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import static nu.marginalia.wmsa.edge.index.EdgeIndexService.DYNAMIC_BUCKET_LENGTH; + +@Singleton +public class SearchIndexPartitioner { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final PartitionSet partitionSet; + + private SearchEngineRanking retroRanking = null; + private SearchEngineRanking smallWebRanking = null; + private SearchEngineRanking prWebRanking = null; + private SearchEngineRanking specialDomainRanking = null; + private SearchEngineRanking academiaRanking = null; + + private volatile TIntHashSet goodUrls; + + private final SearchIndexDao dao; + private final ReadWriteLock rwl = new ReentrantReadWriteLock(); + + @Inject + public SearchIndexPartitioner(SearchIndexDao dao) { + this.dao = dao; + + if (null == dao) { + partitionSet = this::yesFilter; + } + else { + partitionSet = this::byPartitionTable; + } + } + + public boolean isBusy() { + var readLock = rwl.readLock(); + try { + return !readLock.tryLock(); + } + finally { + readLock.unlock(); + } + } + + public void reloadPartitions() { + if (dao == null) { + logger.info("No dao = no partition table"); + return; + } + + logger.info("Fetching URLs"); + + if (goodUrls != null) { + goodUrls.clear(); + } + goodUrls = dao.goodUrls(); + + logger.info("Fetching domains"); + + var retroDomains = dao.getDomainsByRealPageRank(); + var smallWebDomains = dao.getSmallWebDomains(); + var academiaDomains = dao.getAcademiaDomains(); + var prWebDomains = dao.getDomainsByStandardPageRank(); + var specialDomains = dao.getSpecialDomains(); + + logger.info("Got {} retro domains", retroDomains.size()); + logger.info("Got {} small domains", smallWebDomains.size()); + logger.info("Got {} academia domains", academiaDomains.size()); + logger.info("Got {} corpo domains", prWebDomains.size()); + logger.info("Got {} special domains", specialDomains.size()); + + var lock = rwl.writeLock(); + try { + lock.lock(); + retroRanking = new SearchEngineRanking(0, retroDomains, 0.2, 1); + smallWebRanking = new SearchEngineRanking(2, smallWebDomains, 0.15); + academiaRanking = new SearchEngineRanking(3, academiaDomains, 1); + prWebRanking = new SearchEngineRanking(4, prWebDomains, 0.2, 1); + specialDomainRanking = new SearchEngineRanking(6, specialDomains, 1); + logger.info("Finished building partitions table"); + } + finally { + lock.unlock(); + } + } + + public boolean isGoodUrl(int urlId) { + if (goodUrls == null) + return true; + return goodUrls.contains(urlId); + } + + private boolean yesFilter(int domainId, int bucketId) { + return true; + } + private boolean byPartitionTable(int domainId, int bucketId) { + if (retroRanking.hasBucket(bucketId, domainId)) + return true; + if (smallWebRanking.hasBucket(bucketId, domainId)) + return true; + if (academiaRanking.hasBucket(bucketId, domainId)) + return true; + if (prWebRanking.hasBucket(bucketId, domainId)) + return true; + if (specialDomainRanking.hasBucket(bucketId, domainId)) + return true; + + return DYNAMIC_BUCKET_LENGTH == bucketId; + } + + @SneakyThrows + public Lock getReadLock() { + return rwl.readLock(); + } + public boolean filterUnsafe(Lock lock, int domainId, int bucketId) { + return partitionSet.test(domainId, bucketId); + } + + @Deprecated + public boolean filter(int domainId, int bucketId) { + var lock = rwl.readLock(); + try { + lock.lock(); + return partitionSet.test(domainId, bucketId); + } + finally { + lock.unlock(); + } + } + + public int translateId(int bucketId, int id) { + if (retroRanking != null && retroRanking.ownsBucket(bucketId)) { + return retroRanking.translateId(id); + } + if (smallWebRanking != null && smallWebRanking.ownsBucket(bucketId)) { + return smallWebRanking.translateId(id); + } + if (academiaRanking != null && academiaRanking.ownsBucket(bucketId)) { + return academiaRanking.translateId(id); + } + if (prWebRanking != null && prWebRanking.ownsBucket(bucketId)) { + return prWebRanking.translateId(id); + } + if (specialDomainRanking != null && specialDomainRanking.ownsBucket(bucketId)) { + return specialDomainRanking.translateId(id); + } + if (retroRanking != null) { + return retroRanking.translateId(id); + } + return id; + } + + interface PartitionSet { + boolean test(int domainId, int bucketId); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/AcademiaRank.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/AcademiaRank.java new file mode 100644 index 00000000..b14dc405 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/AcademiaRank.java @@ -0,0 +1,49 @@ +package nu.marginalia.wmsa.edge.index.service.util.ranking; + +import com.zaxxer.hikari.HikariDataSource; +import gnu.trove.list.TIntList; +import gnu.trove.list.array.TIntArrayList; +import gnu.trove.map.hash.TIntIntHashMap; +import it.unimi.dsi.fastutil.ints.IntArrays; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.sql.SQLException; + +public class AcademiaRank { + private final TIntArrayList result; + private static final Logger logger = LoggerFactory.getLogger(AcademiaRank.class); + + public AcademiaRank(HikariDataSource ds, String... origins) throws IOException { + + TIntList rankingResults = new BetterStandardPageRank(ds, origins).pageRank(100_000); + TIntIntHashMap idToRanking = new TIntIntHashMap(100_000, 0.5f, -1, 1_000_000_000); + + for (int i = 0; i < rankingResults.size(); i++) { + idToRanking.put(rankingResults.get(i), i); + } + + result = new TIntArrayList(10000); + try (var conn = ds.getConnection(); + var stmt = conn.prepareStatement("select EC_DOMAIN.ID,COUNT(SOURCE_DOMAIN_ID) AS CNT from EC_DOMAIN INNER JOIN DOMAIN_METADATA ON DOMAIN_METADATA.ID=EC_DOMAIN.ID INNER JOIN EC_DOMAIN_LINK ON EC_DOMAIN_LINK.DEST_DOMAIN_ID=EC_DOMAIN.ID WHERE INDEXED>0 AND STATE>=0 AND STATE<2 AND ((VISITED_URLS>1000+1500*RANK AND RANK<1) OR (GOOD_URLS>1000 AND URL_PART LIKE '%edu')) GROUP BY EC_DOMAIN.ID HAVING CNT<1500 ORDER BY RANK ASC")) { + + stmt.setFetchSize(1000); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + result.add(rsp.getInt(1)); + } + } + catch (SQLException ex) { + logger.error("SQL error", ex); + } + + int[] internalArray = result.toArray(); + IntArrays.quickSort(internalArray, (a,b) -> idToRanking.get(a) - idToRanking.get(b)); + result.set(0, internalArray); + } + + public TIntArrayList getResult() { + return result; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/BetterReversePageRank.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/BetterReversePageRank.java new file mode 100644 index 00000000..798be55a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/BetterReversePageRank.java @@ -0,0 +1,46 @@ +package nu.marginalia.wmsa.edge.index.service.util.ranking; + + +import com.zaxxer.hikari.HikariDataSource; + +import java.io.IOException; + +public class BetterReversePageRank extends RankingAlgorithm { + + + public BetterReversePageRank(HikariDataSource dataSource, String... origins) throws IOException { + super(dataSource, origins); + } + + @Override + RankVector createNewRankVector(RankVector rank) { + + double rankNorm = rank.norm(); + RankVector newRank = new RankVector(0); + + for (int domainId = 0; domainId < domainIndexToId.size(); domainId++) { + + var links = linkDataSrc2Dest[domainId]; + double newRankValue = 0; + + if (links != null && links.size() > 0) { + + + for (int j = 0; j < links.size(); j++) { + var revLinks = linkDataDest2Src[links.getQuick(j)]; + newRankValue += rank.get(links.getQuick(j)) / revLinks.size(); + } + } + + newRank.set(domainId, 0.85*newRankValue/rankNorm); + } + + return newRank; + } + + @Override + void adjustRankVector(RankVector vector, double dNorm, double oldNorm) { + originDomainIds.forEach(id -> vector.increment(id, 1.0 / originDomainIds.size())); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/BetterStandardPageRank.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/BetterStandardPageRank.java new file mode 100644 index 00000000..497ac146 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/BetterStandardPageRank.java @@ -0,0 +1,50 @@ +package nu.marginalia.wmsa.edge.index.service.util.ranking; + + +import com.zaxxer.hikari.HikariDataSource; +import gnu.trove.list.array.TIntArrayList; + +import java.io.IOException; + +public class BetterStandardPageRank extends RankingAlgorithm { + + public BetterStandardPageRank(HikariDataSource dataSource, String... origins) throws IOException { + super(dataSource, origins); + } + + @Override + RankVector createNewRankVector(RankVector rank) { + RankVector newRank = new RankVector(0); + + for (int domainId = 0; domainId < domainIndexToId.size(); domainId++) { + + var links = linkDataDest2Src[domainId]; + double newRankValue = 0; + + if (links != null && links.size() > 0) { + for (int j = 0; j < links.size(); j++) { + int linkedDomain = links.getQuick(j); + + int linkSize = 1; + var bl = linkDataSrc2Dest[linkedDomain]; + if (bl != null) { + linkSize = bl.size(); + } + + newRankValue += rank.get(linkedDomain) / linkSize; + + } + } + + newRank.set(domainId, 0.85 * newRankValue); + } + return newRank; + } + + @Override + void adjustRankVector(RankVector vector, double dNorm, double oldNorm) { + originDomainIds.forEach(id -> vector.increment(id, 0.15 / originDomainIds.size() /* dNorm/originDomainIds.size() */ )); +// vector.incrementAll(0.14*dNorm/vector.size()); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/BuggyReversePageRank.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/BuggyReversePageRank.java new file mode 100644 index 00000000..1fd696ab --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/BuggyReversePageRank.java @@ -0,0 +1,43 @@ +package nu.marginalia.wmsa.edge.index.service.util.ranking; + + +import com.zaxxer.hikari.HikariDataSource; + +import java.io.IOException; + +public class BuggyReversePageRank extends RankingAlgorithm { + + + public BuggyReversePageRank(HikariDataSource dataSource, String... origins) throws IOException { + super(dataSource, origins); + } + + @Override + RankVector createNewRankVector(RankVector rank) { + + double rankNorm = rank.norm(); + RankVector newRank = new RankVector(0); + + for (int domainId = 0; domainId < domainIndexToId.size(); domainId++) { + + var links = linkDataSrc2Dest[domainId]; + + if (links != null && links.size() > 0) { + double newRankValue = 0; + + for (int j = 0; j < links.size(); j++) { + newRankValue += rank.get(links.getQuick(j)) / links.size(); + } + + newRank.set(domainId, 0.85*newRankValue/rankNorm); + } + } + return newRank; + } + + @Override + void adjustRankVector(RankVector vector, double dNorm, double oldNorm) { + originDomainIds.forEach(id -> vector.increment(domainIdToIndex.get(id), dNorm/oldNorm)); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/BuggyStandardPageRank.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/BuggyStandardPageRank.java new file mode 100644 index 00000000..c2bf65b4 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/BuggyStandardPageRank.java @@ -0,0 +1,49 @@ +package nu.marginalia.wmsa.edge.index.service.util.ranking; + + +import com.zaxxer.hikari.HikariDataSource; + +import java.io.IOException; + +public class BuggyStandardPageRank extends RankingAlgorithm { + + public BuggyStandardPageRank(HikariDataSource dataSource, String... origins) throws IOException { + super(dataSource, origins); + } + + @Override + RankingAlgorithm.RankVector createNewRankVector(RankingAlgorithm.RankVector rank) { + RankVector newRank = new RankVector(0); + + for (int domainId = 0; domainId < domainIndexToId.size(); domainId++) { + + var links = linkDataSrc2Dest[domainId]; + double newRankValue = 0; + + if (links != null && links.size() > 0) { + for (int j = 0; j < links.size(); j++) { + int linkedDomain = links.getQuick(j); + + int linkSize = 1; + var bl = linkDataSrc2Dest[linkedDomain]; + if (bl != null) { + linkSize = bl.size(); + } + + newRankValue += rank.get(linkedDomain) / linkSize; + + } + } + + newRank.set(domainId, 0.85 * newRankValue); + } + return newRank; + } + + @Override + void adjustRankVector(RankingAlgorithm.RankVector vector, double dNorm, double oldNorm) { + originDomainIds.forEach(id -> vector.increment(id, dNorm/originDomainIds.size())); + vector.incrementAll(0.14*dNorm/vector.size()); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/RankingAlgorithm.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/RankingAlgorithm.java new file mode 100644 index 00000000..ce63c0a6 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/RankingAlgorithm.java @@ -0,0 +1,476 @@ +package nu.marginalia.wmsa.edge.index.service.util.ranking; + +import com.zaxxer.hikari.HikariDataSource; +import gnu.trove.list.TIntList; +import gnu.trove.list.array.TIntArrayList; +import gnu.trove.map.hash.TIntIntHashMap; +import gnu.trove.map.hash.TIntObjectHashMap; +import gnu.trove.set.hash.TIntHashSet; +import it.unimi.dsi.fastutil.ints.IntComparator; +import lombok.AllArgsConstructor; +import lombok.Data; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDomainBlacklistImpl; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.*; +import java.util.function.IntToDoubleFunction; +import java.util.stream.IntStream; +import it.unimi.dsi.fastutil.ints.IntArrays; + +public abstract class RankingAlgorithm { + final TIntObjectHashMap domainsById = new TIntObjectHashMap<>(); + final TIntIntHashMap domainIndexToId = new TIntIntHashMap(); + final TIntIntHashMap domainIdToIndex = new TIntIntHashMap(); + + private final TIntHashSet spamDomains; + private final HikariDataSource dataSource; + + TIntArrayList[] linkDataSrc2Dest; + TIntArrayList[] linkDataDest2Src; + + public Set originDomains = new HashSet<>(); + public Set originDomainIds = new HashSet<>(); + + private int maxKnownUrls = Integer.MAX_VALUE; + + private static boolean getNames = true; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + public static void main(String... args) throws IOException { + var rpr = new BuggyReversePageRank(new DatabaseModule().provideConnection(), "wiki.xxiivv.com"); + var spr = new BuggyStandardPageRank(new DatabaseModule().provideConnection(), "memex.marginalia.nu"); + + var rankVector = spr.pageRankVector(); + var norm = rankVector.norm(); + rpr.pageRank(i -> rankVector.get(i) / norm, 25).forEach(i -> { + System.out.println(spr.domainNameFromId(i)); + return true; + }); + } + + public String domainNameFromId(int id) { + return domainsById.get(id).name; + } + public boolean isPeripheral(int id) { + return domainsById.get(id).peripheral; + } + + public RankingAlgorithm(HikariDataSource dataSource, String... origins) { + this.dataSource = dataSource; + var blacklist = new EdgeDomainBlacklistImpl(dataSource); + + spamDomains = blacklist.getSpamDomains(); + originDomains.addAll(Arrays.asList(origins)); + + try (var conn = dataSource.getConnection()) { + + String s; + if (getNames) { + s = "SELECT EC_DOMAIN.ID,URL_PART,DOMAIN_ALIAS,STATE,KNOWN_URLS FROM EC_DOMAIN INNER JOIN DOMAIN_METADATA ON EC_DOMAIN.ID=DOMAIN_METADATA.ID INNER JOIN EC_DOMAIN_LINK ON SOURCE_DOMAIN_ID=EC_DOMAIN.ID WHERE ((INDEXED>1 AND STATE >= 0) OR (INDEXED=1 AND VISITED_URLS=KNOWN_URLS AND GOOD_URLS>0)) AND QUALITY_RAW>=-20 AND SOURCE_DOMAIN_ID!=DEST_DOMAIN_ID GROUP BY EC_DOMAIN.ID"; + } + else { + s = "SELECT EC_DOMAIN.ID,\"\",DOMAIN_ALIAS,STATE,KNOWN_URLS FROM EC_DOMAIN INNER JOIN DOMAIN_METADATA ON EC_DOMAIN.ID=DOMAIN_METADATA.ID INNER JOIN EC_DOMAIN_LINK ON SOURCE_DOMAIN_ID=EC_DOMAIN.ID WHERE ((INDEXED>1 AND STATE >= 0) OR (INDEXED=1 AND VISITED_URLS=KNOWN_URLS AND GOOD_URLS>0)) AND QUALITY_RAW>=-20 AND SOURCE_DOMAIN_ID!=DEST_DOMAIN_ID GROUP BY EC_DOMAIN.ID"; + } + try (var stmt = conn.prepareStatement(s)) { + stmt.setFetchSize(10000); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + int id = rsp.getInt(1); + if (!spamDomains.contains(id)) { + + domainsById.put(id, new DomainData(id, rsp.getString(2), rsp.getInt(3), rsp.getInt(4), rsp.getInt(5), false)); + + domainIndexToId.put(domainIndexToId.size(), id); + domainIdToIndex.put(id, domainIdToIndex.size()); + } + } + } + + + linkDataSrc2Dest = new TIntArrayList[domainIndexToId.size()]; + linkDataDest2Src = new TIntArrayList[domainIndexToId.size()]; + + try (var stmt = conn.prepareStatement("SELECT SOURCE_DOMAIN_ID, DEST_DOMAIN_ID FROM EC_DOMAIN_LINK")) { + stmt.setFetchSize(10000); + + var rsp = stmt.executeQuery(); + + while (rsp.next()) { + int src = rsp.getInt(1); + int dst = rsp.getInt(2); + + if (src == dst) continue; + + if (domainsById.contains(src) && domainsById.contains(dst)) { + + int srcIdx = domainIdToIndex.get(src); + int dstIdx = domainIdToIndex.get(domainsById.get(dst).resolveAlias()); + + if (linkDataSrc2Dest[srcIdx] == null) { + linkDataSrc2Dest[srcIdx] = new TIntArrayList(); + } + linkDataSrc2Dest[srcIdx].add(dstIdx); + + if (linkDataDest2Src[dstIdx] == null) { + linkDataDest2Src[dstIdx] = new TIntArrayList(); + } + linkDataDest2Src[dstIdx].add(srcIdx); + } + } + } + + try (var stmt = conn.prepareStatement("SELECT ID FROM EC_DOMAIN WHERE URL_PART LIKE ?")) { + for (var seed : this.originDomains) { + stmt.setString(1, seed); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + int i = rsp.getInt(1); + int ival = domainIdToIndex.get(i); + if (ival != domainIdToIndex.getNoEntryValue() || domainIndexToId.get(0) == i) { + originDomainIds.add(ival); + } + else { + logger.debug("No value for {}", i); + } + } + logger.debug("{} -> {}", seed, originDomainIds.size()); + } + } + + logger.info("Origin Domains: {}", originDomainIds.size()); + + } catch (SQLException throwables) { + logger.error("SQL error", throwables); + } + } + + public void addPeripheralNodes(boolean includeErrorStates) { + + int newNodesIdxCutoff = domainIdToIndex.size(); + + logger.info("Inserting peripheral nodes"); + + try (var conn = dataSource.getConnection()) { + String s; + if (getNames) { + s = "SELECT EC_DOMAIN.ID,URL_PART,DOMAIN_ALIAS,STATE,KNOWN_URLS FROM EC_DOMAIN INNER JOIN DOMAIN_METADATA ON EC_DOMAIN.ID=DOMAIN_METADATA.ID LEFT JOIN EC_DOMAIN_LINK ON SOURCE_DOMAIN_ID=EC_DOMAIN.ID WHERE ((INDEXED>1 AND STATE >= 0) OR (INDEXED=1 AND VISITED_URLS=KNOWN_URLS AND GOOD_URLS>0)) AND QUALITY_RAW>=-20 AND EC_DOMAIN_LINK.ID IS NULL GROUP BY EC_DOMAIN.ID"; + } + else { + s = "SELECT EC_DOMAIN.ID,\"\",DOMAIN_ALIAS,STATE,KNOWN_URLS FROM EC_DOMAIN INNER JOIN DOMAIN_METADATA ON EC_DOMAIN.ID=DOMAIN_METADATA.ID LEFT JOIN EC_DOMAIN_LINK ON SOURCE_DOMAIN_ID=EC_DOMAIN.ID WHERE ((INDEXED>1 AND STATE >= 0) OR (INDEXED=1 AND VISITED_URLS=KNOWN_URLS AND GOOD_URLS>0)) AND QUALITY_RAW>=-20 AND EC_DOMAIN_LINK.ID IS NULL GROUP BY EC_DOMAIN.ID"; + } + try (var stmt = conn.prepareStatement(s)) { + stmt.setFetchSize(10000); + var rsp = stmt.executeQuery(); + + while (rsp.next()) { + int id = rsp.getInt(1); + + if (!spamDomains.contains(id)) { + domainsById.put(id, new DomainData(id, rsp.getString(2), rsp.getInt(3), rsp.getInt(4), rsp.getInt(5), true)); + + domainIndexToId.put(domainIndexToId.size(), id); + domainIdToIndex.put(id, domainIdToIndex.size()); + } + } + + } + + linkDataSrc2Dest = Arrays.copyOf(linkDataSrc2Dest, domainIndexToId.size()); + linkDataDest2Src = Arrays.copyOf(linkDataDest2Src, domainIndexToId.size()); + + try (var stmt = conn.prepareStatement("SELECT SOURCE_DOMAIN_ID, DEST_DOMAIN_ID FROM EC_DOMAIN_LINK")) { + stmt.setFetchSize(10000); + + var rsp = stmt.executeQuery(); + + while (rsp.next()) { + int src = rsp.getInt(1); + int dst = rsp.getInt(2); + + if (src == dst) continue; + + if (domainsById.contains(src) && domainsById.contains(dst)) { + + int srcIdx = domainIdToIndex.get(src); + int dstIdx = domainIdToIndex.get(domainsById.get(dst).resolveAlias()); + + // This looks like a bug, but it improves the results + if (srcIdx < newNodesIdxCutoff || dstIdx < newNodesIdxCutoff) + continue; + + if (linkDataSrc2Dest[srcIdx] == null) { + linkDataSrc2Dest[srcIdx] = new TIntArrayList(); + } + linkDataSrc2Dest[srcIdx].add(dstIdx); + + if (linkDataDest2Src[dstIdx] == null) { + linkDataDest2Src[dstIdx] = new TIntArrayList(); + } + linkDataDest2Src[dstIdx].add(srcIdx); + } + } + } + } catch (SQLException throwables) { + logger.error("SQL error", throwables); + } + + logger.info("Peripheral nodes inserted {} -> {}", newNodesIdxCutoff, domainIdToIndex.size()); + } + + public int size() { + return domainsById.size(); + } + + + public RankVector pageRankVector() { + RankVector rank = new RankVector(1.d / domainsById.size()); + + int iter_max = 100; + for (int i = 0; i < iter_max; i++) { + RankVector newRank = createNewRankVector(rank); + + double oldNorm = rank.norm(); + double newNorm = newRank.norm(); + double dNorm = oldNorm - newNorm ; + if (i < iter_max-1) { + adjustRankVector(newRank, dNorm, oldNorm); + } + + rank = newRank; + } + + return rank; + } + + + public TIntList pageRank(int resultCount) { + RankVector rank = new RankVector(1.d / domainsById.size()); + + int iter_max = 100; + for (int i = 0; i < iter_max; i++) { + RankVector newRank = createNewRankVector(rank); + + double oldNorm = rank.norm(); + double newNorm = newRank.norm(); + double dNorm = oldNorm - newNorm; + + if (i < iter_max-1) { + adjustRankVector(newRank, dNorm, oldNorm); + } + + rank = newRank; + } + + + return rank.getRanking(resultCount); + } + + public TIntList pageRankWithPeripheralNodes(int resultCount, boolean includeErrorStates) { + RankVector rank = new RankVector(1.d / domainsById.size()); + + int iter_max = 100; + + for (int i = 0; i < iter_max; i++) { + if (i == iter_max-1) { + addPeripheralNodes(includeErrorStates); + } + RankVector newRank = createNewRankVector(rank); + + double oldNorm = rank.norm(); + double newNorm = newRank.norm(); + double dNorm = oldNorm - newNorm; + + if (i < iter_max-1) { + adjustRankVector(newRank, dNorm, oldNorm); + } + + rank = newRank; + } + + logger.info("PRWPN iteration done"); + + return rank.getRanking(resultCount); + } + + abstract void adjustRankVector(RankVector vector, double dNorm, double oldNorm); + + public TIntList pageRank(IntToDoubleFunction weight, int resultCount) { + RankVector rank = new RankVector(1.d / domainsById.size()); + + int iter_max = 100; + for (int i = 0; i < iter_max; i++) { + RankVector newRank = createNewRankVector(rank); + + double oldNorm = rank.norm(); + double newNorm = newRank.norm(); + double dNorm = oldNorm - newNorm ; + + if (i < iter_max-1) { + adjustRankVector(newRank, dNorm, oldNorm); + } + + rank = newRank; + } + + return rank.getRanking(weight, resultCount); + } + + abstract RankVector createNewRankVector(RankVector rank); + + public boolean includeInRanking(DomainData data) { + if (data.isAlias()) + return false; + if (data.isSpecial()) + return false; + if (data.isSocialMedia()) + return false; + if (data.knownUrls > maxKnownUrls) + return false; + + return true; + } + + public void setMaxKnownUrls(int maxKnownUrls) { + this.maxKnownUrls = maxKnownUrls; + } + + public class RankVector { + private final double[] rank; + public RankVector(double defaultValue) { + rank = new double[domainIndexToId.size()]; + if (defaultValue != 0.) { + Arrays.fill(rank, defaultValue); + } + } + + public void set(int id, double value) { + rank[id] = value; + } + + public void increment(int id, double value) { + rank[id] += value; + } + + public double get(int id) { + if (id >= rank.length) return 0.; + + return rank[id]; + } + + public double norm() { + double v = 0.; + for (int i = 0; i < rank.length; i++) { + if (rank[i] > 0) { v+=rank[i]; } + else { v -= rank[i]; } + } + return v; + } + + public double norm(RankVector other) { + double v = 0.; + for (int i = 0; i < rank.length; i++) { + double dv = rank[i] - other.get(i); + + if (dv > 0) { v+=dv; } + else { v -= dv; } + } + return v; + } + + public TIntList getRanking(IntToDoubleFunction other, int numResults) { + TIntArrayList list = new TIntArrayList(numResults); + + Comparator comparator = Comparator.comparing(i -> Math.sqrt(other.applyAsDouble(domainIdToIndex.get(i)) * rank[i])); + + IntStream.range(0, rank.length) + .boxed() + .sorted(comparator.reversed()) + .map(domainIndexToId::get) + .limit(numResults) + .forEach(list::add); + + return list; + } + + public TIntList getRanking(int numResults) { + if (numResults < 0) { + numResults = domainIdToIndex.size(); + } + if (numResults >= rank.length) { + numResults = rank.length; + } + + TIntArrayList list = new TIntArrayList(numResults); + + int[] nodes = new int[rank.length]; + Arrays.setAll(nodes, i->i); + IntComparator comp = (i,j) -> (int) Math.signum(rank[j] - rank[i]); + IntArrays.quickSort(nodes, comp); + + int i; + + for (i = 0; i < numResults; i++) { + int id = domainIndexToId.get(nodes[i]); + + if (includeInRanking(domainsById.get(id))) + list.add(id); + } + + for (; i < nodes.length && domainsById.size() < numResults; i++) { + int id = domainIndexToId.get(nodes[i]); + + if (includeInRanking(domainsById.get(id))) + list.add(id); + } + + + return list; + } + + + public void incrementAll(double v) { + for (int i = 0; i < rank.length; i++) { + rank[i]+=v; + } + } + + int size() { + return domainsById.size(); + } + } + + @Data + @AllArgsConstructor + static class DomainData { + public final int id; + public final String name; + private int alias; + private int state; + public final int knownUrls; + public boolean peripheral; + + public int resolveAlias() { + if (alias == 0) return id; + return alias; + } + + public boolean isAlias() { + return alias != 0; + } + + public boolean isSpecial() { + return EdgeDomainIndexingState.SPECIAL.code == state; + } + + public boolean isSocialMedia() { + return EdgeDomainIndexingState.SOCIAL_MEDIA.code == state; + } + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/old/OldReversePageRankV2.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/old/OldReversePageRankV2.java new file mode 100644 index 00000000..54b88edc --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/old/OldReversePageRankV2.java @@ -0,0 +1,261 @@ +package nu.marginalia.wmsa.edge.index.service.util.ranking.old; + + +import com.zaxxer.hikari.HikariDataSource; +import gnu.trove.list.TIntList; +import gnu.trove.list.array.TIntArrayList; +import gnu.trove.map.hash.TIntDoubleHashMap; +import gnu.trove.map.hash.TIntObjectHashMap; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.SQLException; +import java.util.*; + +public class OldReversePageRankV2 { + + private final TIntObjectHashMap domains = new TIntObjectHashMap<>(); + private final TIntObjectHashMap linkData = new TIntObjectHashMap<>(); + private final TIntObjectHashMap reverseLinkData = new TIntObjectHashMap<>(); + private final Logger logger = LoggerFactory.getLogger(getClass()); + + public Set originDomains = new HashSet<>(); + public Set originDomainIds = new HashSet<>(); + + public static void main(String... args) throws IOException { + new OldReversePageRankV2( +// "wiki.xxiivv.com", +// "stpeter.im", +// "datagubbe.se", "midnight.pub", +// "www.gameboomers.com", +// "www.wild-seven.org", "iocane-powder.net", "www.doujinshi.org", "ohmydarling.org", +// "lobste.rs", +// "dataswamp.org", "www.ohtori.nu", +// "lukesmith.xyz", "internetgirlfriend.club", +// "tilde.town", "tilde.team", +// "felix.plesoianu.ro", +// "www.neustadt.fr", + "memex.marginalia.nu" + ); + } + + public OldReversePageRankV2(String... seedDomains) throws IOException { + loadDataFromFile(); + + long start = System.currentTimeMillis(); + for (int i = 0; i < 100; i++) { + if (domains.contains(i)) { + int[] ids = pageRank(10).toArray(); + System.out.printf("%d %d\n", i, ids.length); + } +// Arrays.stream(ids).mapToObj(domains::get).map(data -> +// String.format("%3d %2.2f %s", Optional.ofNullable(reverseLinkData.get(data.id)).map(TIntArrayList::size).orElse(0), data.quality, data.name) +// ).forEach(System.out::println); + } + long end = System.currentTimeMillis(); + System.out.printf("%2.2f", (end - start)/1000.0); + } + + public OldReversePageRankV2(HikariDataSource dataSource) throws IOException { + originDomains.add("memex.marginalia.nu"); + + try (var conn = dataSource.getConnection()) { + try (var stmt = conn.prepareStatement("SELECT ID,INDEXED,STATE FROM EC_DOMAIN WHERE INDEXED>1 AND STATE>=0 AND QUALITY_RAW>=-10")) { + stmt.setFetchSize(10000); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + domains.put(rsp.getInt(1), new DomainData("", 0.0, rsp.getInt(1), rsp.getInt(2), rsp.getInt(3))); + } + } + try (var stmt = conn.prepareStatement("SELECT SOURCE_DOMAIN_ID, DEST_DOMAIN_ID FROM EC_DOMAIN_LINK")) { + stmt.setFetchSize(10000); + + var rsp = stmt.executeQuery(); + + while (rsp.next()) { + int src = rsp.getInt(1); + int dst = rsp.getInt(2); + if (domains.contains(src) && domains.contains(dst) && domains.get(src).quality >= -5) { + if (!linkData.contains(src)) { + linkData.put(src, new TIntArrayList()); + } + linkData.get(src).add(dst); + } + } + } + + try (var stmt = conn.prepareStatement("SELECT ID FROM EC_DOMAIN WHERE URL_PART=?")) { + stmt.setFetchSize(10000); + + for (var seed : this.originDomains) { + stmt.setString(1, seed); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + originDomainIds.add(rsp.getInt(1)); + } + } + } + + } catch (SQLException throwables) { + logger.error("SQL error", throwables); + } + + } + + public int size() { + return domains.size(); + } + + public TIntList pageRank(int resultCount) { + RankVector rank = new RankVector(1.d / domains.size()); + + for (int i = 0; i < 100; i++) { + RankVector newRank = createNewRankVector(rank); + + double oldNorm = rank.norm(); + double newNorm = newRank.norm(); + double dNorm = oldNorm - newNorm ; + originDomainIds.forEach(id -> newRank.increment(id, dNorm/oldNorm)); +// newRank.increment(14880, dNorm/rank.norm()); + rank = newRank; + } + + for (var id : originDomainIds) { + rank.increment(id, -1); + } + + return rank.getRanking(resultCount); + } + + @NotNull + private RankVector createNewRankVector(RankVector rank) { + + final TIntArrayList empty = new TIntArrayList(); + + double rankNorm = rank.norm(); + RankVector newRank = new RankVector(0); + + for (DomainData domain : domains.values(new DomainData[domains.size()])) { + + var links = Optional.ofNullable(linkData.get(domain.id)).orElse(empty); + if (links.size() > 0) { + double newRankValue = 0; + for (int linkedDomain : links.toArray()) { + newRankValue += rank.get(linkedDomain) / links.size(); + } + + newRank.set(domain.id, 0.85*newRankValue/rankNorm); + } + } + return newRank; + } + + private void loadDataFromFile() throws IOException { + + try (var str = Files.lines(Path.of("/home/vlofgren/Work/data-domains.txt"))) { + str.map(DomainData::new) + .filter(domain -> domain.indexed>1) + .filter(domain -> domain.state>=1) + .peek(domain -> { + if (originDomains.contains(domain.name)) { + originDomainIds.add(domain.id); + } + }) + .forEach(data -> domains.put(data.id, data)); + } + + try (var str = Files.lines(Path.of("/home/vlofgren/Work/data-links.txt"))) { + str.map(s->s.split("\\s+")).forEach(bits -> { + + int src = Integer.parseInt(bits[0]); + int dst = Integer.parseInt(bits[1]); + + if (domains.contains(src) && domains.contains(dst) && domains.get(src).quality >= -5) { + if (!linkData.contains(src)) { + linkData.put(src, new TIntArrayList()); + } + linkData.get(src).add(dst); + } + + + if (!reverseLinkData.contains(dst)) { + reverseLinkData.put(dst, new TIntArrayList()); + } + reverseLinkData.get(dst).add(src); + }); + } + } + + private class RankVector { + private final TIntDoubleHashMap rank; + private final double defaultValue; + public RankVector(double defaultValue) { + rank = new TIntDoubleHashMap(domains.size(), 0.75f, -1, defaultValue); + this.defaultValue = defaultValue; + } + + public void set(int id, double value) { + rank.put(id, value); + } + + + public void increment(int id, double value) { + rank.adjustOrPutValue(id, value, value); + } + + public double get(int id) { + return rank.get(id); + } + + public double norm() { + if (rank.isEmpty()) { + return defaultValue * domains.size(); + } + return Arrays.stream(rank.values()).map(Math::abs).sum(); + } + + public double norm(RankVector other) { + return Arrays.stream(rank.keys()).mapToDouble(k -> Math.abs(rank.get(k) - other.get(k))).sum(); + } + + public TIntList getRanking(int numResults) { + TIntArrayList list = new TIntArrayList(numResults); + + Comparator comparator = Comparator.comparing(e -> rank.get(e.id)); + + domains.valueCollection().stream() + .sorted(comparator.reversed()) + .map(DomainData::getId) + .limit(numResults) + .forEach(list::add); + + return list; + } + + } + @Data @AllArgsConstructor + static class DomainData { + + public DomainData(String str) { + String[] parts = str.split("\\s+"); + + id = Integer.parseInt(parts[0]); + quality = Double.parseDouble(parts[1]); + name = parts[2]; + indexed = Integer.parseInt(parts[3]); + state = Integer.parseInt(parts[4]); + } + public final String name; + public final double quality; + public final int id; + public final int indexed; + public final int state; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/old/StandardPageRank.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/old/StandardPageRank.java new file mode 100644 index 00000000..613a8aa2 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/old/StandardPageRank.java @@ -0,0 +1,270 @@ +package nu.marginalia.wmsa.edge.index.service.util.ranking.old; + + +import com.zaxxer.hikari.HikariDataSource; +import gnu.trove.list.TIntList; +import gnu.trove.list.array.TIntArrayList; +import gnu.trove.map.hash.TIntDoubleHashMap; +import gnu.trove.map.hash.TIntObjectHashMap; +import gnu.trove.set.hash.TIntHashSet; +import lombok.AllArgsConstructor; +import lombok.Data; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.SQLException; +import java.util.*; +import java.util.function.IntToDoubleFunction; + +public class StandardPageRank { + + private final TIntObjectHashMap domains = new TIntObjectHashMap<>(); + private final TIntObjectHashMap linkData = new TIntObjectHashMap<>(); + private final TIntObjectHashMap reverseLinkData = new TIntObjectHashMap<>(); + private final Logger logger = LoggerFactory.getLogger(getClass()); + + public Set originDomains = new HashSet(); + public Set originDomainIds = new HashSet<>(); + + public StandardPageRank(IntToDoubleFunction weight, String... seedDomains) throws IOException { + originDomains.addAll(Arrays.asList(seedDomains)); + loadDataFromFile(); + + int[] ids = pageRank(weight, 1000).toArray(); + Arrays.stream(ids).mapToObj(domains::get).map(data -> + String.format("%3d %2.2f %s", Optional.ofNullable(reverseLinkData.get(data.id)).map(TIntArrayList::size).orElse(0), data.quality, data.name) + ).forEach(System.out::println); + } + + public String domainNameFromId(int id) { + return domains.get(id).name; + } + + public StandardPageRank(HikariDataSource dataSource, String... origins) throws IOException { + originDomains.addAll(Arrays.asList(origins)); + + try (var conn = dataSource.getConnection()) { + try (var stmt = conn.prepareStatement("SELECT ID,INDEXED,STATE,URL_PART FROM EC_DOMAIN WHERE INDEXED>1 AND STATE>=0 AND QUALITY>=-10")) { + stmt.setFetchSize(10000); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + domains.put(rsp.getInt(1), new DomainData(rsp.getInt(1), rsp.getString(4), rsp.getInt(2), rsp.getInt(3), 0)); + } + } + try (var stmt = conn.prepareStatement("SELECT SOURCE_DOMAIN_ID, DEST_DOMAIN_ID FROM EC_DOMAIN_LINK")) { + stmt.setFetchSize(10000); + + var rsp = stmt.executeQuery(); + + while (rsp.next()) { + int src = rsp.getInt(1); + int dst = rsp.getInt(2); + + if (domains.contains(src) && domains.contains(dst) && domains.get(src).quality >= -5) { + if (!linkData.contains(src)) { + linkData.put(src, new TIntArrayList()); + } + linkData.get(src).add(dst); + + if (!reverseLinkData.contains(dst)) { + reverseLinkData.put(dst, new TIntArrayList()); + } + reverseLinkData.get(dst).add(src); + } + } + } + + try (var stmt = conn.prepareStatement("SELECT ID FROM EC_DOMAIN WHERE URL_PART=?")) { + for (var seed : this.originDomains) { + stmt.setString(1, seed); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + originDomainIds.add(rsp.getInt(1)); + } + } + } + + } catch (SQLException throwables) { + logger.error("SQL error", throwables); + } + + } + + public int size() { + return domains.size(); + } + + public TIntList pageRank(IntToDoubleFunction weight, int resultCount) { + RankVector rank = new RankVector(1.d / domains.size()); + + int iter_max = 100; + for (int i = 0; i < iter_max; i++) { + RankVector newRank = createNewRankVector(rank); + + double oldNorm = rank.norm(); + double newNorm = newRank.norm(); + double dNorm = oldNorm - newNorm; + if (i < iter_max-1) { + originDomainIds.forEach(id -> newRank.increment(id, dNorm/originDomainIds.size())); + newRank.incrementAll(0.14*dNorm/rank.size()); + } + logger.debug("{} {} {}", dNorm, newNorm, rank.norm(newRank)); + rank = newRank; + } + + + return rank.getRanking(weight, resultCount); + } + + @NotNull + private RankVector createNewRankVector(RankVector rank) { + + final TIntArrayList empty = new TIntArrayList(); + + double rankNorm = rank.norm(); + RankVector newRank = new RankVector(0); + + for (DomainData domain : domains.valueCollection()) { + + var links = Optional.ofNullable(reverseLinkData.get(domain.id)).orElse(empty); + double newRankValue = 0; + if (links.size() > 0) { + for (int linkedDomain : links.toArray()) { + newRankValue += rank.get(linkedDomain) / linkData.get(linkedDomain).size(); + } + } + + newRank.set(domain.id, 0.85 * newRankValue); + } + return newRank; + } + + private void loadDataFromFile() throws IOException { + + try (var str = Files.lines(Path.of("/home/vlofgren/Work/data-domains.txt"))) { + str.map(DomainData::new) + .filter(domain -> domain.indexed>1) + .filter(domain -> domain.quality>=0.1) + .peek(domain -> { + if (originDomains.contains(domain.name)) { + originDomainIds.add(domain.id); + } + }) + .forEach(data -> domains.put(data.id, data)); + } + + try (var str = Files.lines(Path.of("/home/vlofgren/Work/data-links.txt"))) { + str.map(s->s.split("\\s+")).forEach(bits -> { + + int src = Integer.parseInt(bits[0]); + int dst = Integer.parseInt(bits[1]); + + if (domains.contains(src) && domains.contains(dst) && domains.get(src).quality >= -5) { + if (!linkData.contains(src)) { + linkData.put(src, new TIntArrayList()); + } + linkData.get(src).add(dst); + + if (!reverseLinkData.contains(dst)) { + reverseLinkData.put(dst, new TIntArrayList()); + } + reverseLinkData.get(dst).add(src); + } + }); + } + + TIntHashSet deadEnds = new TIntHashSet(domains.size()); + } + + private class RankVector { + private final TIntDoubleHashMap rank; + private final double defaultValue; + public RankVector(double defaultValue) { + rank = new TIntDoubleHashMap(domains.size(), 0.75f, -1, defaultValue); + this.defaultValue = defaultValue; + } + + public void set(int id, double value) { + rank.put(id, value); + } + + public void increment(int id, double value) { + rank.adjustOrPutValue(id, value, value); + } + + public double get(int id) { + return rank.get(id); + } + + public double norm() { + if (rank.isEmpty()) { + return defaultValue * domains.size(); + } + return Arrays.stream(rank.values()).map(Math::abs).sum(); + } + + public double norm(RankVector other) { + return Arrays.stream(rank.keys()).mapToDouble(k -> Math.abs(rank.get(k) - other.get(k))).sum(); + } + + public TIntList getRanking(IntToDoubleFunction other, int numResults) { + TIntArrayList list = new TIntArrayList(numResults); + + Comparator comparator = Comparator.comparing(e -> Math.sqrt(other.applyAsDouble(e.id) * rank.get(e.id))); + + domains.valueCollection().stream() + .sorted(comparator.reversed()) + .map(DomainData::getId) + .limit(numResults) + .forEach(list::add); + + return list; + } + + public TIntList getRanking2(int numResults) { + TIntArrayList list = new TIntArrayList(numResults); + + Comparator comparator = Comparator.comparing(e -> rank.get(e.id)); + + domains.valueCollection().stream() + .sorted(comparator.reversed()) + .map(DomainData::getId) + .limit(numResults) + .forEach(list::add); + + return list; + } + + public void incrementAll(double v) { + rank.transformValues(oldv -> oldv + v); + } + + int size() { + return domains.size(); + } + } + @Data @AllArgsConstructor + static class DomainData { + + public DomainData(String str) { + String[] parts = str.split("\\s+"); + + id = Integer.parseInt(parts[0]); + name = parts[2]; + indexed = Integer.parseInt(parts[3]); + state = Integer.parseInt(parts[4]); + quality = Double.parseDouble(parts[5]); + } + public final int id; + public final String name; + public final int indexed; + public final int state; + public double quality; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/DedupTool.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/DedupTool.java new file mode 100644 index 00000000..9e0423cd --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/DedupTool.java @@ -0,0 +1,89 @@ +package nu.marginalia.wmsa.edge.index.service.util.ranking.tool; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.ToString; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import org.mariadb.jdbc.Driver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class DedupTool { + + private static final Logger logger = LoggerFactory.getLogger(DedupTool.class); + + public Set originDomains = new HashSet<>(); + public Set originDomainIds = new HashSet<>(); + public long domainIdMax = -1; + public int domainCount; + private volatile static int rankMax; + + public int maxId() { + return (int) domainIdMax; + } + public int domainCount() { + return domainCount; + } + + static LinkedBlockingQueue uploadQueue = new LinkedBlockingQueue<>(10); + volatile static boolean running = true; + + @AllArgsConstructor @ToString @Getter + static class Data { + String url; + int id; + String domain; + } + + @SneakyThrows + public static void main(String... args) throws IOException { + Driver driver = new Driver(); + var ds = new DatabaseModule().provideConnection(); + + Map>> domainToHashToUrl = new HashMap<>(); + + try (var conn = ds.getConnection(); + var fetchStmt = conn.prepareStatement("SELECT URL_TOP_DOMAIN_ID,DATA_HASH,URL,EC_URL.ID,EC_DOMAIN.URL_PART FROM EC_URL INNER JOIN EC_DOMAIN ON EC_DOMAIN.ID=DOMAIN_ID WHERE DATA_HASH IS NOT NULL"); + var updateStmt = conn.prepareStatement("UPDATE EC_URL SET STATE='redirect' WHERE ID=?"); + + ) { + fetchStmt.setFetchSize(10_000); + var rsp = fetchStmt.executeQuery(); + while (rsp.next()) { + domainToHashToUrl.computeIfAbsent(rsp.getInt(1), i -> new HashMap<>()) + .computeIfAbsent(rsp.getInt(2), i -> new ArrayList<>()).add(new Data(rsp.getString(3), rsp.getInt(4), rsp.getString(5))); + } + + + List updateIds = new ArrayList<>(); + + domainToHashToUrl.forEach((domain, hashes) -> { + hashes.forEach((hash, urls) -> { + if (urls.size() > 1) { + Comparator c = Comparator.comparing(d -> d.domain.length()); + var urls2 = urls.stream().sorted(c.thenComparing(d -> d.url.length())) + .collect(Collectors.partitioningBy(d -> d.url.endsWith("/"))); + + Stream + .concat(urls2.get(true).stream(),urls2.get(false).stream()).skip(1) + .map(Data::getId) + .forEach(updateIds::add); + } + }); + }); + + for (int id : updateIds) { + updateStmt.setInt(1, id); + updateStmt.executeUpdate(); + } + } + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/PerusePageRankV2.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/PerusePageRankV2.java new file mode 100644 index 00000000..7f525daf --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/PerusePageRankV2.java @@ -0,0 +1,340 @@ +package nu.marginalia.wmsa.edge.index.service.util.ranking.tool; + + +import com.zaxxer.hikari.HikariDataSource; +import gnu.trove.list.TIntList; +import gnu.trove.list.array.TIntArrayList; +import gnu.trove.map.hash.TIntDoubleHashMap; +import gnu.trove.map.hash.TIntIntHashMap; +import gnu.trove.map.hash.TIntObjectHashMap; +import gnu.trove.set.hash.TIntHashSet; +import it.unimi.dsi.fastutil.ints.IntArrays; +import it.unimi.dsi.fastutil.ints.IntComparator; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDomainBlacklistImpl; +import nu.marginalia.wmsa.edge.index.service.util.ranking.RankingAlgorithm; +import org.jetbrains.annotations.NotNull; +import org.mariadb.jdbc.Driver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.function.IntToDoubleFunction; +import java.util.stream.IntStream; + +public class PerusePageRankV2 { + + final TIntObjectHashMap domainsById = new TIntObjectHashMap<>(); + final TIntIntHashMap domainIndexToId = new TIntIntHashMap(); + final TIntIntHashMap domainIdToIndex = new TIntIntHashMap(); + + private final TIntHashSet spamDomains; + private final HikariDataSource dataSource; + + TIntArrayList[] linkDataSrc2Dest; + TIntArrayList[] linkDataDest2Src; + + private static boolean getNames = true; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + static LinkedBlockingQueue uploadQueue = new LinkedBlockingQueue<>(10); + volatile static boolean running = true; + + public int indexMax() { + return domainIndexToId.size(); + } + + public int getDomainId(int idx) { + return domainIndexToId.get(idx); + } + + @SneakyThrows + public static void main(String... args) throws IOException { + org.mariadb.jdbc.Driver driver = new Driver(); + var conn = new DatabaseModule().provideConnection(); + var rank = new PerusePageRankV2(conn); + + long start = System.currentTimeMillis(); + var uploader = new Thread(() -> uploadThread(conn)); + uploader.start(); + + IntStream.range(0, rank.indexMax()).parallel().forEach(i -> { + int[] ids = rank.pageRank(i, 25).toArray(); + try { + uploadQueue.put(new LinkAdjacencies(rank.getDomainId(i), ids)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + + long end = System.currentTimeMillis(); + running = false; + uploader.join(); + System.out.printf("%2.2f", (end - start)/1000.0); + } + + @AllArgsConstructor + static class LinkAdjacencies { + public final int id; + public final int[] neighbors; + }; + + public static void uploadThread(HikariDataSource dataSource) { + try (var conn = dataSource.getConnection()) { + try (var stmt = conn.prepareStatement("INSERT INTO EC_DOMAIN_NEIGHBORS(DOMAIN_ID, NEIGHBOR_ID, ADJ_IDX) VALUES (?,?,?) ON DUPLICATE KEY UPDATE NEIGHBOR_ID=VALUES(NEIGHBOR_ID)")) { + while (running || (!running && !uploadQueue.isEmpty())) { + var job = uploadQueue.take(); + for (int i = 0; i < job.neighbors.length; i++) { + stmt.setInt(1, job.id); + stmt.setInt(2, job.neighbors[i]); + stmt.setInt(3, i); + stmt.addBatch(); + } + stmt.executeBatch(); + } + } + } catch (SQLException | InterruptedException throwables) { + throwables.printStackTrace(); + } + } + + public PerusePageRankV2(HikariDataSource dataSource) throws IOException { + var blacklist = new EdgeDomainBlacklistImpl(dataSource); + spamDomains = blacklist.getSpamDomains(); + this.dataSource = dataSource; + + try (var conn = dataSource.getConnection()) { + String s; + if (getNames) { + s = "SELECT EC_DOMAIN.ID,URL_PART,DOMAIN_ALIAS FROM EC_DOMAIN INNER JOIN DOMAIN_METADATA ON EC_DOMAIN.ID=DOMAIN_METADATA.ID WHERE ((INDEXED>1 AND STATE >= 0) OR (INDEXED=1 AND VISITED_URLS=KNOWN_URLS AND GOOD_URLS>0)) AND QUALITY_RAW>=-20 GROUP BY EC_DOMAIN.ID"; + } + else { + s = "SELECT EC_DOMAIN.ID,\"\",DOMAIN_ALIAS FROM EC_DOMAIN ON EC_DOMAIN.ID=DOMAIN_METADATA.ID WHERE ((INDEXED>1 AND STATE >= 0) OR (INDEXED=1 AND VISITED_URLS=KNOWN_URLS AND GOOD_URLS>0)) AND QUALITY_RAW>=-20 GROUP BY EC_DOMAIN.ID"; + } + try (var stmt = conn.prepareStatement(s)) { + stmt.setFetchSize(10000); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + int id = rsp.getInt(1); + if (!spamDomains.contains(id)) { + + domainsById.put(id, new DomainData(id, rsp.getString(2), rsp.getInt(3), false)); + + domainIndexToId.put(domainIndexToId.size(), id); + domainIdToIndex.put(id, domainIdToIndex.size()); + } + } + } + + + linkDataSrc2Dest = new TIntArrayList[domainIndexToId.size()]; + linkDataDest2Src = new TIntArrayList[domainIndexToId.size()]; + + try (var stmt = conn.prepareStatement("SELECT SOURCE_DOMAIN_ID, DEST_DOMAIN_ID FROM EC_DOMAIN_LINK")) { + stmt.setFetchSize(10000); + + var rsp = stmt.executeQuery(); + + while (rsp.next()) { + int src = rsp.getInt(1); + int dst = rsp.getInt(2); + + if (src == dst) continue; + + if (domainsById.contains(src) && domainsById.contains(dst)) { + + int srcIdx = domainIdToIndex.get(src); + int dstIdx = domainIdToIndex.get(domainsById.get(dst).resolveAlias()); + + if (linkDataSrc2Dest[srcIdx] == null) { + linkDataSrc2Dest[srcIdx] = new TIntArrayList(); + } + linkDataSrc2Dest[srcIdx].add(dstIdx); + + if (linkDataDest2Src[dstIdx] == null) { + linkDataDest2Src[dstIdx] = new TIntArrayList(); + } + linkDataDest2Src[dstIdx].add(srcIdx); + } + } + } + + } catch (SQLException throwables) { + logger.error("SQL error", throwables); + } + + } + + public TIntList pageRank(int origin, int resultCount) { + RankVector rank = new RankVector(1.d / domainsById.size()); + + int iter_max = 10; + for (int i = 0; i < iter_max; i++) { + RankVector newRank = createNewRankVector(rank); + + double oldNorm = rank.norm(); + double newNorm = newRank.norm(); + double dNorm = oldNorm - newNorm ; + + newRank.increment(origin, dNorm/oldNorm); + + rank = newRank; + } + + rank.increment(origin, -1); + + return rank.getRanking(resultCount); + } + + @NotNull + private RankVector createNewRankVector(RankVector rank) { + + double rankNorm = rank.norm(); + RankVector newRank = new RankVector(0); + + for (int domainId = 0; domainId < domainIndexToId.size(); domainId++) { + + var links = linkDataSrc2Dest[domainId]; + double newRankValue = 0; + + if (links != null && links.size() > 0) { + + + for (int j = 0; j < links.size(); j++) { + var revLinks = linkDataDest2Src[links.getQuick(j)]; + newRankValue += rank.get(links.getQuick(j)) / revLinks.size(); + } + } + + newRank.set(domainId, 0.85*newRankValue/rankNorm); + } + + return newRank; + } + + public class RankVector { + private final double[] rank; + public RankVector(double defaultValue) { + rank = new double[domainIndexToId.size()]; + if (defaultValue != 0.) { + Arrays.fill(rank, defaultValue); + } + } + + public void set(int id, double value) { + rank[id] = value; + } + + public void increment(int id, double value) { + rank[id] += value; + } + + public double get(int id) { + if (id >= rank.length) return 0.; + + return rank[id]; + } + + public double norm() { + double v = 0.; + for (int i = 0; i < rank.length; i++) { + if (rank[i] > 0) { v+=rank[i]; } + else { v -= rank[i]; } + } + return v; + } + + public double norm(RankingAlgorithm.RankVector other) { + double v = 0.; + for (int i = 0; i < rank.length; i++) { + double dv = rank[i] - other.get(i); + + if (dv > 0) { v+=dv; } + else { v -= dv; } + } + return v; + } + + public TIntList getRanking(IntToDoubleFunction other, int numResults) { + TIntArrayList list = new TIntArrayList(numResults); + + Comparator comparator = Comparator.comparing(i -> Math.sqrt(other.applyAsDouble(domainIdToIndex.get(i)) * rank[i])); + + IntStream.range(0, rank.length) + .boxed() + .sorted(comparator.reversed()) + .map(domainIndexToId::get) + .limit(numResults) + .forEach(list::add); + + return list; + } + + public TIntList getRanking(int numResults) { + if (numResults < 0) { + numResults = domainIdToIndex.size(); + } + TIntArrayList list = new TIntArrayList(numResults); + + int[] nodes = new int[rank.length]; + Arrays.setAll(nodes, i->i); + IntComparator comp = (i,j) -> (int) Math.signum(rank[j] - rank[i]); + IntArrays.quickSort(nodes, comp); + + int i; + + for (i = 0; i < numResults; i++) { + int id = domainIndexToId.get(nodes[i]); + + if (!domainsById.get(id).isAlias()) + list.add(id); + } + + for (; i < nodes.length && domainsById.size() < numResults; i++) { + int id = domainIndexToId.get(nodes[i]); + + if (!domainsById.get(id).isAlias()) + list.add(id); + } + + + return list; + } + + public void incrementAll(double v) { + for (int i = 0; i < rank.length; i++) { + rank[i]+=v; + } + } + + int size() { + return domainsById.size(); + } + } + + @Data + @AllArgsConstructor + static class DomainData { + public final int id; + public final String name; + private int alias; + + public int resolveAlias() { + if (alias == 0) return id; + return alias; + } + + public boolean isAlias() { + return alias != 0; + } + + public boolean peripheral; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/TestAcademiaRankTool.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/TestAcademiaRankTool.java new file mode 100644 index 00000000..638e3f6d --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/TestAcademiaRankTool.java @@ -0,0 +1,30 @@ +package nu.marginalia.wmsa.edge.index.service.util.ranking.tool; + +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.edge.index.service.util.ranking.AcademiaRank; +import org.mariadb.jdbc.Driver; + +import java.io.IOException; + +public class TestAcademiaRankTool { + + @SneakyThrows + public static void main(String... args) throws IOException { + Driver driver = new Driver(); + var conn = new DatabaseModule().provideConnection(); + + var rank = new AcademiaRank(new DatabaseModule().provideConnection(), "www.perseus.tufts.edu", "xroads.virginia.edu"); + var res = rank.getResult(); + + try (var c = conn.getConnection(); var stmt = c.prepareStatement("SELECT URL_PART FROM EC_DOMAIN WHERE ID=?")) { + for (int i = 0; i < Math.min(res.size(), 100); i++) { + stmt.setInt(1, res.getQuick(i)); + var rsp = stmt.executeQuery(); + while (rsp.next()) + System.out.println(rsp.getString(1)); + } + } + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/UpdateDomainRanksTool.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/UpdateDomainRanksTool.java new file mode 100644 index 00000000..a78dae31 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/UpdateDomainRanksTool.java @@ -0,0 +1,95 @@ +package nu.marginalia.wmsa.edge.index.service.util.ranking.tool; + +import com.zaxxer.hikari.HikariDataSource; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.edge.index.service.util.ranking.BuggyStandardPageRank; +import org.mariadb.jdbc.Driver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.LinkedBlockingQueue; + +public class UpdateDomainRanksTool { + + private static final Logger logger = LoggerFactory.getLogger(UpdateDomainRanksTool.class); + + public Set originDomains = new HashSet<>(); + public Set originDomainIds = new HashSet<>(); + public long domainIdMax = -1; + public int domainCount; + private volatile static int rankMax; + + public int maxId() { + return (int) domainIdMax; + } + public int domainCount() { + return domainCount; + } + + static LinkedBlockingQueue uploadQueue = new LinkedBlockingQueue<>(10); + volatile static boolean running = true; + + @SneakyThrows + public static void main(String... args) throws IOException { + org.mariadb.jdbc.Driver driver = new Driver(); + var conn = new DatabaseModule().provideConnection(); + + long start = System.currentTimeMillis(); + var uploader = new Thread(() -> uploadThread(conn), "Uploader"); + + logger.info("Ranking"); + var spr = new BuggyStandardPageRank(new DatabaseModule().provideConnection(),"memex.marginalia.nu"); + + rankMax = spr.size()*2; + uploader.start(); + + spr.pageRankWithPeripheralNodes(rankMax, false).forEach(i -> { + try { + uploadQueue.put(i); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return true; + }); + + long end = System.currentTimeMillis(); + running = false; + uploader.join(); + + logger.info("Done in {}", (end - start)/1000.0); + } + + public static void uploadThread(HikariDataSource dataSource) { + int i = 0; + + try (var conn = dataSource.getConnection()) { + logger.info("Resetting rank"); + try (var stmt = conn.prepareStatement("UPDATE EC_DOMAIN SET RANK=1")) { + stmt.executeUpdate(); + } + + logger.info("Updating ranks"); + try (var stmt = conn.prepareStatement("UPDATE EC_DOMAIN SET RANK=? WHERE ID=?")) { + while (running || (!running && !uploadQueue.isEmpty())) { + var job = uploadQueue.take(); + stmt.setDouble(1, i++ / (double) rankMax); + stmt.setInt(2, job); + stmt.executeUpdate(); + } + } + + logger.info("Recalculating quality"); + try (var stmt = conn.prepareStatement("UPDATE EC_DOMAIN SET QUALITY=-5*RANK+IF(RANK=1,RANK*GREATEST(QUALITY_RAW,QUALITY_ORIGINAL)/2, 0)")) { + stmt.executeUpdate(); + } + + } catch (SQLException | InterruptedException throwables) { + throwables.printStackTrace(); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/UpdateDomainRanksTool2.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/UpdateDomainRanksTool2.java new file mode 100644 index 00000000..4ac2600d --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/index/service/util/ranking/tool/UpdateDomainRanksTool2.java @@ -0,0 +1,105 @@ +package nu.marginalia.wmsa.edge.index.service.util.ranking.tool; + +import com.zaxxer.hikari.HikariDataSource; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.edge.index.service.util.ranking.BetterReversePageRank; +import org.mariadb.jdbc.Driver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.LinkedBlockingQueue; + +public class UpdateDomainRanksTool2 { + + private static final Logger logger = LoggerFactory.getLogger(UpdateDomainRanksTool2.class); + + public Set originDomains = new HashSet<>(); + public Set originDomainIds = new HashSet<>(); + public long domainIdMax = -1; + public int domainCount; + private volatile static int rankMax; + + public int maxId() { + return (int) domainIdMax; + } + public int domainCount() { + return domainCount; + } + + static LinkedBlockingQueue uploadQueue = new LinkedBlockingQueue<>(10); + volatile static boolean running = true; + + @SneakyThrows + public static void main(String... args) throws IOException { + Driver driver = new Driver(); + var conn = new DatabaseModule().provideConnection(); + + long start = System.currentTimeMillis(); + var uploader = new Thread(() -> uploadThread(conn), "Uploader"); + + logger.info("Ranking"); + // "memex.marginalia.nu", "wiki.xxiivv.com", "bikobatanari.art", "sadgrl.online", "lileks.com", + // "www.rep.routledge.com", "www.personal.kent.edu", "xroads.virginia.edu", "classics.mit.edu", "faculty.washington.edu", "monadnock.net" + var rpr = new BetterReversePageRank(new DatabaseModule().provideConnection(), "memex.marginalia.nu", "bikobatanari.art", "sadgrl.online", "wiki.xxiivv.com", "%neocities.org"); +// var rpr = new BetterStandardPageRank(new DatabaseModule().provideConnection(), "%edu"); +// var spr = new BetterStandardPageRank(new DatabaseModule().provideConnection(), "memex.marginalia.nu"); + + var rankVector = rpr.pageRankVector(); + var norm = rankVector.norm(); + rankMax = rpr.size(); + uploader.start(); + + + rankMax = rpr.size(); + + + rpr.pageRankWithPeripheralNodes(rankMax, false).forEach(i -> { + try { + uploadQueue.put(i); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return true; + }); + + long end = System.currentTimeMillis(); + running = false; + uploader.join(); + + logger.info("Done in {}", (end - start)/1000.0); + } + + public static void uploadThread(HikariDataSource dataSource) { + int i = 0; + + try (var conn = dataSource.getConnection()) { + logger.info("Resetting rank"); + try (var stmt = conn.prepareStatement("UPDATE EC_DOMAIN SET RANK=1")) { + stmt.executeUpdate(); + } + + logger.info("Updating ranks"); + try (var stmt = conn.prepareStatement("UPDATE EC_DOMAIN SET RANK=? WHERE ID=?")) { + while (running || (!running && !uploadQueue.isEmpty())) { + var job = uploadQueue.take(); + stmt.setDouble(1, i++ / (double) rankMax); + stmt.setInt(2, job); + stmt.executeUpdate(); + } + } + + logger.info("Recalculating quality"); + try (var stmt = conn.prepareStatement("UPDATE EC_DOMAIN SET QUALITY=-5*RANK+IF(RANK=1,RANK*GREATEST(QUALITY_RAW,QUALITY_ORIGINAL)/2, 0)")) { + stmt.executeUpdate(); + } + + } catch (SQLException | InterruptedException throwables) { + throwables.printStackTrace(); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/BasicPageUploader.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/BasicPageUploader.java new file mode 100644 index 00000000..3408bd25 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/BasicPageUploader.java @@ -0,0 +1,57 @@ +package nu.marginalia.wmsa.edge.integration; + +import com.google.inject.Inject; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlFeature; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDao; +import nu.marginalia.wmsa.edge.index.client.EdgeIndexClient; +import nu.marginalia.wmsa.edge.integration.model.BasicDocumentData; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.EdgeHtmlStandard; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageWordSet; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlState; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlVisit; + +import java.util.EnumSet; + +public class BasicPageUploader { + private final EdgeDataStoreDao edgeStoreDao; + private final EdgeIndexClient indexClient; + + private final int features; + + @Inject + public BasicPageUploader(EdgeDataStoreDao edgeStoreDao, EdgeIndexClient indexClient, + EnumSet features) { + + this.edgeStoreDao = edgeStoreDao; + this.indexClient = indexClient; + this.features = HtmlFeature.encode(features); + + } + + public void upload(BasicDocumentData indexData) { + var url = indexData.getUrl(); + + edgeStoreDao.putUrl(-2, url); + edgeStoreDao.putUrlVisited(new EdgeUrlVisit(url, indexData.getHashCode(), -2., + indexData.getTitle(), + indexData.getDescription() + , "", + EdgeHtmlStandard.HTML5.toString(), + features, + indexData.wordCount, indexData.wordCount, EdgeUrlState.OK)); + edgeStoreDao.putLink(false, indexData.domainLinks); + + putWords(edgeStoreDao.getDomainId(url.domain).getId(), + edgeStoreDao.getUrlId(url).getId(), + -2, + indexData.words); + } + + void putWords(int didx, int idx, double quality, EdgePageWordSet wordsSet) { + indexClient.putWords(Context.internal(), new EdgeId<>(didx), new EdgeId<>(idx), quality, + wordsSet, 0).blockingSubscribe(); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/arxiv/ArxivParser.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/arxiv/ArxivParser.java new file mode 100644 index 00000000..ed6a656b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/arxiv/ArxivParser.java @@ -0,0 +1,29 @@ +package nu.marginalia.wmsa.edge.integration.arxiv; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import nu.marginalia.wmsa.edge.integration.arxiv.model.ArxivMetadata; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +public class ArxivParser { + private final Gson gson = new GsonBuilder().create(); + + public ArxivParser() { + + } + + public List parse(File jsonFile) throws IOException { + + List ret = new ArrayList<>(); + try (var lines = Files.lines(jsonFile.toPath())) { + lines.map(line -> gson.fromJson(line, ArxivMetadata.class)).forEach(ret::add); + } + + return ret; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/arxiv/model/ArxivMetadata.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/arxiv/model/ArxivMetadata.java new file mode 100644 index 00000000..ba6307a8 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/arxiv/model/ArxivMetadata.java @@ -0,0 +1,21 @@ +package nu.marginalia.wmsa.edge.integration.arxiv.model; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor @NoArgsConstructor +public class ArxivMetadata { + public String id; + public String submitter; + public String authors; + public String title; + @SerializedName("abstract") + public String _abstract; + + public String getAbstract() { + return _abstract; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/model/BasicDocumentData.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/model/BasicDocumentData.java new file mode 100644 index 00000000..905b7486 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/model/BasicDocumentData.java @@ -0,0 +1,22 @@ +package nu.marginalia.wmsa.edge.integration.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainLink; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageWordSet; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + + +@Data +@AllArgsConstructor +public class BasicDocumentData { + public final EdgeUrl url; + + public final String title; + public final String description; + public int hashCode; + + public final EdgePageWordSet words; + public final EdgeDomainLink[] domainLinks; + public final int wordCount; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/stackoverflow/StackOverflowPostProcessor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/stackoverflow/StackOverflowPostProcessor.java new file mode 100644 index 00000000..dcb29ace --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/stackoverflow/StackOverflowPostProcessor.java @@ -0,0 +1,83 @@ +package nu.marginalia.wmsa.edge.integration.stackoverflow; + +import com.google.inject.Inject; +import nu.marginalia.wmsa.edge.crawler.domain.LinkParser; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.DocumentKeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.integration.model.BasicDocumentData; +import nu.marginalia.wmsa.edge.integration.stackoverflow.model.StackOverflowPost; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainLink; +import org.apache.commons.lang3.StringUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class StackOverflowPostProcessor { + private final LinkParser linkParser = new LinkParser(); + + private final SentenceExtractor sentenceExtractor; + private final DocumentKeywordExtractor documentKeywordExtractor; + + @Inject + public StackOverflowPostProcessor(SentenceExtractor sentenceExtractor, DocumentKeywordExtractor documentKeywordExtractor) { + this.sentenceExtractor = sentenceExtractor; + this.documentKeywordExtractor = documentKeywordExtractor; + } + + public BasicDocumentData process(StackOverflowPost post) { + + final var docUrl = post.getUrl(); + final var doc = Jsoup.parseBodyFragment(""+post.getTitle()+"" + post.getFullBody()); + + EdgeDomainLink[] domainLinks = getDomainLinks(docUrl, doc); + + for (var tag : doc.getElementsByTag("code")) { + if (tag.text().length() > 32) { + tag.remove(); + } + } + + var dld = sentenceExtractor.extractSentences(doc); + var keywords = documentKeywordExtractor.extractKeywords(dld); + + keywords.get(IndexBlock.Meta).addJust("site:"+post.getUrl().domain); + keywords.get(IndexBlock.Words).addJust("site:"+post.getUrl().domain); + keywords.get(IndexBlock.Words).addJust("special:wikipedia"); + keywords.get(IndexBlock.Meta).addJust("special:wikipedia"); + keywords.get(IndexBlock.Meta).addJust("js:true"); + + String title = StringUtils.abbreviate(post.getTitle(), 255); + String description = StringUtils.abbreviate(Jsoup.parseBodyFragment(post.getJustBody()).text(), 255); + + return new BasicDocumentData(docUrl, title, description, post.fullBody.hashCode(), keywords, domainLinks, + dld.totalNumWords()); + + } + + private EdgeDomainLink[] getDomainLinks(EdgeUrl docUrl, Document doc) { + List links = new ArrayList<>(10); + + for (var tag : doc.getElementsByTag("a")) { + if (!tag.hasAttr("href")) { + continue; + } + String href = tag.attr("href"); + if (href.length()<10 || !href.contains(".") || !href.contains("://")) { + continue; + } + + linkParser.parseLink(docUrl, tag) + .filter(url -> !Objects.equals(docUrl.getDomain(), url.getDomain())) + .ifPresent(links::add); + } + + return links.stream().map(EdgeUrl::getDomain).map(domain -> new EdgeDomainLink(docUrl.domain, domain)) + .distinct().toArray(EdgeDomainLink[]::new); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/stackoverflow/StackOverflowPostsReader.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/stackoverflow/StackOverflowPostsReader.java new file mode 100644 index 00000000..17b88447 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/stackoverflow/StackOverflowPostsReader.java @@ -0,0 +1,123 @@ +package nu.marginalia.wmsa.edge.integration.stackoverflow; + +import gnu.trove.map.hash.TIntObjectHashMap; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.integration.stackoverflow.model.StackOverflowPost; +import nu.marginalia.wmsa.edge.integration.stackoverflow.model.StackOverflowQuestionData; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedList; +import java.util.function.Consumer; + +public class StackOverflowPostsReader extends DefaultHandler { + private static final int MAX_QUESTION_WINDOW_SIZE = 10_000; + + private final Thread runThread; + private final String postsFile; + private final EdgeDomain domain; + private final Consumer postConsumer; + + private Deque questionWindow = new LinkedList<>(); + private final TIntObjectHashMap questionsById = new TIntObjectHashMap<>(1_000_000); + + public StackOverflowPostsReader(String postsFile, EdgeDomain domain, Consumer postConsumer) { + this.postsFile = postsFile; + this.domain = domain; + this.postConsumer = postConsumer; + runThread = new Thread(this::run, "StackOverflowPostReader"); + runThread.start(); + + } + + @Override + public void startElement(String uri, String lName, String qName, Attributes attr) throws SAXException { + if (!"row".equals(qName)) { + return; + } + + if ("1".equals(attr.getValue("PostTypeId"))) { + onQuestion(attr); + } + if ("2".equals(attr.getValue("PostTypeId"))) { + onReply(attr); + } + + while (questionWindow.size() > MAX_QUESTION_WINDOW_SIZE) { + var data = questionWindow.removeFirst(); + finalizeQuestion(data); + } + + } + + private void finalizeQuestion(StackOverflowQuestionData data) { + questionsById.remove(data.getId()); + var post = createPost(data); + postConsumer.accept(post); + } + + private StackOverflowPost createPost(StackOverflowQuestionData data) { + EdgeUrl url = new EdgeUrl("https", domain, null, "/questions/"+data.getId()); + + StringBuilder body = new StringBuilder(); + body.append(data.getQuestion()); + data.getReplies().forEach(body::append); + + return new StackOverflowPost(url, data.getTitle(), body.toString(), data.getQuestion()); + } + + + private void onQuestion(Attributes attr) { + String id = attr.getValue("Id"); + String title = attr.getValue("Title"); + String body = attr.getValue("Body"); + String score = attr.getValue("Score"); + if (parseInt(score) < 0) + return; + + var data = new StackOverflowQuestionData(parseInt(id), title, body, new ArrayList<>()); + questionsById.put(data.getId(), data); + questionWindow.addLast(data); + } + + private void onReply(Attributes attr) { + String parentId = attr.getValue("ParentId"); + String body = attr.getValue("Body"); + String score = attr.getValue("Score"); + if (parseInt(score) < 0) + return; + + var data = questionsById.get(parseInt(parentId)); + if (data != null) { + data.getReplies().add(body); + } + } + + private int parseInt(String id) { + return Integer.parseInt(id); + } + + @SneakyThrows + private void run() { + SAXParserFactory factory = SAXParserFactory.newInstance(); + SAXParser saxParser = factory.newSAXParser(); + + saxParser.parse(postsFile, this); + + while (!questionWindow.isEmpty()) { + var data = questionWindow.removeFirst(); + finalizeQuestion(data); + } + } + + public void join() throws InterruptedException { + runThread.join(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/stackoverflow/model/StackOverflowPost.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/stackoverflow/model/StackOverflowPost.java new file mode 100644 index 00000000..03cc1c90 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/stackoverflow/model/StackOverflowPost.java @@ -0,0 +1,14 @@ +package nu.marginalia.wmsa.edge.integration.stackoverflow.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.ToString; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +@Data @AllArgsConstructor @ToString +public class StackOverflowPost { + public EdgeUrl url; + public String title; + public String fullBody; + public String justBody; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/stackoverflow/model/StackOverflowQuestionData.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/stackoverflow/model/StackOverflowQuestionData.java new file mode 100644 index 00000000..52e2ff6e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/stackoverflow/model/StackOverflowQuestionData.java @@ -0,0 +1,14 @@ +package nu.marginalia.wmsa.edge.integration.stackoverflow.model; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data @AllArgsConstructor +public class StackOverflowQuestionData { + int id; + String title; + String question; + List replies; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/wikipedia/WikipediaProcessor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/wikipedia/WikipediaProcessor.java new file mode 100644 index 00000000..c2577b93 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/wikipedia/WikipediaProcessor.java @@ -0,0 +1,82 @@ +package nu.marginalia.wmsa.edge.integration.wikipedia; + +import nu.marginalia.wmsa.edge.crawler.domain.LinkParser; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.DocumentKeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.integration.model.BasicDocumentData; +import nu.marginalia.wmsa.edge.integration.wikipedia.model.WikipediaArticle; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainLink; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.apache.commons.lang3.StringUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class WikipediaProcessor { + private final LinkParser linkParser = new LinkParser(); + + private final SentenceExtractor sentenceExtractor; + private final DocumentKeywordExtractor documentKeywordExtractor; + + public WikipediaProcessor(SentenceExtractor sentenceExtractor, DocumentKeywordExtractor documentKeywordExtractor) { + this.sentenceExtractor = sentenceExtractor; + this.documentKeywordExtractor = documentKeywordExtractor; + } + + + public BasicDocumentData process(WikipediaArticle post) { + + final var docUrl = post.getUrl(); + final var doc = Jsoup.parseBodyFragment(post.body); + + String title = StringUtils.abbreviate(doc.getElementsByTag("title").text(), 255); + String description = getSummary(doc); + + EdgeDomainLink[] domainLinks = getDomainLinks(docUrl, doc); + + var dld = sentenceExtractor.extractSentences(doc); + var keywords = documentKeywordExtractor.extractKeywords(dld); + + keywords.get(IndexBlock.Meta).addJust("site:"+post.getUrl().domain); + keywords.get(IndexBlock.Words).addJust("site:"+post.getUrl().domain); + keywords.get(IndexBlock.Words).addJust("special:stackoverflow"); + keywords.get(IndexBlock.Meta).addJust("special:stackoverflow"); + keywords.get(IndexBlock.Meta).addJust("js:true"); + + return new BasicDocumentData(docUrl, title, description, post.body.hashCode(), keywords, domainLinks, + dld.totalNumWords()); + + } + + private String getSummary(Document doc) { + doc = doc.clone(); + doc.select("table,sup,.reference").remove(); + return StringUtils.abbreviate(doc.select("#bodyContent p").text(), 255); + } + + private EdgeDomainLink[] getDomainLinks(EdgeUrl docUrl, Document doc) { + List links = new ArrayList<>(10); + + for (var tag : doc.getElementsByTag("a")) { + if (!tag.hasAttr("href")) { + continue; + } + String href = tag.attr("href"); + if (href.length()<10 || !href.contains(".") || !href.contains("://")) { + continue; + } + + linkParser.parseLink(docUrl, tag) + .filter(url -> !Objects.equals(docUrl.getDomain(), url.getDomain())) + .ifPresent(links::add); + } + + return links.stream().map(EdgeUrl::getDomain).map(domain -> new EdgeDomainLink(docUrl.domain, domain)) + .distinct().toArray(EdgeDomainLink[]::new); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/wikipedia/WikipediaReader.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/wikipedia/WikipediaReader.java new file mode 100644 index 00000000..12bfec3f --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/wikipedia/WikipediaReader.java @@ -0,0 +1,46 @@ +package nu.marginalia.wmsa.edge.integration.wikipedia; + +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.integration.wikipedia.model.WikipediaArticle; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.openzim.ZIMTypes.ZIMFile; +import org.openzim.ZIMTypes.ZIMReader; + +import java.util.function.Consumer; + +public class WikipediaReader { + + private final Thread runThread; + private final String zimFile; + private final EdgeDomain domain; + private final Consumer postConsumer; + + public WikipediaReader(String zimFile, EdgeDomain domain, Consumer postConsumer) { + this.zimFile = zimFile; + this.domain = domain; + this.postConsumer = postConsumer; + + runThread = new Thread(this::run, "WikipediaReader"); + runThread.start(); + } + + @SneakyThrows + private void run() { + var zr = new ZIMReader(new ZIMFile(zimFile)); + + zr.forEachArticles((originalUrl, art) -> { + if (art != null) { + postConsumer.accept(new WikipediaArticle(synthesizeUrl(originalUrl), art)); + } + }, p -> true); + } + + private EdgeUrl synthesizeUrl(String originalUrl) { + return new EdgeUrl("https", domain, null, "/wiki/"+originalUrl); + } + + public void join() throws InterruptedException { + runThread.join(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/wikipedia/model/WikipediaArticle.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/wikipedia/model/WikipediaArticle.java new file mode 100644 index 00000000..bc221492 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/integration/wikipedia/model/WikipediaArticle.java @@ -0,0 +1,12 @@ +package nu.marginalia.wmsa.edge.integration.wikipedia.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +@Data +@AllArgsConstructor +public class WikipediaArticle { + public final EdgeUrl url; + public final String body; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/EdgeCrawlPlan.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/EdgeCrawlPlan.java new file mode 100644 index 00000000..4e237908 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/EdgeCrawlPlan.java @@ -0,0 +1,30 @@ +package nu.marginalia.wmsa.edge.model; + +import lombok.*; + +import java.nio.file.Path; + +@AllArgsConstructor @NoArgsConstructor @ToString +public class EdgeCrawlPlan { + public String jobSpec; + public WorkDir crawl; + public WorkDir process; + + public Path getJobSpec() { + return Path.of(jobSpec); + } + + @AllArgsConstructor @NoArgsConstructor @ToString + public static class WorkDir { + public String dir; + public String logName; + + public Path getDir() { + return Path.of(dir); + } + public Path getLogFile() { + return Path.of(dir).resolve(logName); + } + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/EdgeDomain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/EdgeDomain.java new file mode 100644 index 00000000..cb778947 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/EdgeDomain.java @@ -0,0 +1,130 @@ +package nu.marginalia.wmsa.edge.model; + +import lombok.*; + +import javax.annotation.Nonnull; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +@AllArgsConstructor +@Getter @Setter @Builder +public class EdgeDomain implements WideHashable { + + private static final Predicate ipPatternTest = Pattern.compile("[\\d]{1,3}\\.[\\d]{1,3}\\.[\\d]{1,3}\\.[\\d]{1,3}").asMatchPredicate(); + private static final Predicate govListTest = Pattern.compile(".*\\.(ac|co|org|gov|edu|com)\\.[a-z]{2}").asMatchPredicate(); + + @Nonnull + public final String subDomain; + @Nonnull + public final String domain; + + @SneakyThrows + public EdgeDomain(String host) { + + var dot = host.lastIndexOf('.'); + + if (dot < 0 || ipPatternTest.test(host)) { // IPV6 >.> + subDomain = ""; + domain = host; + } + else { + int dot2 = host.substring(0, dot).lastIndexOf('.'); + if (dot2 < 0) { + subDomain = ""; + domain = host; + } + else { + if (govListTest.test(host)) + { // Capture .ac.jp, .co.uk + int dot3 = host.substring(0, dot2).lastIndexOf('.'); + if (dot3 >= 0) { + dot2 = dot3; + subDomain = host.substring(0, dot2); + domain = host.substring(dot2 + 1); + } + else { + subDomain = ""; + domain = host; + } + } + else { + subDomain = host.substring(0, dot2); + domain = host.substring(dot2 + 1); + } + } + } + + + } + + public String toString() { + return getAddress(); + } + + public String getAddress() { + if (!subDomain.isEmpty()) { + return subDomain + "." + domain; + } + return domain; + } + + public String getDomainKey() { + int cutPoint = domain.indexOf('.'); + if (cutPoint < 0) { + return domain; + } + return domain.substring(0, cutPoint).toLowerCase(); + } + public String getLongDomainKey() { + StringBuilder ret = new StringBuilder(); + + int cutPoint = domain.indexOf('.'); + if (cutPoint < 0) { + ret.append(domain); + } + else { + ret.append(domain, 0, cutPoint); + } + + if (!"".equals(subDomain) && !"www".equals(subDomain)) { + ret.append(":"); + ret.append(subDomain); + } + + return ret.toString().toLowerCase(); + } + + @Override + public long wideHash() { + return ((long) Objects.hash(domain, subDomain) << 32) | toString().hashCode(); + } + + public boolean equals(final Object o) { + if (o == this) return true; + if (!(o instanceof EdgeDomain)) return false; + final EdgeDomain other = (EdgeDomain) o; + if (!other.canEqual((Object) this)) return false; + final String this$subDomain = this.getSubDomain(); + final String other$subDomain = other.getSubDomain(); + if (!this$subDomain.equalsIgnoreCase(other$subDomain)) return false; + final String this$domain = this.getDomain(); + final String other$domain = other.getDomain(); + if (!this$domain.equalsIgnoreCase(other$domain)) return false; + return true; + } + + protected boolean canEqual(final Object other) { + return other instanceof EdgeDomain; + } + + public int hashCode() { + final int PRIME = 59; + int result = 1; + final Object $subDomain = this.getSubDomain().toLowerCase(); + result = result * PRIME + $subDomain.hashCode(); + final Object $domain = this.getDomain().toLowerCase(); + result = result * PRIME + $domain.hashCode(); + return result; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/EdgeId.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/EdgeId.java new file mode 100644 index 00000000..f2be15fa --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/EdgeId.java @@ -0,0 +1,15 @@ +package nu.marginalia.wmsa.edge.model; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** This exists entirely for strengthening the typing of IDs + * + * @param + */ +@AllArgsConstructor @Getter @EqualsAndHashCode @ToString +public class EdgeId { + private final int id; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/EdgeUrl.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/EdgeUrl.java new file mode 100644 index 00000000..39bc475b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/EdgeUrl.java @@ -0,0 +1,124 @@ +package nu.marginalia.wmsa.edge.model; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.regex.Pattern; + +@Getter @Setter @Builder @EqualsAndHashCode +public class EdgeUrl implements WideHashable { + public final String proto; + public final EdgeDomain domain; + public final Integer port; + public final String path; + + public EdgeUrl(String proto, EdgeDomain domain, Integer port, String path) { + this.proto = proto; + this.domain = domain; + this.port = port(port, proto); + this.path = path; + } + + public EdgeUrl(String url) throws URISyntaxException { + this(new URI(urlencodeFixer(url))); + } + + private static Pattern badCharPattern = Pattern.compile("[ \t\n\"<>\\[\\]()',|]"); + + public static String urlencodeFixer(String url) throws URISyntaxException { + var s = new StringBuilder(); + String goodChars = "&.?:/-;+$"; + String hexChars = "0123456789abcdefABCDEF"; + + int pathIdx = findPathIdx(url); + if (pathIdx < 0) { + return url; + } + s.append(url, 0, pathIdx); + + for (int i = pathIdx; i < url.length(); i++) { + int c = url.charAt(i); + + if (goodChars.indexOf(c) >= 0 || (c >= 'A' && c <='Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { + s.appendCodePoint(c); + } + else if (c == '%' && i+2= 0 && hexChars.indexOf(cnn) >= 0) { + s.appendCodePoint(c); + } + else { + s.append("%25"); + } + } + else { + s.append(String.format("%%%02X", c)); + } + } + + return s.toString(); + } + + private static int findPathIdx(String url) throws URISyntaxException { + int colonIdx = url.indexOf(':'); + if (colonIdx < 0 || colonIdx + 2 >= url.length()) { + throw new URISyntaxException(url, "Lacking protocol"); + } + return url.indexOf('/', colonIdx+2); + } + + public EdgeUrl(URI URI) { + this.domain = new EdgeDomain(URI.getHost()); + this.path = URI.getPath().isEmpty() ? "/" : URI.getPath(); + this.proto = URI.getScheme().toLowerCase(); + this.port = port(URI.getPort(), proto); + } + + public EdgeUrl sibling(String newPath) { + return new EdgeUrl(proto, domain, port, newPath); + } + + + private static Integer port(Integer port, String protocol) { + if (null == port || port < 1) { + return null; + } + if (protocol.equals("http") && port == 80) { + return null; + } + else if (protocol.equals("https") && port == 443) { + return null; + } + return port; + } + + public String toString() { + String portPart = port == null ? "" : (":" + port); + + return proto + "://" + domain + portPart + "" + path; + } + + public String dir() { + return path.replaceAll("/[^/]+$", "/"); + } + public String fileName() { + return path.replaceAll(".*/", ""); + } + + public long wideHash() { + long domainHash = domain.hashCode(); + long thisHash = hashCode(); + return (domainHash << 32) | thisHash; + } + + public int depth() { + return (int) path.chars().filter(c -> c=='/').count(); + } + + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/WideHashable.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/WideHashable.java new file mode 100644 index 00000000..3b95711d --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/WideHashable.java @@ -0,0 +1,5 @@ +package nu.marginalia.wmsa.edge.model; + +public interface WideHashable { + long wideHash(); +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeContentType.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeContentType.java new file mode 100644 index 00000000..70978166 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeContentType.java @@ -0,0 +1,15 @@ +package nu.marginalia.wmsa.edge.model.crawl; + + +import lombok.*; + +@AllArgsConstructor +@EqualsAndHashCode +@Getter +@Setter +@Builder +@ToString +public class EdgeContentType { + public final String contentType; + public final String charset; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeDomainIndexingState.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeDomainIndexingState.java new file mode 100644 index 00000000..119da59d --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeDomainIndexingState.java @@ -0,0 +1,27 @@ +package nu.marginalia.wmsa.edge.model.crawl; + +public enum EdgeDomainIndexingState { + ACTIVE(0), + EXHAUSTED(1), + SPECIAL(2), + SOCIAL_MEDIA(3), + BLOCKED(-1), + REDIR(-2), + ERROR(-3), + UNKNOWN(-100); + + public final int code; + + EdgeDomainIndexingState(int code) { + this.code = code; + } + + public static EdgeDomainIndexingState fromCode(int code) { + for (var state : values()) { + if (state.code == code) { + return state; + } + } + return UNKNOWN; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeDomainLink.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeDomainLink.java new file mode 100644 index 00000000..7486fec3 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeDomainLink.java @@ -0,0 +1,10 @@ +package nu.marginalia.wmsa.edge.model.crawl; + +import lombok.*; +import nu.marginalia.wmsa.edge.model.EdgeDomain; + +@AllArgsConstructor @EqualsAndHashCode @Getter @Setter @Builder @ToString +public class EdgeDomainLink { + public final EdgeDomain source; + public final EdgeDomain destination; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeHtmlStandard.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeHtmlStandard.java new file mode 100644 index 00000000..18142da2 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeHtmlStandard.java @@ -0,0 +1,19 @@ +package nu.marginalia.wmsa.edge.model.crawl; + +public enum EdgeHtmlStandard { + PLAIN(0, 1), + UNKNOWN(0, 1), + HTML123(0, 1), + HTML4(-0.1, 1.05), + XHTML(-0.1, 1.05), + HTML5(0.5, 1.1); + + public final double offset; + public final double scale; + + EdgeHtmlStandard(double offset, double scale) { + this.offset = offset; + this.scale = scale; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeIndexTask.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeIndexTask.java new file mode 100644 index 00000000..1212bfa9 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeIndexTask.java @@ -0,0 +1,33 @@ +package nu.marginalia.wmsa.edge.model.crawl; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +@Getter @AllArgsConstructor @ToString +public class EdgeIndexTask { + public final EdgeDomain domain; + public final List visited = new ArrayList<>(); + public final List urls = new ArrayList<>(); + public final int pass; + public final int limit; + public double rank; + + public boolean isEmpty() { + return domain == null || urls.isEmpty(); + } + + public Stream streamUrls() { + return urls.stream(); + } + + public int size() { + return urls.size(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgePageContent.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgePageContent.java new file mode 100644 index 00000000..997d25c1 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgePageContent.java @@ -0,0 +1,25 @@ +package nu.marginalia.wmsa.edge.model.crawl; + +import lombok.Data; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +import java.util.Map; +import java.util.Set; + +@Data +public class EdgePageContent { + public final EdgeUrl url; + public final EdgePageWordSet words; + public final Map> linkWords; + public final EdgePageMetadata metadata; + public final int hash; + public final String ipAddress; + + public boolean hasHotLink(EdgeUrl url) { + return linkWords.containsKey(url); + } + + public int numWords() { + return metadata.totalWords; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgePageMetadata.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgePageMetadata.java new file mode 100644 index 00000000..bb192f9e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgePageMetadata.java @@ -0,0 +1,50 @@ +package nu.marginalia.wmsa.edge.model.crawl; + +import lombok.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@AllArgsConstructor @EqualsAndHashCode @Getter @Setter @ToString @With +public class EdgePageMetadata { + public final int features; + public final int scriptTags; + public final int rawLength; + public final int textBodyLength; + public final int textDistinctWords; + public final String title; + public final String description; + public final double smutCoefficient; + public final int totalWords; + public final EdgeHtmlStandard htmlStandard; + private static final Logger logger = LoggerFactory.getLogger(EdgePageMetadata.class); + private static EdgePageMetadata _empty + = new EdgePageMetadata(0, 0, + 0, + 0, + 0, + "", + "", + 0., + 1, + EdgeHtmlStandard.UNKNOWN); + public static EdgePageMetadata empty() { + return _empty; + } + + public double quality() { + if (rawLength == 0 || textBodyLength == 0) { + return -5.; + } + +/* double dictionaryFactor = textDistinctWords / 10000.; + if (dictionaryFactor < 0.1) { + dictionaryFactor = 0; + }*/ + + return Math.log(textBodyLength / (double) rawLength)*htmlStandard.scale + + htmlStandard.offset + - scriptTags + // - dictionaryFactor + - smutCoefficient; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgePageWordSet.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgePageWordSet.java new file mode 100644 index 00000000..c4355ae3 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgePageWordSet.java @@ -0,0 +1,48 @@ +package nu.marginalia.wmsa.edge.model.crawl; + +import lombok.Data; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; + +import java.util.*; + +@Data +public class EdgePageWordSet { + public final Map wordSets; + + public EdgePageWordSet(EdgePageWords... words) { + wordSets = new EnumMap<>(IndexBlock.class); + for (EdgePageWords w : words) { + wordSets.put(w.block, w); + } + } + + public EdgePageWords get(IndexBlock block) { + var words = wordSets.get(block); + if (words == null) { + return new EdgePageWords(block); + } + return words; + } + + public void append(IndexBlock block, Collection words) { + wordSets.computeIfAbsent(block, b -> new EdgePageWords(block)).addAll(words); + } + + public Collection values() { + return new ArrayList<>(wordSets.values()); + } + + public boolean isEmpty() { + return 0 == wordSets.values().stream().mapToInt(EdgePageWords::size).sum(); + } + + public String toString() { + var sj = new StringJoiner("\n", "EdgePageWordSet:\n", ""); + wordSets.forEach((block, words) -> { + if (words.size() > 0) { + sj.add("\t" + block + "\t" + words.getWords()); + } + }); + return sj.toString(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgePageWords.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgePageWords.java new file mode 100644 index 00000000..efb20dcc --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgePageWords.java @@ -0,0 +1,34 @@ +package nu.marginalia.wmsa.edge.model.crawl; +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@ToString @Getter +public class EdgePageWords { + public final IndexBlock block; + public final List words = new ArrayList<>(); + + public EdgePageWords(IndexBlock block) { + this.block = block; + } + public EdgePageWords(IndexBlock block, Collection initial) { + this.block = block; + + addAll(initial); + } + + public void addAll(Collection words) { + this.words.addAll(words); + } + public void addAllMax(Collection words, int limit) { + words.stream().limit(limit).forEach(this.words::add); + } + public int size() { + return words.size(); + } + public void addJust(String word) { words.add(word); } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeRawPageContents.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeRawPageContents.java new file mode 100644 index 00000000..8bc5c8d2 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeRawPageContents.java @@ -0,0 +1,24 @@ +package nu.marginalia.wmsa.edge.model.crawl; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +@Data @Getter @AllArgsConstructor +public class EdgeRawPageContents { + public final EdgeUrl url; + public final EdgeUrl redirectUrl; + public final String data; + public final EdgeContentType contentType; + public final String ip; + public boolean hasCookies; + public final String fetchTimestamp; + + public boolean isAfter(String dateIso8601) { + if (fetchTimestamp == null) { + return false; + } + return fetchTimestamp.compareTo(dateIso8601) >= 0; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeRobotsTxt.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeRobotsTxt.java new file mode 100644 index 00000000..5a8bfaea --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeRobotsTxt.java @@ -0,0 +1,10 @@ +package nu.marginalia.wmsa.edge.model.crawl; + +import lombok.*; +import nu.marginalia.wmsa.edge.model.EdgeDomain; + +@AllArgsConstructor @EqualsAndHashCode @Getter @Setter @Builder +public class EdgeRobotsTxt { + public final EdgeDomain domain; + public final String robotsTxt; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeUrlState.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeUrlState.java new file mode 100644 index 00000000..67fc2b61 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeUrlState.java @@ -0,0 +1,10 @@ +package nu.marginalia.wmsa.edge.model.crawl; + +/** This should correspond to EC_URL.STATE */ +public enum EdgeUrlState { + OK, + REDIRECT, + DEAD, + ARCHIVED, + DISQUALIFIED +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeUrlVisit.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeUrlVisit.java new file mode 100644 index 00000000..07d7492e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/crawl/EdgeUrlVisit.java @@ -0,0 +1,21 @@ +package nu.marginalia.wmsa.edge.model.crawl; + +import lombok.Data; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +@Data +public class EdgeUrlVisit { + public final EdgeUrl url; + public final Integer data_hash_code; + public final Double quality; + public final String title; + public final String description; + public final String ipAddress; + public final String format; + public final int features; + + public final int wordCountDistinct; + public final int wordCountTotal; + + public final EdgeUrlState urlState; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgePageScoreAdjustment.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgePageScoreAdjustment.java new file mode 100644 index 00000000..e0cda818 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgePageScoreAdjustment.java @@ -0,0 +1,30 @@ +package nu.marginalia.wmsa.edge.model.search; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class EdgePageScoreAdjustment { + final double titleAdj; + final double titleFullHit; + final double urlAdj; + final double domainAdj; + final double descAdj; + final double descHitsAdj; + + private static final EdgePageScoreAdjustment zero = new EdgePageScoreAdjustment(0,0, 0,0,0, 0); + public static EdgePageScoreAdjustment zero() { + return zero; + } + + public double getScore() { + return titleAdj + titleFullHit + urlAdj + domainAdj + descAdj + descHitsAdj; + } + + @Override + public String toString() { + return String.format("(%2.2f %2.2f %2.2f %2.2f %2.2f %2.2f)=%2.2f", + titleAdj, titleFullHit, urlAdj, domainAdj, descAdj, descHitsAdj, getScore()); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResultItem.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResultItem.java new file mode 100644 index 00000000..dece8aca --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResultItem.java @@ -0,0 +1,39 @@ +package nu.marginalia.wmsa.edge.model.search; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeId; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@AllArgsConstructor @ToString @Getter @EqualsAndHashCode +public class EdgeSearchResultItem { + public final int blockId; + public final int queryLength; + public final EdgeId domain; + public final EdgeId url; + public final List scores; + + public EdgeSearchResultItem(int blockId, int queryLength, long val) { + int urlId = (int) (val & 0xFFFFFFFFL); + int domainId = (int) (val >>> 32); + + this.queryLength = queryLength; + this.blockId = blockId; + + url = new EdgeId<>(urlId); + domain = new EdgeId<>(domainId); + scores = new ArrayList<>(); + } + + public long getCombinedId() { + return ((long) domain.getId() << 32L) | url.getId(); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResultKeywordScore.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResultKeywordScore.java new file mode 100644 index 00000000..e20dfbcd --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResultKeywordScore.java @@ -0,0 +1,14 @@ +package nu.marginalia.wmsa.edge.model.search; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; + +@AllArgsConstructor @ToString @EqualsAndHashCode +public class EdgeSearchResultKeywordScore { + public final String keyword; + public final IndexBlock index; + public boolean title; + public boolean link; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResultSet.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResultSet.java new file mode 100644 index 00000000..a703636c --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResultSet.java @@ -0,0 +1,19 @@ +package nu.marginalia.wmsa.edge.model.search; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@AllArgsConstructor @Getter @ToString +public class EdgeSearchResultSet { + public Map> resultsList; + + public int size() { + return resultsList.values().stream().mapToInt(List::size).sum(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResults.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResults.java new file mode 100644 index 00000000..e23496c4 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResults.java @@ -0,0 +1,32 @@ +package nu.marginalia.wmsa.edge.model.search; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@AllArgsConstructor @Getter @ToString +public class EdgeSearchResults { + public final Map> results; + + public EdgeSearchResults() { + results = new HashMap<>(); + } + + public int size() { + return results.values().stream().mapToInt(List::size).sum(); + } + + public Stream stream() { + return results.values().stream().flatMap(List::stream); + } + + public List getAllItems() { + return stream().collect(Collectors.toList()); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResultsKey.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResultsKey.java new file mode 100644 index 00000000..aefee330 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchResultsKey.java @@ -0,0 +1,13 @@ +package nu.marginalia.wmsa.edge.model.search; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode +@AllArgsConstructor +@Getter +public class EdgeSearchResultsKey { + public final int bucket; + public final int searchTermCount; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchSpecification.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchSpecification.java new file mode 100644 index 00000000..1f88e518 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchSpecification.java @@ -0,0 +1,32 @@ +package nu.marginalia.wmsa.edge.model.search; + +import lombok.*; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.index.service.SearchOrder; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeId; + +import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@ToString @Getter @Builder @With @AllArgsConstructor +public class EdgeSearchSpecification { + + public List buckets; + public List subqueries; + public final int limitByBucket; + public final int limitByDomain; + public final int limitTotal; + + public final String humanQuery; + public final SearchOrder searchOrder; + public boolean stagger; + public boolean experimental; + + public static EdgeSearchSpecification justIncludes(String... words) { + return new EdgeSearchSpecification(Collections.emptyList(), Collections.singletonList(new EdgeSearchSubquery(Arrays.asList(words), Collections.emptyList(), IndexBlock.Title)), 10, 10, 10, "", SearchOrder.ASCENDING, false, false); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchSubquery.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchSubquery.java new file mode 100644 index 00000000..9de07248 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeSearchSubquery.java @@ -0,0 +1,34 @@ +package nu.marginalia.wmsa.edge.model.search; + +import lombok.*; +import lombok.experimental.FieldNameConstants; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import org.h2.index.Index; + +import java.util.List; + +@ToString +@Getter +@AllArgsConstructor +public class EdgeSearchSubquery { + + public final List searchTermsInclude; + public final List searchTermsExclude; + public final IndexBlock block; + + private final int termSize; + public EdgeSearchSubquery(List searchTermsInclude, List searchTermsExclude, IndexBlock block) { + this.searchTermsInclude = searchTermsInclude; + this.searchTermsExclude = searchTermsExclude; + this.block = block; + this.termSize = (int) searchTermsInclude.stream().flatMapToInt(String::chars).filter(i -> '_'==i).count(); + } + + public EdgeSearchSubquery withBlock(IndexBlock block) { + return new EdgeSearchSubquery(searchTermsInclude, searchTermsExclude, block); + } + + public int termSize() { + return termSize; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeUrlDetails.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeUrlDetails.java new file mode 100644 index 00000000..769f32f8 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/model/search/EdgeUrlDetails.java @@ -0,0 +1,151 @@ +package nu.marginalia.wmsa.edge.model.search; + +import lombok.*; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlFeature; +import nu.marginalia.wmsa.edge.index.EdgeIndexService; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import nu.marginalia.wmsa.edge.search.EdgeSearchRankingSymbols; + +import java.util.Objects; + +@AllArgsConstructor @NoArgsConstructor @With @Getter @ToString +public class EdgeUrlDetails { + public int id; + public EdgeUrl url; + public String title; + public String description; + + public double urlQuality; + public double urlQualityRaw; + public double domainQuality; + + public int links; // DEAD + public int words; + public String format; + public int features; + + public EdgePageScoreAdjustment urlQualityAdjustment; + + public long rankingId; + public double termScore; + + public String ip; // BROKEN + public int domainState; + public int queryLength; + + public int dataHash; + + public long rankingIdAdjustment() { + int penalty = 0; + + if (words < 500) { + penalty -= 1; + } + if (urlQuality < -10) { + penalty -= 1; + } + if (isSpecialDomain()) { + penalty -= 1; + } + return penalty; //(int)(Math.log(1+rankingId) / Math.log(100))-1-penalty; + } + + public String getFormat() { + if (null == format) { + return "?"; + } + switch (format) { + case "HTML123": + return "HTML 1-3"; + case "HTML4": + return "HTML 4"; + case "XHTML": + return "XHTML"; + case "HTML5": + return "HTML 5"; + case "PLAIN": + return "Plain Text"; + default: + return "?"; + } + } + + public int hashCode() { + return Integer.hashCode(id); + } + + public boolean equals(Object other) { + if (other == null) { + return false; + } + if (other == this) { + return true; + } + if (other instanceof EdgeUrlDetails) { + return ((EdgeUrlDetails) other).id == id; + } + return false; + } + public String getTitle() { + if (title == null || title.isBlank()) { + return url.toString(); + } + return title; + } + + public String getQualityPercent() { + return String.format("%2.2f%%", 100*Math.exp(urlQuality+urlQualityAdjustment.getScore())); + } + public double getRanking() { + double lengthAdjustment = Math.max(1, words / (words + 1000.)); + return (1+termScore)*Math.sqrt(1+rankingId)/Math.max(1E-10, lengthAdjustment *(0.7+0.3*Math.exp(urlQualityAdjustment.getScore()))); + } + + public int getSuperficialHash() { + return Objects.hash(url.path, title); + } + public String getSuperficialHashStr() { + return String.format("%8X", getSuperficialHash()); + } + + + public String getGeminiLink() { + return url.proto + "://" + url.domain.toString() + url.path.replace(" ", "%20").replace("\"", "%22"); + } + public String getGeminiDescription() { + return description.trim(); + } + + public boolean isPlainText() { + return "PLAIN".equals(format); + } + + public boolean isScripts() { + return HtmlFeature.hasFeature(features, HtmlFeature.JS); + } + public boolean isTracking() { + return HtmlFeature.hasFeature(features, HtmlFeature.TRACKING); + } + public boolean isAffiliate() { + return HtmlFeature.hasFeature(features, HtmlFeature.AFFILIATE_LINK); + } + public boolean isMedia() { + return HtmlFeature.hasFeature(features, HtmlFeature.MEDIA); + } + public boolean isCookies() { + return HtmlFeature.hasFeature(features, HtmlFeature.COOKIES); + } + public boolean isSpecialDomain() { + return domainState == EdgeDomainIndexingState.SPECIAL.code; + } + public int getLogRank() { return (int) Math.round(Math.min(Math.log(1+rankingId),10)); } + + public String getRankingSymbol() { + return EdgeSearchRankingSymbols.getRankingSymbol(termScore); + } + + public String getRankingSymbolDesc() { + return EdgeSearchRankingSymbols.getRankingSymbolDescription(termScore); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/BrowseResult.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/BrowseResult.java new file mode 100644 index 00000000..df23d75d --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/BrowseResult.java @@ -0,0 +1,11 @@ +package nu.marginalia.wmsa.edge.search; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import nu.marginalia.wmsa.edge.model.EdgeUrl; + +@Data @EqualsAndHashCode +public class BrowseResult { + public final EdgeUrl url; + public final int domainId; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/BrowseResultSet.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/BrowseResultSet.java new file mode 100644 index 00000000..01f3be99 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/BrowseResultSet.java @@ -0,0 +1,12 @@ +package nu.marginalia.wmsa.edge.search; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@AllArgsConstructor +@Getter +public class BrowseResultSet { + public final List results; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/DecoratedSearchResultSet.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/DecoratedSearchResultSet.java new file mode 100644 index 00000000..3b0e2074 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/DecoratedSearchResultSet.java @@ -0,0 +1,22 @@ +package nu.marginalia.wmsa.edge.search; + +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.edge.model.search.EdgeUrlDetails; + +import java.util.List; +import java.util.Objects; + +@ToString @Getter +public class DecoratedSearchResultSet { + public final List resultSet; + + public int size() { + return resultSet.size(); + } + + public DecoratedSearchResultSet(List resultSet) { + this.resultSet = Objects.requireNonNull(resultSet); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/DecoratedSearchResults.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/DecoratedSearchResults.java new file mode 100644 index 00000000..de831c77 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/DecoratedSearchResults.java @@ -0,0 +1,33 @@ +package nu.marginalia.wmsa.edge.search; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import nu.marginalia.wmsa.edge.assistant.dict.WikiArticles; +import nu.marginalia.wmsa.edge.model.search.EdgeUrlDetails; +import nu.marginalia.wmsa.edge.search.query.model.EdgeUserSearchParameters; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +@AllArgsConstructor @Getter +public class DecoratedSearchResults { + private final EdgeUserSearchParameters params; + private final List problems; + private final String evalResult; + private final WikiArticles wiki; + private final List results; + + private final String focusDomain; + private final int focusDomainId; + + public String getQuery() { + return params.humanQuery; + } + public String getProfile() { + return params.getProfile().name; + } + public String getJs() { + return params.jsSetting; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchMain.java new file mode 100644 index 00000000..cc8b9c0d --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchMain.java @@ -0,0 +1,38 @@ +package nu.marginalia.wmsa.edge.search; + +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import nu.marginalia.wmsa.configuration.MainClass; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.module.ConfigurationModule; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.configuration.server.Initialization; +import spark.Spark; + +import java.io.IOException; + +public class EdgeSearchMain extends MainClass { + private EdgeSearchService service; + + @Inject + public EdgeSearchMain(EdgeSearchService service) throws IOException { + this.service = service; + } + + public static void main(String... args) { + init(ServiceDescriptor.EDGE_SEARCH, args); + + Spark.staticFileLocation("/static/edge/"); + + Injector injector = Guice.createInjector( + new EdgeSearchModule(), + new ConfigurationModule(), + new DatabaseModule() + ); + + injector.getInstance(EdgeSearchMain.class); + injector.getInstance(Initialization.class).setReady(); + + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchModule.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchModule.java new file mode 100644 index 00000000..d40f147a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchModule.java @@ -0,0 +1,22 @@ +package nu.marginalia.wmsa.edge.search; + +import com.google.inject.AbstractModule; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; + +import java.nio.file.Path; + +public class EdgeSearchModule extends AbstractModule { + + public void configure() { + + bind(LanguageModels.class).toInstance(new LanguageModels( + Path.of("/var/lib/wmsa/model/ngrams-generous-emstr.bin"), + Path.of("/var/lib/wmsa/model/tfreq-new-algo3.bin"), + Path.of("/var/lib/wmsa/model/opennlp-sentence.bin"), + Path.of("/var/lib/wmsa/model/English.RDR"), + Path.of("/var/lib/wmsa/model/English.DICT"), + Path.of("/var/lib/wmsa/model/opennlp-tok.bin") + )); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchOperator.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchOperator.java new file mode 100644 index 00000000..e8f236e2 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchOperator.java @@ -0,0 +1,334 @@ +package nu.marginalia.wmsa.edge.search; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.edge.assistant.client.AssistantClient; +import nu.marginalia.wmsa.edge.assistant.dict.WikiArticles; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDao; +import nu.marginalia.wmsa.edge.index.client.EdgeIndexClient; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.index.service.SearchOrder; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.search.*; +import nu.marginalia.wmsa.edge.search.query.model.EdgeSearchQuery; +import nu.marginalia.wmsa.edge.search.query.QueryFactory; +import nu.marginalia.wmsa.edge.search.query.model.EdgeUserSearchParameters; +import nu.marginalia.wmsa.edge.search.results.SearchResultValuator; +import nu.marginalia.wmsa.edge.search.results.model.AccumulatedQueryResults; +import nu.marginalia.wmsa.edge.search.results.SearchResultDecorator; +import nu.marginalia.wmsa.edge.search.results.UrlDeduplicator; +import org.apache.logging.log4j.util.Strings; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; + +@Singleton +public class EdgeSearchOperator { + + private static final Logger logger = LoggerFactory.getLogger(EdgeSearchOperator.class); + private final AssistantClient assistantClient; + private final EdgeDataStoreDao edgeDataStoreDao; + private final EdgeIndexClient indexClient; + private final QueryFactory queryFactory; + private final SearchResultDecorator resultDecorator; + private final SearchResultValuator valuator; + private final Comparator resultListComparator; + + @Inject + public EdgeSearchOperator(AssistantClient assistantClient, + EdgeDataStoreDao edgeDataStoreDao, + EdgeIndexClient indexClient, + QueryFactory queryFactory, + SearchResultDecorator resultDecorator, + SearchResultValuator valuator + ) { + + this.assistantClient = assistantClient; + this.edgeDataStoreDao = edgeDataStoreDao; + this.indexClient = indexClient; + this.queryFactory = queryFactory; + this.resultDecorator = resultDecorator; + this.valuator = valuator; + + Comparator c = Comparator.comparing(ud -> Math.round(10*(ud.getTermScore() - ud.rankingIdAdjustment()))); + resultListComparator = c.thenComparing(EdgeUrlDetails::getRanking).thenComparing(EdgeUrlDetails::getId); + } + + public List doApiSearch(Context ctx, + EdgeUserSearchParameters params) { + + + var processedQuery = queryFactory.createQuery(params); + + logger.info("Human terms (API): {}", Strings.join(processedQuery.searchTermsHuman, ',')); + + DecoratedSearchResultSet queryResults = performQuery(ctx, processedQuery, true); + + return queryResults.resultSet; + } + + public DecoratedSearchResults doSearch(Context ctx, EdgeUserSearchParameters params, String evalResult) { + + + Observable definitions = getWikiArticle(ctx, params.getHumanQuery()); + + var processedQuery = queryFactory.createQuery(params); + + logger.info("Human terms: {}", Strings.join(processedQuery.searchTermsHuman, ',')); + + DecoratedSearchResultSet queryResults = performQuery(ctx, processedQuery, false); + + return new DecoratedSearchResults(params, + getProblems(ctx, params.getHumanQuery(), evalResult, queryResults, processedQuery), + evalResult, + definitions.onErrorReturn((e) -> new WikiArticles()).blockingFirst(), + queryResults.resultSet, + processedQuery.domain, + getDomainId(processedQuery.domain)); + } + + private int getDomainId(String domain) { + int domainId = -1; + try { + if (domain != null) { + return edgeDataStoreDao.getDomainId(new EdgeDomain(domain)).getId(); + } + } + catch (NoSuchElementException ex) { + + } + return domainId; + } + + public DecoratedSearchResultSet performDumbQuery(Context ctx, EdgeSearchProfile profile, IndexBlock block, int limitPerDomain, int limitTotal, String... termsInclude) { + List sqs = new ArrayList<>(); + + sqs.add(new EdgeSearchSubquery(Arrays.asList(termsInclude), Collections.emptyList(), block)); + + EdgeSearchSpecification specs = new EdgeSearchSpecification(profile.buckets, sqs, 100, limitPerDomain, limitTotal, "", SearchOrder.ASCENDING, EdgeSearchProfile.YOLO.equals(profile), false); + + return performQuery(ctx, new EdgeSearchQuery(specs), true); + } + + private DecoratedSearchResultSet performQuery(Context ctx, EdgeSearchQuery processedQuery, boolean asFastAsPossible) { + + AccumulatedQueryResults queryResults = new AccumulatedQueryResults(); + UrlDeduplicator deduplicator = new UrlDeduplicator(processedQuery.specs.limitByDomain); + + if (processedQuery.searchTermsHuman.size()<=4 && !asFastAsPossible) { + fetchResultsMulti(ctx, processedQuery, queryResults, deduplicator); + } + else { + fetchResultsSimple(ctx, processedQuery, queryResults, deduplicator); + } + + List resultList = new ArrayList<>(queryResults.size()); + + for (var details : queryResults.results) { + if (details.getUrlQuality() < -100) { + continue; + } + var scoreAdjustment = adjustScoreBasedOnQuery(details, processedQuery.specs); + details = details.withUrlQualityAdjustment(scoreAdjustment); + + resultList.add(details); + }; + + resultList.sort(resultListComparator); + + return new DecoratedSearchResultSet(resultList); + } + + private List getProblems(Context ctx, String humanQuery, String evalResult, DecoratedSearchResultSet queryResults, EdgeSearchQuery processedQuery) { + final List problems = new ArrayList<>(processedQuery.problems); + boolean siteSearch = processedQuery.domain != null; + + if (!siteSearch) { + if (queryResults.size() <= 5 && null == evalResult) { + spellCheckTerms(ctx, processedQuery).forEach(problems::add); + } + + if (queryResults.size() <= 5) { + problems.add("Try rephrasing the query, changing the word order or using synonyms to get different results. Tips."); + } + + if (humanQuery.toLowerCase().matches(".*(definition|define).*")) { + problems.add("Tip: Try using a query that looks like define:word if you want a dictionary definition"); + } + } + + if (humanQuery.contains("/")) { + problems.clear(); + problems.add("There is a known bug with search terms that contain a slash that causes them to be marked as unsupported; as a workaround, try using a dash instead. AC-DC will work, AC/DC does not."); + } + + return problems; + } + + + private EdgePageScoreAdjustment adjustScoreBasedOnQuery(EdgeUrlDetails p, EdgeSearchSpecification specs) { + String titleLC = p.title == null ? "" : p.title.toLowerCase(); + String descLC = p.description == null ? "" : p.description.toLowerCase(); + String urlLC = p.url == null ? "" : p.url.path.toLowerCase(); + String domainLC = p.url == null ? "" : p.url.domain.toString().toLowerCase(); + + String[] searchTermsLC = specs.subqueries.get(0).searchTermsInclude.stream() + .map(String::toLowerCase) + .flatMap(s -> Arrays.stream(s.split("_"))) + .toArray(String[]::new); + int termCount = searchTermsLC.length; + + String[] titleParts = titleLC.split("[:!|./]|(\\s-|-\\s)|\\s{2,}"); + double titleHitsAdj = 0.; + for (String titlePart : titleParts) { + titleHitsAdj += Arrays.stream(searchTermsLC).filter(titlePart::contains).mapToInt(String::length).sum() + / (double) Math.max(1, titlePart.trim().length()); + } + + double titleFullHit = 0.; + if (termCount > 1 && titleLC.contains(specs.humanQuery.replaceAll("\"", "").toLowerCase())) { + titleFullHit = termCount; + } + long descHits = Arrays.stream(searchTermsLC).filter(descLC::contains).count(); + long urlHits = Arrays.stream(searchTermsLC).filter(urlLC::contains).count(); + long domainHits = Arrays.stream(searchTermsLC).filter(domainLC::contains).count(); + + double descHitsAdj = 0.; + for (String word : descLC.split("[^\\w]+")) { + descHitsAdj += Arrays.stream(searchTermsLC) + .filter(term -> term.length() > word.length()) + .filter(term -> term.contains(word)) + .mapToDouble(term -> word.length() / (double) term.length()) + .sum(); + } + + return EdgePageScoreAdjustment.builder() + .descAdj(Math.min(termCount, descHits) / (10. * termCount)) + .descHitsAdj(descHitsAdj / 10.) + .domainAdj(2 * Math.min(termCount, domainHits) / (double) termCount) + .urlAdj(Math.min(termCount, urlHits) / (10. * termCount)) + .titleAdj(5 * titleHitsAdj / (Math.max(1, titleParts.length) * Math.log(titleLC.length() + 2))) + .titleFullHit(titleFullHit) + .build(); + } + + @NotNull + private Observable getWikiArticle(Context ctx, String humanQuery) { + return assistantClient + .encyclopediaLookup(ctx, + humanQuery.replaceAll("\\s+", "_") + .replaceAll("\"", "") + ).subscribeOn(Schedulers.io()); + } + + private void fetchResultsMulti(Context ctx, EdgeSearchQuery processedQuery, AccumulatedQueryResults queryResults, UrlDeduplicator deduplicator) { + + boolean debug = processedQuery.specs.subqueries.get(0).searchTermsExclude.contains("special:debug"); + + var blocksOrder = processedQuery.specs.subqueries.stream().map(sq -> sq.block).distinct().sorted(Comparator.comparing(block -> block.sortOrder)).toList(); + + EdgeSearchSpecification[] specsArray = + processedQuery.specs.subqueries.stream() + .filter(sq -> sq.block == IndexBlock.TitleKeywords) + .map(sq -> processedQuery.specs.withSubqueries(blocksOrder.stream().map(sq::withBlock).collect(Collectors.toList()))) + //.flatMap(specs -> processedQuery.specs.buckets.stream().map(bucket -> specs.withBuckets(List.of(bucket)))) + .toArray(EdgeSearchSpecification[]::new); + var resultSets = indexClient.multiQuery(ctx, specsArray); + + if (debug) { + for (var s : specsArray) { + logger.info("{}", s); + } + for (IndexBlock block : indexBlockSearchOrder) { + resultSets.forEach(res -> { + res.resultsList.getOrDefault(block, Collections.emptyList()).forEach(b2 -> { + b2.results.forEach((idx,items) -> { + items.forEach(i -> + logger.info("{} {} - {}", block, idx, i) + ); + }); + }); + }); + } + } + + Set> seenUrls = new HashSet<>(); + for (IndexBlock block : indexBlockSearchOrder) { + var resultsJoined = resultSets.stream().flatMap(rs -> rs.resultsList.getOrDefault(block, Collections.emptyList()).stream()) + .map(EdgeSearchResults::getResults) + .flatMap(m -> m.entrySet().stream()) + .flatMap(m -> m.getValue().stream()) + .sorted(Comparator.comparing(item -> preEvaluateItem(item, block))) + .filter(item -> seenUrls.add(item.url)) + .collect(Collectors.toList()); + + queryResults.append( 100, resultDecorator.decorateSearchResults(resultsJoined, block, deduplicator)); + + if (debug) { + logger.info("{} -> {} items", resultsJoined, queryResults.size()); + } + + } + if (debug) { + logger.info("-> {} items", queryResults.size()); + } + + + } + + private final WeakHashMap scoreCache = new WeakHashMap<>(); + private double preEvaluateItem(EdgeSearchResultItem item, IndexBlock block) { + synchronized (scoreCache) { + return scoreCache.computeIfAbsent(item, i -> valuator.evaluateTerms(i.scores, block, 1000)); + } + } + + private void fetchResultsSimple(Context ctx, EdgeSearchQuery processedQuery, AccumulatedQueryResults queryResults, UrlDeduplicator deduplicator) { + var resultSet = indexClient.query(ctx, processedQuery.specs); + + logger.debug("{}", resultSet); + + for (IndexBlock block : indexBlockSearchOrder) { + for (var results : resultSet.resultsList.getOrDefault(block, Collections.emptyList())) { + var items = results.getAllItems(); + queryResults.append(100, resultDecorator.decorateSearchResults(items, block, deduplicator)); + } + } + } + + static IndexBlock[] indexBlockSearchOrder = Arrays.stream(IndexBlock.values()).sorted(Comparator.comparing(i -> i.sortOrder)).toArray(IndexBlock[]::new); + + private Iterable spellCheckTerms(Context ctx, EdgeSearchQuery disjointedQuery) { + return Observable.fromIterable(disjointedQuery.searchTermsHuman) + .subscribeOn(Schedulers.io()) + .flatMap(term -> assistantClient.spellCheck(ctx, term) + .onErrorReturn(e -> Collections.emptyList()) + .filter(results -> hasSpellSuggestions(term, results)) + .map(suggestions -> searchTermToProblemDescription(term, suggestions)) + ) + .blockingIterable(); + } + + private boolean hasSpellSuggestions(String term, List results) { + if (results.size() > 1) { + return true; + } + else if (results.size() == 1) { + return !term.equalsIgnoreCase(results.get(0)); + } + return false; + } + + private String searchTermToProblemDescription(String term, List suggestions) { + return "\"" + term + "\" could be spelled " + + suggestions.stream().map(s -> "\""+s+"\"").collect(Collectors.joining(", ")); + } + + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchProfile.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchProfile.java new file mode 100644 index 00000000..05fcaa04 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchProfile.java @@ -0,0 +1,68 @@ +package nu.marginalia.wmsa.edge.search; + +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.index.service.SearchOrder; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public enum EdgeSearchProfile { + DEFAULT("default", SearchOrder.ASCENDING, + Collections.emptyList(), + List.of(IndexBlock.TitleKeywords, IndexBlock.Title, IndexBlock.Top, IndexBlock.Middle, IndexBlock.Low, IndexBlock.Link, IndexBlock.Words, IndexBlock.NamesWords), + 0, 1), + MODERN("modern", SearchOrder.ASCENDING, + Collections.emptyList(), + List.of(IndexBlock.TitleKeywords, IndexBlock.Title, IndexBlock.Top, IndexBlock.Middle, IndexBlock.Low, IndexBlock.Link, IndexBlock.Words, IndexBlock.NamesWords), + 2), + CORPO("corpo", SearchOrder.ASCENDING, + Collections.emptyList(), + List.of(IndexBlock.TitleKeywords, IndexBlock.Title, IndexBlock.Top, IndexBlock.Middle, IndexBlock.Low, IndexBlock.Link, IndexBlock.Words, IndexBlock.NamesWords), + 4, 5, 6, 7), + YOLO("yolo", SearchOrder.ASCENDING, + Collections.emptyList(), + List.of(IndexBlock.TitleKeywords, IndexBlock.Title, IndexBlock.Top, IndexBlock.Middle, IndexBlock.Low, IndexBlock.Link, IndexBlock.Words, IndexBlock.NamesWords), + 0, 2, 1, 3, 4, 6), + CORPO_CLEAN("corpo-clean", SearchOrder.ASCENDING, + Collections.emptyList(), + List.of(IndexBlock.TitleKeywords, IndexBlock.Title, IndexBlock.Top, IndexBlock.Middle, IndexBlock.Low, IndexBlock.Link, IndexBlock.Words, IndexBlock.NamesWords), + 4, 5), + ACADEMIA("academia", SearchOrder.ASCENDING, + Collections.emptyList(), + List.of(IndexBlock.TitleKeywords, IndexBlock.Title, IndexBlock.Top, IndexBlock.Middle, IndexBlock.Low, IndexBlock.Link, IndexBlock.Words, IndexBlock.NamesWords), + 3), + ; + + + public final String name; + public final SearchOrder order; + public final List additionalSearchTerm; + public final List buckets; + public final List indexBlocks; + + EdgeSearchProfile(String name, SearchOrder order, + List additionalSearchTerm, + List indexBlocks, + int... buckets) { + this.name = name; + this.order = order; + this.additionalSearchTerm = additionalSearchTerm; + this.indexBlocks = indexBlocks; + this.buckets = Arrays.stream(buckets).boxed().collect(Collectors.toList()); + } + + static EdgeSearchProfile getSearchProfile(String param) { + if (null == param) { + return YOLO; + } + return switch (param) { + case "modern" -> MODERN; + case "default" -> DEFAULT; + case "corpo" -> CORPO; + case "academia" -> ACADEMIA; + default -> YOLO; + }; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchRankingSymbols.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchRankingSymbols.java new file mode 100644 index 00000000..43ffd0a8 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchRankingSymbols.java @@ -0,0 +1,34 @@ +package nu.marginalia.wmsa.edge.search; + +import java.util.TreeMap; + +public class EdgeSearchRankingSymbols { + + private static final TreeMap symbols; + static { + symbols = new TreeMap<>(); + symbols.put(1.0, new RankingSymbol("⭐", "Fits search terms very well")); + symbols.put(2.0, new RankingSymbol("🟢", "Fits search terms well")); + symbols.put(4.0, new RankingSymbol("🟡", "Fits search terms decently")); + symbols.put(6.0, new RankingSymbol("🟠", "Could fit search terms")); + symbols.put(100.0, new RankingSymbol("🟤", "Poor fit for search terms, grasping at straws")); + } + + public static String getRankingSymbol(double termScore) { + return forScore(termScore).symbol; + } + public static String getRankingSymbolDescription(double termScore) { + return forScore(termScore).description; + } + + private static RankingSymbol forScore(double score) { + var e = symbols.ceilingEntry(score); + if (e == null) { + e = symbols.lastEntry(); + } + return e.getValue(); + } + + private record RankingSymbol(String symbol, String description) { + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchService.java new file mode 100644 index 00000000..c7e8bbda --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/EdgeSearchService.java @@ -0,0 +1,369 @@ +package nu.marginalia.wmsa.edge.search; + +import com.google.common.base.Strings; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.inject.Inject; +import com.google.inject.name.Named; +import io.reactivex.rxjava3.schedulers.Schedulers; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.auth.api.model.ApiSearchResult; +import nu.marginalia.wmsa.auth.api.model.ApiSearchResults; +import nu.marginalia.wmsa.client.exception.TimeoutException; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.configuration.server.MetricsServer; +import nu.marginalia.wmsa.configuration.server.Service; +import nu.marginalia.wmsa.data_store.client.DataStoreClient; +import nu.marginalia.wmsa.data_store.meta.DomainInformation; +import nu.marginalia.wmsa.edge.assistant.client.AssistantClient; +import nu.marginalia.wmsa.edge.assistant.dict.DictionaryResponse; +import nu.marginalia.wmsa.edge.assistant.screenshot.ScreenshotService; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDao; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDomainBlacklist; +import nu.marginalia.wmsa.edge.index.client.EdgeIndexClient; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeId; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import nu.marginalia.wmsa.edge.search.query.model.EdgeUserSearchParameters; +import nu.marginalia.wmsa.renderer.mustache.MustacheRenderer; +import nu.marginalia.wmsa.renderer.mustache.RendererFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Request; +import spark.Response; +import spark.Spark; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class EdgeSearchService extends Service { + + private final EdgeDataStoreDao edgeDataStoreDao; + private final EdgeIndexClient indexClient; + private final DataStoreClient dataStoreClient; + private final AssistantClient assistantClient; + private final UnitConversion unitConversion; + private final EdgeSearchOperator searchOperator; + private final EdgeDomainBlacklist blacklist; + private final ScreenshotService screenshotService; + + private final MustacheRenderer browseResultsRenderer; + private final MustacheRenderer searchResultsRenderer; + private final MustacheRenderer searchResultsRendererGmi; + private final MustacheRenderer dictionaryRenderer; + private final MustacheRenderer dictionaryRendererGmi; + private final MustacheRenderer> conversionRenderer; + private final MustacheRenderer> conversionRendererGmi; + + private final MustacheRenderer siteInfoRenderer; + private final MustacheRenderer siteInfoRendererGmi; + + private final Gson gson = new GsonBuilder().create(); + + private static final Logger logger = LoggerFactory.getLogger(EdgeSearchService.class); + volatile private int indexSize = 0; + + private final String maintenanceMessage = null; + + @SneakyThrows + @Inject + public EdgeSearchService(@Named("service-host") String ip, + @Named("service-port") Integer port, + EdgeDataStoreDao edgeDataStoreDao, + EdgeIndexClient indexClient, + RendererFactory rendererFactory, + Initialization initialization, + MetricsServer metricsServer, + DataStoreClient dataStoreClient, + AssistantClient assistantClient, + UnitConversion unitConversion, + EdgeSearchOperator searchOperator, + EdgeDomainBlacklist blacklist, + ScreenshotService screenshotService + ) { + super(ip, port, initialization, metricsServer); + this.edgeDataStoreDao = edgeDataStoreDao; + this.indexClient = indexClient; + + browseResultsRenderer = rendererFactory.renderer("edge/browse-results"); + + searchResultsRenderer = rendererFactory.renderer("edge/search-results"); + searchResultsRendererGmi = rendererFactory.renderer("edge/search-results-gmi"); + + dictionaryRenderer = rendererFactory.renderer("edge/dictionary-results"); + dictionaryRendererGmi = rendererFactory.renderer("edge/dictionary-results-gmi"); + + siteInfoRenderer = rendererFactory.renderer("edge/site-info"); + siteInfoRendererGmi = rendererFactory.renderer("edge/site-info-gmi"); + + conversionRenderer = rendererFactory.renderer("edge/conversion-results"); + conversionRendererGmi = rendererFactory.renderer("edge/conversion-results-gmi"); + + this.dataStoreClient = dataStoreClient; + this.assistantClient = assistantClient; + this.unitConversion = unitConversion; + this.searchOperator = searchOperator; + this.blacklist = blacklist; + this.screenshotService = screenshotService; + + Spark.staticFiles.expireTime(600); + + Spark.get("/search", this::pathSearch); + + Spark.get("/api/search", this::apiSearch, gson::toJson); + + Spark.get("/public/search", this::pathSearch); + Spark.get("/public/submit", this::pathSubmit); + + Spark.get("/site-search/:site/*", this::siteSearchRedir); + Spark.get("/public/site-search/:site/*", this::siteSearchRedir); + + Spark.exception(Exception.class, (e,p,q) -> { + logger.error("Error during processing", e); + serveError(Context.fromRequest(p), q); + }); + + Spark.awaitInitialization(); + } + + private Object siteSearchRedir(Request request, Response response) { + final String site = request.params("site"); + final String queryRaw = request.splat()[0]; + + final String query = URLEncoder.encode(String.format("%s site:%s", queryRaw, site), StandardCharsets.UTF_8); + final String profile = request.queryParamOrDefault("profile", "yolo"); + + response.redirect("https://search.marginalia.nu/search?query="+query+"&profile="+profile); + + return null; + } + + + private void serveError(Context ctx, Response rsp) { + boolean isIndexUp = indexClient.isAlive(); + + try { + if (!isIndexUp) { + rsp.body("Error

    Error

    Oops! It appears the index server is offline.

    The server was probably restarted to bring online some changes. Restarting the index typically takes a few minutes, during which searches can't be served.

    This page will attempt to refresh automatically every few seconds.

    "); + } else if (indexClient.isBlocked(ctx).blockingFirst()) { + rsp.body("Error

    Error

    Oops! It appears the index server is starting up.

    The server was probably restarted to bring online some changes. Restarting the index typically takes a few minutes, during which searches can't be served.

    This page will attempt to refresh automatically every few seconds.

    "); + } + else { + rsp.body("Error

    Error

    Oops! An unknown error occurred. The index server seems to be up, so I don't know why this is. Please send an email to kontakt@marginalia.nu telling me what you did :-)

    "); + } + } + catch (Exception ex) { + logger.error("Error", ex); + rsp.body("Error

    Error

    Oops! It appears the index server is unresponsive.

    The server was probably restarted to bring online some changes. Restarting the index typically takes a few minutes, during which searches can't be served.

    This page will attempt to refresh automatically every few seconds.

    "); + } + + } + + @SneakyThrows + private Object pathSubmit(Request request, Response response) { + String url = request.queryString(); + + var urlToSubmit = new EdgeUrl(url); + + logger.info("Submitting {}", url); + edgeDataStoreDao.putUrl(0, urlToSubmit); + + return "ok"; + } + + @SneakyThrows + private Object apiSearch(Request request, Response response) { + + final var ctx = Context.fromRequest(request); + final String queryParam = request.queryParams("query"); + final int limit; + EdgeSearchProfile profile = EdgeSearchProfile.YOLO; + + String count = request.queryParamOrDefault("count", "20"); + limit = Integer.parseInt(count); + + String index = request.queryParamOrDefault("index", "0"); + if (!Strings.isNullOrEmpty(index)) { + profile = switch (index) { + case "0" -> EdgeSearchProfile.YOLO; + case "1" -> EdgeSearchProfile.MODERN; + case "2" -> EdgeSearchProfile.DEFAULT; + case "3" -> EdgeSearchProfile.CORPO_CLEAN; + default -> EdgeSearchProfile.CORPO_CLEAN; + }; + } + + final String humanQuery = queryParam.trim(); + + var results = searchOperator.doApiSearch(ctx, new EdgeUserSearchParameters(humanQuery, profile, "")); + + return new ApiSearchResults("RESTRICTED", humanQuery, results.stream().map(ApiSearchResult::new).limit(limit).collect(Collectors.toList())); + } + + @SneakyThrows + private Object pathSearch(Request request, Response response) { + + final var ctx = Context.fromRequest(request); + + final String queryParam = request.queryParams("query"); + if (null == queryParam || queryParam.isBlank()) { + response.redirect("https://search.marginalia.nu/"); + return null; + } + + final String profileStr = Optional.ofNullable(request.queryParams("profile")).orElse("yolo"); + + try { + final String humanQuery = queryParam.trim(); + final String format = request.queryParams("format"); + + var eval = unitConversion.tryEval(ctx, humanQuery); + var conversion = unitConversion.tryConversion(ctx, humanQuery); + if (conversion.isPresent()) { + if ("gmi".equals(format)) { + response.type("text/gemini"); + return conversionRendererGmi.render(Map.of("query", humanQuery, "result", conversion.get())); + } else { + return conversionRenderer.render(Map.of("query", humanQuery, "result", conversion.get(), "profile", profileStr)); + } + } + if (humanQuery.matches("define:[A-Za-z\\s-0-9]+")) { + var results = lookupDefinition(ctx, humanQuery); + + if ("gmi".equals(format)) { + response.type("text/gemini"); + return dictionaryRendererGmi.render(results, Map.of("query", humanQuery)); + } else { + return dictionaryRenderer.render(results, Map.of("query", humanQuery, "profile", profileStr)); + } + } else if (humanQuery.matches("site:[.A-Za-z\\-0-9]+")) { + var results = siteInfo(ctx, humanQuery); + + + var domain = results.getDomain(); + logger.info("Domain: {}", domain); + + DecoratedSearchResultSet resultSet; + Path screenshotPath = null; + if (null != domain) { + resultSet = searchOperator.performDumbQuery(ctx, EdgeSearchProfile.CORPO, IndexBlock.Words, 100, 100, "site:"+domain); + + screenshotPath = Path.of("/screenshot/" + edgeDataStoreDao.getDomainId(domain).getId()); + } + else { + resultSet = new DecoratedSearchResultSet(Collections.emptyList()); + } + + if ("gmi".equals(format)) { + response.type("text/gemini"); + return siteInfoRendererGmi.render(results, Map.of("query", humanQuery)); + } else { + return siteInfoRenderer.render(results, Map.of("query", humanQuery, "focusDomain", Objects.requireNonNullElse(domain, ""), "profile", profileStr, "results", resultSet.resultSet, "screenshot", screenshotPath == null ? "" : screenshotPath.toString())); + } + } else if (humanQuery.matches("browse:[.A-Za-z\\-0-9]+")) { + var results = browseSite(ctx, humanQuery); + + if (null != results) { + return browseResultsRenderer.render(results, Map.of("query", humanQuery, "profile", profileStr)); + } + } + + + final var jsSetting = Optional.ofNullable(request.queryParams("js")).orElse("default"); + var results = searchOperator.doSearch(ctx, new EdgeUserSearchParameters(humanQuery, + EdgeSearchProfile.getSearchProfile(profileStr), jsSetting), eval.orElse(null) + ); + + results.getResults().removeIf(detail -> blacklist.isBlacklisted(edgeDataStoreDao.getDomainId(detail.url.domain))); + + if ("gmi".equals(format)) { + response.type("text/gemini"); + return searchResultsRendererGmi.render(results); + } else { + if (maintenanceMessage != null) { + return searchResultsRenderer.render(results, Map.of("maintenanceMessage", maintenanceMessage)); + } + else { + return searchResultsRenderer.render(results); + } + } + } + catch (TimeoutException te) { + serveError(ctx, response); + return null; + } + catch (Exception ex) { + logger.error("Error", ex); + serveError(ctx, response); + return null; + } + } + + private DomainInformation siteInfo(Context ctx, String humanQuery) { + String definePrefix = "site:"; + String word = humanQuery.substring(definePrefix.length()).toLowerCase(); + + logger.info("Fetching Site Info: {}", word); + try { + var results = dataStoreClient + .siteInfo(ctx, word) + .blockingFirst(); + logger.debug("Results = {}", results); + + return results; + } + catch (Exception ex) { + logger.debug("No Results"); + + return new DomainInformation(null, false, 0, 0, 0, 0, 0, 0, 0, EdgeDomainIndexingState.UNKNOWN, Collections.emptyList()); + } + + } + + private BrowseResultSet browseSite(Context ctx, String humanQuery) { + String definePrefix = "browse:"; + String word = humanQuery.substring(definePrefix.length()).toLowerCase(); + + try { + if ("random".equals(word)) { + var results = edgeDataStoreDao.getRandomDomains(25, blacklist); + results.removeIf(res -> !screenshotService.hasScreenshot(new EdgeId<>(res.domainId))); + return new BrowseResultSet(results); + } + else { + var domain = edgeDataStoreDao.getDomainId(new EdgeDomain(word)); + var neighbors = edgeDataStoreDao.getDomainNeighborsAdjacent(domain, blacklist, 45); + + neighbors.removeIf(res -> !screenshotService.hasScreenshot(new EdgeId<>(res.domainId))); + + return new BrowseResultSet(neighbors); + } + } + catch (Exception ex) { + logger.info("No Results"); + return null; + } + } + + @SneakyThrows + private DictionaryResponse lookupDefinition(Context ctx, String humanQuery) { + String definePrefix = "define:"; + String word = humanQuery.substring(definePrefix.length()).toLowerCase(); + + logger.info("Defining: {}", word); + var results = assistantClient + .dictionaryLookup(ctx, word) + .blockingFirst(); + logger.debug("Results = {}", results); + + return results; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/UnitConversion.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/UnitConversion.java new file mode 100644 index 00000000..15cbafeb --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/UnitConversion.java @@ -0,0 +1,77 @@ +package nu.marginalia.wmsa.edge.search; + +import nu.marginalia.wmsa.client.exception.RemoteException; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.edge.assistant.client.AssistantClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +@Singleton +public class UnitConversion { + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Pattern conversionPattern = Pattern.compile("((\\d+|\\s+|[.()\\-^+%*/]|log[^a-z]|log2[^a-z]|sqrt[^a-z]|log10|cos[^a-z]|sin[^a-z]|tan[^a-z]|log2|pi[^a-z]|e[^a-z]|2pi[^a-z])+)\\s*([a-zA-Z][a-zA-Z^.0-9]*\\s?[a-zA-Z^.0-9]*)\\s+in\\s+([a-zA-Z^.0-9]+\\s?[a-zA-Z^.0-9]*)"); + private final Predicate evalPredicate = Pattern.compile("(\\d+|\\s+|[.()\\-^+%*/]|log|log2|sqrt|log10|cos|sin|tan|pi|e|2pi)+").asMatchPredicate(); + + private final AssistantClient assistantClient; + + @Inject + public UnitConversion(AssistantClient assistantClient) { + this.assistantClient = assistantClient; + } + + public Optional tryConversion(Context context, String query) { + var matcher = conversionPattern.matcher(query); + if (!matcher.matches()) + return Optional.empty(); + + String value = matcher.group(1); + String from = matcher.group(3); + String to = matcher.group(4); + + logger.info("{} -> '{}' '{}' '{}'", query, value, from, to); + + try { + return Optional.of(assistantClient.unitConversion(context, value, from, to).blockingFirst()); + } + catch (RemoteException ex) { + return Optional.empty(); + } + } + + public boolean isNumeric(String str) { + try { + Double.parseDouble(str); + return true; + } + catch (NumberFormatException ex) { + return false; + } + } + + public Optional tryEval(Context context, String query) { + if (!evalPredicate.test(query)) { + return Optional.empty(); + } + + var expr = query.toLowerCase().trim(); + + if (expr.chars().allMatch(Character::isDigit)) { + return Optional.empty(); + } + + logger.info("eval({})", expr); + + try { + return Optional.of(assistantClient.evalMath(context, expr).blockingFirst()); + } + catch (RemoteException ex) { + return Optional.empty(); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/client/EdgeSearchClient.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/client/EdgeSearchClient.java new file mode 100644 index 00000000..53a29067 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/client/EdgeSearchClient.java @@ -0,0 +1,29 @@ +package nu.marginalia.wmsa.edge.search.client; + +import com.google.inject.Singleton; +import io.reactivex.rxjava3.core.Observable; +import nu.marginalia.wmsa.auth.api.model.ApiSearchResults; +import nu.marginalia.wmsa.client.AbstractDynamicClient; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.server.Context; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.CheckReturnValue; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +@Singleton +public class EdgeSearchClient extends AbstractDynamicClient { + private final Logger logger = LoggerFactory.getLogger(getClass()); + + public EdgeSearchClient() { + super(ServiceDescriptor.EDGE_SEARCH); + } + + @CheckReturnValue + public Observable query(Context ctx, String queryString, int count, int profile) { + return this.get(ctx, String.format("/api/search?query=%s&count=%d&index=%d", URLEncoder.encode(queryString, StandardCharsets.UTF_8), count, profile), ApiSearchResults.class); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/EnglishDictionary.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/EnglishDictionary.java new file mode 100644 index 00000000..b45cec7d --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/EnglishDictionary.java @@ -0,0 +1,161 @@ +package nu.marginalia.wmsa.edge.search.query; + +import com.google.inject.Inject; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class EnglishDictionary { + private final Set englishWords = new HashSet<>(); + private final NGramDict dict; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Inject + public EnglishDictionary(NGramDict dict) { + this.dict = dict; + try (var resource = Objects.requireNonNull(ClassLoader.getSystemResourceAsStream("dictionary/en-words"), + "Could not load word frequency table"); + var br = new BufferedReader(new InputStreamReader(resource)) + ) { + for (;;) { + String s = br.readLine(); + if (s == null) { + break; + } + englishWords.add(s.toLowerCase()); + } + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + public boolean isWord(String word) { + return englishWords.contains(word); + } + + private static Pattern ingPattern = Pattern.compile(".*(\\w)\\1ing$"); + + public Collection getWordVariants(String s) { + var variants = findWordVariants(s); + long freqBaseline = dict.getTermFreq(s); + + return variants.stream() + .filter(var -> freqBaseline*10 > dict.getTermFreq(var) && freqBaseline/10 < dict.getTermFreq(var) + ).collect(Collectors.toList()); + } + + + public Collection findWordVariants(String s) { + int sl = s.length(); + + if (sl < 2) { + return Collections.emptyList(); + } + if (s.endsWith("s")) { + String a = s.substring(0, sl-1); + String b = s + "es"; + if (isWord(a) && isWord(b)) { + return List.of(a, b); + } + else if (isWord(a)) { + return List.of(a); + } + else if (isWord(b)) { + return List.of(b); + } + } + if (s.endsWith("sm")) { + String a = s.substring(0, sl-1)+"t"; + String b = s.substring(0, sl-1)+"ts"; + if (isWord(a) && isWord(b)) { + return List.of(a, b); + } + else if (isWord(a)) { + return List.of(a); + } + else if (isWord(b)) { + return List.of(b); + } + } + if (s.endsWith("st")) { + String a = s.substring(0, sl-1)+"m"; + String b = s + "s"; + if (isWord(a) && isWord(b)) { + return List.of(a, b); + } + else if (isWord(a)) { + return List.of(a); + } + else if (isWord(b)) { + return List.of(b); + } + } + else if (ingPattern.matcher(s).matches() && sl > 4) { // humming, clapping + var a = s.substring(0, sl-4); + var b = s.substring(0, sl-3) + "ed"; + + if (isWord(a) && isWord(b)) { + return List.of(a, b); + } + else if (isWord(a)) { + return List.of(a); + } + else if (isWord(b)) { + return List.of(b); + } + } + else { + String a = s + "s"; + String b = ingForm(s); + String c = s + "ed"; + + if (isWord(a) && isWord(b) && isWord(c)) { + return List.of(a, b, c); + } + else if (isWord(a) && isWord(b)) { + return List.of(a, b); + } + else if (isWord(b) && isWord(c)) { + return List.of(b, c); + } + else if (isWord(a) && isWord(c)) { + return List.of(a, c); + } + else if (isWord(a)) { + return List.of(a); + } + else if (isWord(b)) { + return List.of(b); + } + else if (isWord(c)) { + return List.of(c); + } + } + + return Collections.emptyList(); + } + + public String ingForm(String s) { + if (s.endsWith("t") && !s.endsWith("tt")) { + return s + "ting"; + } + if (s.endsWith("n") && !s.endsWith("nn")) { + return s + "ning"; + } + if (s.endsWith("m") && !s.endsWith("mm")) { + return s + "ming"; + } + if (s.endsWith("r") && !s.endsWith("rr")) { + return s + "ring"; + } + return s + "ing"; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/QueryFactory.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/QueryFactory.java new file mode 100644 index 00000000..2dd9bab0 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/QueryFactory.java @@ -0,0 +1,186 @@ +package nu.marginalia.wmsa.edge.search.query; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.WordPatterns; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.model.search.EdgeSearchSpecification; +import nu.marginalia.wmsa.edge.model.search.EdgeSearchSubquery; +import nu.marginalia.wmsa.edge.search.EdgeSearchProfile; +import nu.marginalia.wmsa.edge.search.query.model.EdgeSearchQuery; +import nu.marginalia.wmsa.edge.search.query.model.EdgeUserSearchParameters; +import org.eclipse.jetty.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Spark; + +import java.util.*; + +@Singleton +public class QueryFactory { + + private final LanguageModels lm; + private final NGramDict dict; + private final EnglishDictionary englishDictionary; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Inject + public QueryFactory(LanguageModels lm, NGramDict dict, EnglishDictionary englishDictionary) { + this.lm = lm; + this.dict = dict; + + this.englishDictionary = englishDictionary; + } + + public QueryParser getParser() { + return new QueryParser(englishDictionary, new QueryVariants(lm ,dict, englishDictionary)); + } + + public EdgeSearchQuery createQuery(EdgeUserSearchParameters params) { + final var profile = params.getProfile(); + final var jsSetting = params.getJsSetting(); + + final var processedQuery = createQuery(getParser(), params); + + processedQuery.specs.experimental = EdgeSearchProfile.CORPO.equals(profile); + processedQuery.specs.stagger = EdgeSearchProfile.YOLO.equals(profile); + + List subqueries = new ArrayList<>(processedQuery.specs.subqueries.size() * profile.indexBlocks.size()); + + for (var sq : processedQuery.specs.subqueries) { + for (var block : profile.indexBlocks) { + subqueries.add(sq.withBlock(block)); + } + } + + processedQuery.specs.subqueries.clear(); + processedQuery.specs.subqueries.addAll(subqueries); + + processedQuery.specs.subqueries.forEach(sq -> { + sq.searchTermsInclude.addAll(profile.additionalSearchTerm); + if (jsSetting.equals("yes-js")) { + sq.searchTermsExclude.add("js:false"); + } + if (jsSetting.equals("no-js")) { + sq.searchTermsExclude.add("js:true"); + } + }); + + processedQuery.specs.subqueries.sort(Comparator.comparing(sq -> -sq.termSize()*2.3 + sq.block.sortOrder)); + + return processedQuery; + } + + + public EdgeSearchQuery createQuery(QueryParser queryParser, EdgeUserSearchParameters params) { + final var query = params.humanQuery; + final var profile = params.getProfile(); + + if (query.length() > 1000) { + Spark.halt(HttpStatus.BAD_REQUEST_400, "That's too much, man"); + } + + List searchTermsHuman = new ArrayList<>(); + List problems = new ArrayList<>(); + String domain = null; + + var basicQuery = queryParser.parse(query); + + if (basicQuery.size() >= 8) { + problems.add("Your search query is too long"); + basicQuery.clear(); + } + + for (Token t : basicQuery) { + if (t.type == TokenType.QUOT_TERM || t.type == TokenType.LITERAL_TERM) { + if (t.str.startsWith("site:")) { + t.str = normalizeDomainName(t.str); + } + + searchTermsHuman.addAll(toHumanSearchTerms(t)); + analyzeSearchTerm(problems, t); + } + } + + var queryPermutations = queryParser.permuteQueriesNew(basicQuery); + List subqueries = new ArrayList<>(); + + + for (var parts : queryPermutations) { + List searchTermsExclude = new ArrayList<>(); + List searchTermsInclude = new ArrayList<>(); + + for (Token t : parts) { + switch (t.type) { + case EXCLUDE_TERM: + searchTermsExclude.add(t.str); + break; + case LITERAL_TERM: // fallthrough; + case QUOT_TERM: + searchTermsInclude.add(t.str); + if (t.str.toLowerCase().startsWith("site:")) { + domain = t.str.substring("site:".length()); + } + + break; + default: + logger.warn("Unexpected token type {}", t); + } + } + + subqueries.add(new EdgeSearchSubquery(searchTermsInclude, searchTermsExclude, IndexBlock.TitleKeywords)); + } + + + var specsBuilder = EdgeSearchSpecification.builder() + .subqueries(subqueries) + .limitByBucket(50) + .limitTotal(100) + .searchOrder(profile.order) + .humanQuery(query) + .buckets(profile.buckets); + + if (domain != null) { + specsBuilder = specsBuilder.limitByDomain(100); + } else { + specsBuilder = specsBuilder.limitByDomain(2); + } + + EdgeSearchSpecification specs = specsBuilder.build(); + + return new EdgeSearchQuery(specs, searchTermsHuman, domain); + + } + + private String normalizeDomainName(String str) { + return str.toLowerCase(); + } + + private List toHumanSearchTerms(Token t) { + if (t.type == TokenType.LITERAL_TERM) { + return Arrays.asList(t.displayStr.split("\\s+")); + } + else if (t.type == TokenType.QUOT_TERM) { + return Arrays.asList(t.displayStr.replace("\"", "").split("\\s+")); + + } + return Collections.emptyList(); + } + + private void analyzeSearchTerm(List problems, Token term) { + final String word = term.str; + + if (word.length() < WordPatterns.MIN_WORD_LENGTH) { + problems.add("Search term \"" + term.displayStr + "\" too short"); + } + if (!word.contains("_") && word.length() >= WordPatterns.MAX_WORD_LENGTH) { + problems.add("Search term \"" + term.displayStr + "\" too long"); + } + if (!word.contains("_") && !WordPatterns.wordPattern.matcher(word.replaceAll("[_:]","")).matches()) { + problems.add("The term \"" + term.displayStr + "\" contains characters that are not currently supported"); + } + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/QueryParser.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/QueryParser.java new file mode 100644 index 00000000..ef0ce398 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/QueryParser.java @@ -0,0 +1,428 @@ +package nu.marginalia.wmsa.edge.search.query; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import nu.marginalia.wmsa.edge.crawler.domain.language.WordPatterns; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.stream.Stream.concat; + +public class QueryParser { + private static final Logger logger = LoggerFactory.getLogger(QueryParser.class); + + private final EnglishDictionary englishDictionary; + private final QueryVariants queryVariants; + + public QueryParser(EnglishDictionary englishDictionary, QueryVariants queryVariants) { + this.englishDictionary = englishDictionary; + this.queryVariants = queryVariants; + } + + public List parse(String query) { + List tokens = extractBasicTokens(query); + + for (int i = 0; i < tokens.size(); i++) { + var t = tokens.get(i); + if (t.type == TokenType.QUOT) { + tokens.set(i, new Token(TokenType.QUOT_TERM, + t.str.replaceAll("\\s+", WordPatterns.WORD_TOKEN_JOINER), + t.displayStr)); + } + else if (t.type == TokenType.LITERAL_TERM + && (t.str.endsWith(":")||t.str.endsWith(".")) + && t.str.length() > 1) + { + tokens.set(i, + new Token(TokenType.LITERAL_TERM, t.str.substring(0, t.str.length()-1), + t.displayStr)); + } + } + + for (int i = 0; i < tokens.size() - 1; i++) { + var t = tokens.get(i); + var tn = tokens.get(i+1); + + if (t.type == TokenType.MINUS) { + tokens.set(i, new Token(TokenType.EXCLUDE_TERM, tn.str, "-"+tn.str)); + tokens.remove(i+1); + } + } + + return tokens; + } + + private static final Pattern noisePattern = Pattern.compile("[(),]"); + + public List extractBasicTokens(String rawQuery) { + List tokens = new ArrayList<>(); + + String query = noisePattern.matcher(rawQuery).replaceAll(" "); + + for (int i = 0; i < query.length(); i++) { + int chr = query.charAt(i); + if ('"' == chr) { + int end = query.indexOf('"', i+1); + if (end == -1) { + end = query.length(); + } + tokens.add(new Token(TokenType.QUOT, + query.substring(i+1, end).toLowerCase(), + query.substring(i, Math.min(query.length(), end+1)))); + i = end; + } + else if ('\u201C' == chr) { + int end = query.indexOf('\u201D', i+1); + if (end == -1) { + end = query.length(); + } + tokens.add(new Token(TokenType.QUOT, + query.substring(i+1, end).toLowerCase(), + query.substring(i, Math.min(query.length(), end+1)))); + i = end; + } + else if ('-' == chr) { + tokens.add(new Token(TokenType.MINUS, "\"")); + } + else if (Character.isSpaceChar(chr)) { + // + } + else { + int end = query.indexOf(' ', i); + if (end == -1) { + end = query.length(); + } + tokens.add(new Token(TokenType.LITERAL_TERM, + query.substring(i, end).toLowerCase(), + query.substring(i, end))); + i = end; + } + } + return tokens; + } + + + public List> variantQueries(List items) { + int start = -1; + int end = items.size(); + + for (int i = 0; i < items.size(); i++) { + var token = items.get(i); + + if (start < 0) { + if (token.type == TokenType.LITERAL_TERM && WordPatterns.wordQualitiesPredicate.test(token.str)) { + start = i; + } + } + else { + if (token.type != TokenType.LITERAL_TERM || !WordPatterns.wordPredicateEither.test(token.str)) { + end = i; + break; + } + } + } + + if (start >= 0 && end - start > 1) { + List> variantParts = getVariantSearchTerms(items.subList(start, end)); + int s = start; + int e = end; + return variantParts.stream().map(part -> + concat(items.subList(0, s).stream(), concat(part.stream(), items.subList(e, items.size()).stream())) + .collect(Collectors.toList())) + .peek(lst -> lst.removeIf(this::isJunkWord)) + .limit(24) + .collect(Collectors.toList()); + } + else { + return List.of(items); + } + } + + + + public List> permuteQueries(List items) { + int start = -1; + int end = items.size(); + + for (int i = 0; i < items.size(); i++) { + var token = items.get(i); + + if (start < 0) { + if (token.type == TokenType.LITERAL_TERM && WordPatterns.wordQualitiesPredicate.test(token.str)) { + start = i; + } + } + else { + if (token.type != TokenType.LITERAL_TERM || !WordPatterns.wordPredicateEither.test(token.str)) { + end = i; + break; + } + } + } + + if (start >= 0 && end - start > 1) { + List> permuteParts = combineSearchTerms(items.subList(start, end)); + int s = start; + int e = end; + return permuteParts.stream().map(part -> + concat(items.subList(0, s).stream(), concat(part.stream(), items.subList(e, items.size()).stream())) + .collect(Collectors.toList())) + .peek(lst -> lst.removeIf(this::isJunkWord)) + .limit(24) + .collect(Collectors.toList()); + } + else { + return List.of(items); + } + } + + + public List> permuteQueriesNew(List items) { + int start = -1; + int end = items.size(); + + for (int i = 0; i < items.size(); i++) { + var token = items.get(i); + + if (start < 0) { + if (token.type == TokenType.LITERAL_TERM && WordPatterns.wordQualitiesPredicate.test(token.str)) { + start = i; + } + } + else { + if (token.type != TokenType.LITERAL_TERM || !WordPatterns.wordPredicateEither.test(token.str)) { + end = i; + break; + } + } + } + + if (start >= 0 && end - start >= 1) { + var result = queryVariants.getQueryVariants(items.subList(start, end)); + + logger.debug("{}", result); + + if (result.isEmpty()) { + logger.warn("Empty variants result, falling back on old code"); + return permuteQueries(items); + } + + List> basic = new ArrayList<>(); + + for (var query : result.faithful) { + var tokens = query.terms.stream().map(term -> new Token(TokenType.LITERAL_TERM, term)).collect(Collectors.toList()); + + basic.add(tokens); + } + for (var query : result.alternative) { + var tokens = query.terms.stream().map(term -> new Token(TokenType.LITERAL_TERM, term)).collect(Collectors.toList()); + + basic.add(tokens); + } + + int si = start; + int ei = end; + return basic.stream().map(part -> + concat(items.subList(0, si).stream(), concat(part.stream(), items.subList(ei, items.size()).stream())).collect(Collectors.toList())) + .collect(Collectors.toList()); + } + else { + return List.of(items); + } + } + + private boolean isJunkWord(Token token) { + if (WordPatterns.isStopWord(token.str) && + !token.str.matches("^(\\d+|([a-z]+:.*))$")) { + return true; + } + return switch (token.str) { + case "vs", "versus", "or", "and" -> true; + default -> false; + }; + } + + private List> getVariantSearchTerms(List subList) { + int size = subList.size(); + if (size < 1) { + return Collections.emptyList(); + } + else if (size == 1) { + if (WordPatterns.isStopWord(subList.get(0).str)) { + return Collections.emptyList(); + } + return getWordVariants(subList.get(0)).map(List::of).collect(Collectors.toList()); + } + + List> cdrs = getVariantSearchTerms(subList.subList(1, subList.size())); + List cars = getWordVariants(subList.get(0)).collect(Collectors.toList()); + + List> ret = new ArrayList<>(cars.size() * cdrs.size()); + for (var car : cars) { + if (ret.size() >= 32) { + break; + } + for (var cdr : cdrs) { + ret.add(List.of(joinTokens(prepend(car, cdr)))); + } + } + return ret; + } + + private Stream getWordVariants(Token token) { + var s = token.str; + int sl = s.length(); + Stream base = Stream.of(token); + Stream alternatives; + if (sl < 2) { + return base; + } + if (s.endsWith("s")) { + alternatives = Stream.of(s.substring(0, sl-1), s + "es"); + } + else if (s.matches(".*(\\w)\\1ing$") && sl > 4) { // humming, clapping + var basea = s.substring(0, sl-4); + var baseb = s.substring(0, sl-3); + alternatives = Stream.of(basea, baseb + "ed"); + } + else { + alternatives = Stream.of(s+"s", s+"ing", s+"ed"); + } + + return Stream.concat(Stream.of(token), alternatives.filter(englishDictionary::isWord).map(str -> new Token(token.type, str, token.displayStr))); + } + + private List prepend(Token t, List lst) { + List ret = new ArrayList<>(lst.size() + 1); + ret.add(t); + ret.addAll(lst); + return ret; + } + + private List> combineSearchTerms(List subList) { + int size = subList.size(); + if (size < 1) { + return Collections.emptyList(); + } + else if (size == 1) { + if (WordPatterns.isStopWord(subList.get(0).str)) { + return Collections.emptyList(); + } + return List.of(subList); + } + + List> results = new ArrayList<>(size*(size+1)/2); + + if (subList.size() <= 4 && subList.get(0).str.length() >= 2 && !isPrefixWord(subList.get(subList.size()-1).str)) { + results.add(List.of(joinTokens(subList))); + } +outer: for (int i = size - 1; i >= 1; i--) { + + var left = combineSearchTerms(subList.subList(0, i)); + var right = combineSearchTerms(subList.subList(i, size)); + + for (var l : left) { + if (results.size() > 48) { + break outer; + } + + for (var r : right) { + if (results.size() > 48) { + break outer; + } + + List combined = new ArrayList<>(l.size() + r.size()); + combined.addAll(l); + combined.addAll(r); + if (!results.contains(combined)) { + results.add(combined); + } + } + } + } + if (!results.contains(subList)) { + results.add(subList); + } + Comparator> tc = (o1, o2) -> { + int dJoininess = o2.stream().mapToInt(s->(int)Math.pow(joininess(s.str), 2)).sum() - + o1.stream().mapToInt(s->(int)Math.pow(joininess(s.str), 2)).sum(); + if (dJoininess == 0) { + return (o2.stream().mapToInt(s->(int)Math.pow(rightiness(s.str), 2)).sum() - + o1.stream().mapToInt(s->(int)Math.pow(rightiness(s.str), 2)).sum()); + } + return (int) Math.signum(dJoininess); + }; + results.sort(tc); + return results; + } + + private boolean isPrefixWord(String str) { + return switch (str) { + case "the", "of", "when" -> true; + default -> false; + }; + } + + private boolean isSuffixWord(String str) { + return (str.length() < 2); + } + + + int joininess(String s) { + return (int) s.chars().filter(c -> c == '_').count(); + } + int rightiness(String s) { + int rightiness = 0; + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == '_') { + rightiness+=i; + } + } + return rightiness; + } + + private Token joinTokens(List subList) { + return new Token(TokenType.LITERAL_TERM, + subList.stream().map(t -> t.str).collect(Collectors.joining("_")), + subList.stream().map(t -> t.str).collect(Collectors.joining(" "))); + } +} + +@ToString @EqualsAndHashCode +class Token { + public final TokenType type; + public String str; + public final String displayStr; + + Token(TokenType type, String str, String displayStr) { + this.type = type; + this.str = str; + this.displayStr = safeString(displayStr); + } + + + Token(TokenType type, String str) { + this.type = type; + this.str = str; + this.displayStr = safeString(str); + } + + private static String safeString(String s) { + return s.replaceAll("<", "<") + .replaceAll(">", ">"); + } +} + +enum TokenType { + TERM, + QUOT, + MINUS, + LITERAL_TERM, + QUOT_TERM, + EXCLUDE_TERM, +}; \ No newline at end of file diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/QueryVariants.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/QueryVariants.java new file mode 100644 index 00000000..92b6cc7e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/QueryVariants.java @@ -0,0 +1,396 @@ +package nu.marginalia.wmsa.edge.search.query; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.KeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentSentence; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.WordSpan; +import opennlp.tools.stemmer.PorterStemmer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.regex.Pattern; + +@Singleton +public class QueryVariants { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final KeywordExtractor keywordExtractor; + private final SentenceExtractor sentenceExtractor; + private final NGramDict dict; + private final PorterStemmer ps = new PorterStemmer(); + + private final static int MAX_NGRAM_LENGTH = 4; + private final EnglishDictionary englishDictionary; + + @Inject + public QueryVariants(LanguageModels lm, NGramDict dict, EnglishDictionary englishDictionary) { + this.englishDictionary = englishDictionary; + this.keywordExtractor = new KeywordExtractor(); + this.sentenceExtractor = new SentenceExtractor(lm); + this.dict = dict; + } + + + final Pattern numWordBoundary = Pattern.compile("[0-9][a-zA-Z]|[a-zA-Z][0-9]"); + final Pattern dashBoundary = Pattern.compile("-"); + + @AllArgsConstructor + private static class Word { + public final String stemmed; + public final String word; + public final String wordOriginal; + } + + @AllArgsConstructor @Getter @ToString + public static class QueryVariant { + public final List terms; + public final double value; + } + + @Getter @ToString + public static class QueryVariantSet { + List faithful = new ArrayList<>(); + List alternative = new ArrayList<>(); + + public boolean isEmpty() { + return faithful.isEmpty() && alternative.isEmpty(); + } + } + + public QueryVariantSet getQueryVariants(List query) { + final String queryAsString = joinQuery(query); + + final TreeMap> byStart = new TreeMap<>(); + + logger.debug("QAS: {}", queryAsString); + + var sentence = sentenceExtractor.extractSentence(queryAsString); + + for (int i = 0; i < sentence.posTags.length; i++) { + if (sentence.posTags[i].startsWith("N") || sentence.posTags[i].startsWith("V")) { + sentence.posTags[i] = "NNP"; + } + else if ("JJ".equals(sentence.posTags[i]) || "CD".equals(sentence.posTags[i]) || sentence.posTags[i].startsWith("P")) { + sentence.posTags[i] = "NNP"; + sentence.setIsStopWord(i, false); + } + } + + for (var kw : keywordExtractor.getKeywordsFromSentence(sentence)) { + byStart.computeIfAbsent(kw.start, k -> new ArrayList<>()).add(kw); + } + + final List> livingSpans = new ArrayList<>(); + + var first = byStart.firstEntry(); + if (first == null) { + byStart.put(0, List.of(new WordSpan(0, sentence.length()))); + } + else if (first.getKey() > 0) { + List elongatedFirstWords = new ArrayList<>(first.getValue().size()); + + first.getValue().forEach(span -> { + elongatedFirstWords.add(new WordSpan(0, span.end)); + }); + + byStart.put(0, elongatedFirstWords); + } + + final List> goodSpans = getWordSpans(byStart, sentence, livingSpans); + + List> faithfulQueries = new ArrayList<>(); + List> alternativeQueries = new ArrayList<>(); + + for (var ls : goodSpans) { + faithfulQueries.addAll(createTokens(ls)); + } + + for (var span : goodSpans) { + alternativeQueries.addAll(joinTerms(span)); +// alternativeQueries.addAll(swapTerms(span)); + } + + for (var ls : goodSpans) { + var last = ls.get(ls.size() - 1); + + if (!last.wordOriginal.isBlank() && !Character.isUpperCase(last.wordOriginal.charAt(0))) { + var altLast = englishDictionary.getWordVariants(last.word); + for (String s : altLast) { + List newList = new ArrayList<>(ls.size()); + for (int i = 0; i < ls.size() - 1; i++) { + newList.add(ls.get(i).word); + } + newList.add(s); + alternativeQueries.add(newList); + } + } + + } + QueryVariantSet returnValue = new QueryVariantSet(); + returnValue.faithful.addAll(evaluateQueries(faithfulQueries)); + returnValue.faithful.addAll(evaluateQueries(alternativeQueries)); + + returnValue.faithful.sort(Comparator.comparing(QueryVariant::getValue)); + returnValue.alternative.sort(Comparator.comparing(QueryVariant::getValue)); + + return returnValue; + } + + Pattern underscore = Pattern.compile("_"); + + private List evaluateQueries(List> queryStrings) { + List ret = new ArrayList<>(); + for (var lst : queryStrings) { + double q = 0; + for (var word : lst) { + String[] parts = underscore.split(word); + StringJoiner combined = new StringJoiner("_"); + for (String part : parts) { + combined.add(ps.stem(part)); + } + q += Math.log(1 + dict.getTermFreqStemmed(combined.toString())); + } + ret.add(new QueryVariant(lst, q)); + } + return ret; + } + + private Collection> createTokens(List ls) { + List asTokens = new ArrayList<>(); + List> ret = new ArrayList<>(); + + + boolean dash = false; + boolean num = false; + + for (var span : ls) { + dash |= dashBoundary.matcher(span.word).find(); + num |= numWordBoundary.matcher(span.word).find(); + if (ls.size() == 1 || !isOmittableWord(span.word)) { + asTokens.add(span.word); + } + }; + ret.add(asTokens); + + if (dash) { + ret.addAll(combineDashWords(ls)); + } + + if (num) { + ret.addAll(splitWordNum(ls)); + } + + return ret; + } + + private boolean isOmittableWord(String word) { + return switch (word) { + case "vs", "or", "and", "versus", "is", "the", "why", "when", "if", "who", "are", "am" -> true; + default -> false; + }; + } + + private Collection> splitWordNum(List ls) { + List asTokens2 = new ArrayList<>(); + + boolean num = false; + + for (var span : ls) { + var wordMatcher = numWordBoundary.matcher(span.word); + var stemmedMatcher = numWordBoundary.matcher(span.stemmed); + + int ws = 0; + int ss = 0; + boolean didSplit = false; + while (wordMatcher.find(ws) && stemmedMatcher.find(ss)) { + ws = wordMatcher.start()+1; + ss = stemmedMatcher.start()+1; + if (dict.getTermFreqStemmed(splitAtNumBoundaryAndStem(span.word, stemmedMatcher.start(), "_")) > 0 + || dict.getTermFreqStemmed(splitAtNumBoundaryAndStem(span.word, stemmedMatcher.start(), "-")) > 0) + { + String combined = splitAtNumBoundary(span.word, wordMatcher.start(), "_"); + asTokens2.add(combined); + didSplit = true; + num = true; + } + } + + if (!didSplit) { + asTokens2.add(span.word); + } + }; + + if (num) { + return List.of(asTokens2); + } + return Collections.emptyList(); + } + + private Collection> combineDashWords(List ls) { + List asTokens2 = new ArrayList<>(); + boolean dash = false; + + for (var span : ls) { + var matcher = dashBoundary.matcher(span.word); + if (matcher.find() && dict.getTermFreqStemmed(ps.stem(dashBoundary.matcher(span.word).replaceAll(""))) > 0) { + dash = true; + String combined = dashBoundary.matcher(span.word).replaceAll(""); + asTokens2.add(combined); + } + else { + asTokens2.add(span.word); + } + }; + + if (dash) { + return List.of(asTokens2); + } + return Collections.emptyList(); + } + + private String splitAtNumBoundary(String in, int splitPoint, String joiner) { + return in.substring(0, splitPoint+1) + joiner + in.substring(splitPoint+1); + } + + private String splitAtNumBoundaryAndStem(String in, int splitPoint, String joiner) { + return ps.stem(in.substring(0, splitPoint+1)) + joiner + ps.stem(in.substring(splitPoint+1)); + } + + private List> getWordSpans(TreeMap> byStart, DocumentSentence sentence, List> livingSpans) { + List> goodSpans = new ArrayList<>(); + for (int i = 0; i < sentence.length(); i++) { + var spans = byStart.get(i); + + + if (spans == null ) + continue; + + for (var span : spans) { + ArrayList fragment = new ArrayList<>(); + fragment.add(span); + livingSpans.add(fragment); + } + + if (sentence.posTags[i].startsWith("N") || sentence.posTags[i].startsWith("V")) break; + } + + + while (!livingSpans.isEmpty()) { + + final List> newLivingSpans = new ArrayList<>(livingSpans.size()); + + for (var span : livingSpans) { + int end = span.get(span.size()-1).end; + + if (end == sentence.length()) { + var gs = new ArrayList(span.size()); + for (var s : span) { + gs.add(new Word(sentence.constructStemmedWordFromSpan(s), sentence.constructWordFromSpan(s), + s.size() == 1 ? sentence.words[s.start] : "")); + } + goodSpans.add(gs); + } + var nextWordsKey = byStart.ceilingKey(end); + + if (null == nextWordsKey) + continue; + + for (var next : byStart.get(nextWordsKey)) { + var newSpan = new ArrayList(span.size() + 1); + newSpan.addAll(span); + newSpan.add(next); + newLivingSpans.add(newSpan); + } + } + + livingSpans.clear(); + livingSpans.addAll(newLivingSpans); + } + + return goodSpans; + } + + private List> swapTerms(List span) { + List> ret = new ArrayList<>(); + + for (int i = 0; i < span.size()-1; i++) { + var a = span.get(i); + var b = span.get(i+1); + + var stemmed = b.stemmed + "_" + a.stemmed; + + if (dict.getTermFreqStemmed(stemmed) > 0) { + List asTokens = new ArrayList<>(); + + for (int j = 0; j < i; j++) { + var word = span.get(j).word; + asTokens.add(word); + } + { + var word = b.word + "_" + a.word; + asTokens.add(word); + } + for (int j = i+2; j < span.size(); j++) { + var word = span.get(j).word; + asTokens.add(word); + } + + ret.add(asTokens); + } + } + + return ret; + } + + + private List> joinTerms(List span) { + List> ret = new ArrayList<>(); + + for (int i = 0; i < span.size()-1; i++) { + var a = span.get(i); + var b = span.get(i+1); + + var stemmed = ps.stem(a.word + b.word); + + double scoreCombo = dict.getTermFreqStemmed(stemmed); + if (scoreCombo > 0) { + List asTokens = new ArrayList<>(); + + for (int j = 0; j < i; j++) { + var word = span.get(j).word; + asTokens.add(word); + } + { + var word = a.word + b.word; + asTokens.add(word); + } + for (int j = i+2; j < span.size(); j++) { + var word = span.get(j).word; + asTokens.add(word); + } + + ret.add(asTokens); + } + } + + return ret; + } + + private String joinQuery(List query) { + StringJoiner s = new StringJoiner(" "); + + for (var t : query) { + s.add(t.displayStr); + } + + return s.toString(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/model/EdgeSearchQuery.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/model/EdgeSearchQuery.java new file mode 100644 index 00000000..5cdc8892 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/model/EdgeSearchQuery.java @@ -0,0 +1,21 @@ +package nu.marginalia.wmsa.edge.search.query.model; + +import lombok.AllArgsConstructor; +import nu.marginalia.wmsa.edge.model.search.EdgeSearchSpecification; + +import java.util.*; + +@AllArgsConstructor +public class EdgeSearchQuery { + public final EdgeSearchSpecification specs; + + public final Set problems = new TreeSet<>(); + public final List searchTermsHuman; + public String domain; + + public EdgeSearchQuery(EdgeSearchSpecification justSpecs) { + searchTermsHuman = new ArrayList<>(); + specs = justSpecs; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/model/EdgeUserSearchParameters.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/model/EdgeUserSearchParameters.java new file mode 100644 index 00000000..ee58f099 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/query/model/EdgeUserSearchParameters.java @@ -0,0 +1,12 @@ +package nu.marginalia.wmsa.edge.search.query.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import nu.marginalia.wmsa.edge.search.EdgeSearchProfile; + +@AllArgsConstructor @Getter +public class EdgeUserSearchParameters { + public final String humanQuery; + public final EdgeSearchProfile profile; + public final String jsSetting; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/SearchResultDecorator.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/SearchResultDecorator.java new file mode 100644 index 00000000..487e1556 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/SearchResultDecorator.java @@ -0,0 +1,113 @@ +package nu.marginalia.wmsa.edge.search.results; + +import com.google.inject.Inject; +import gnu.trove.list.array.TIntArrayList; +import gnu.trove.map.hash.TIntObjectHashMap; +import io.reactivex.rxjava3.annotations.NonNull; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDao; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.search.EdgeSearchResultItem; +import nu.marginalia.wmsa.edge.model.search.EdgeUrlDetails; +import nu.marginalia.wmsa.edge.search.results.model.TieredSearchResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.TreeMap; +import java.util.stream.Collectors; + +public class SearchResultDecorator { + private final EdgeDataStoreDao edgeDataStoreDao; + private final SearchResultValuator valuator; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Inject + public SearchResultDecorator(EdgeDataStoreDao edgeDataStoreDao, SearchResultValuator valuator) { + this.edgeDataStoreDao = edgeDataStoreDao; + this.valuator = valuator; + } + + @NonNull + public List decorateSearchResults(List items, IndexBlock block, UrlDeduplicator deduplicator) { + List results = new ArrayList<>(); + + int dedups = 0; + for (var details : getAllUrlDetails(items, block)) { + if (deduplicator.filter(details)) { + results.add(new TieredSearchResult(details.queryLength, getEffectiveBlock(details), details)); + } + else { + dedups++; + } + } + if (dedups > 0) { + logger.debug("dedups: {}", dedups); + } + + return results; + } + + + private static final TreeMap blocksByOrder = new TreeMap<>(); + static { + for (var block : IndexBlock.values()) { + blocksByOrder.put((double) block.sortOrder, block); + } + } + + private IndexBlock getEffectiveBlock(EdgeUrlDetails details) { + return blocksByOrder.floorEntry(details.termScore).getValue(); + } + + private List getAllUrlDetails(List resultItems, IndexBlock block) { + TIntObjectHashMap detailsById = new TIntObjectHashMap<>(resultItems.size()); + + var idList = resultItems.stream().map(EdgeSearchResultItem::getUrl).collect(Collectors.toList()); + + List ret = edgeDataStoreDao.getUrlDetailsMulti(idList); + + for (var val : ret) { + detailsById.put(val.id, val); + } + + List retList = new ArrayList<>(resultItems.size()); + + TIntArrayList missedIds = new TIntArrayList(); + for (var resultItem : resultItems) { + + var did = resultItem.getDomain().getId(); + var uid = resultItem.getUrl().getId(); + + var details = detailsById.get(uid); + if (details == null) { + missedIds.add(uid); + continue; + } + + if (details.rankingId == Integer.MAX_VALUE) { + details.rankingId = did; + } + + details.termScore = calculateTermScore(block, resultItem, details); + details.queryLength = resultItem.queryLength; + + logger.debug("{} -> {}", details.url, details.termScore); + + retList.add(details); + } + if (!missedIds.isEmpty()) { + logger.warn("Could not look up documents: {}", missedIds.toArray()); + } + retList.sort(Comparator.comparing(EdgeUrlDetails::getTermScore)); + return retList; + } + + private double calculateTermScore(IndexBlock block, EdgeSearchResultItem resultItem, EdgeUrlDetails details) { + return valuator.evaluateTerms(resultItem.scores, block, details.words) / Math.sqrt(1 + resultItem.queryLength) + + ((details.domainState == EdgeDomainIndexingState.SPECIAL.code) ? 1.25 : 0); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/SearchResultValuator.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/SearchResultValuator.java new file mode 100644 index 00000000..c2cb781e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/SearchResultValuator.java @@ -0,0 +1,92 @@ +package nu.marginalia.wmsa.edge.search.results; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.WordPatterns; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.model.search.EdgeSearchResultKeywordScore; + +import java.util.List; +import java.util.regex.Pattern; + +@Singleton +public class SearchResultValuator { + private final NGramDict dict; + + private static final Pattern separator = Pattern.compile("_"); + + private static final int MIN_LENGTH = 500; + private static final int AVG_LENGTH = 1400; + + @Inject + public SearchResultValuator(NGramDict dict) { + this.dict = dict; + } + + + // This is basically a bargain bin BM25 + public double evaluateTerms(List rawScores, IndexBlock block, int length) { + EdgeSearchResultKeywordScore[] scores = rawScores.stream().filter(w -> !w.keyword.contains(":")).toArray(EdgeSearchResultKeywordScore[]::new); + + if (scores.length == 0) { + return IndexBlock.Words.sortOrder; + } + + final double[] weights = getTermWeights(scores); + final double lengthPenalty = getLengthPenalty(length); + + double termSum = 0.; + double factorSum = 0.; + + for (int i = 0; i < scores.length; i++) { + final double factor = 1.0 / (1.0 + weights[i]); + factorSum += factor; + + double termValue = (scores[i].index.sortOrder + 0.5) * factor; + + if (!scores[i].link && !scores[i].title) { + termValue *= lengthPenalty; + } + + termSum += termValue; + } + + assert factorSum != 0 ; + + if (block == IndexBlock.Title || block == IndexBlock.TitleKeywords) { + return block.sortOrder + (termSum / factorSum) / 5; + } + + return termSum / factorSum; + } + + private double getLengthPenalty(int length) { + if (length < MIN_LENGTH) { + length = MIN_LENGTH; + } + return (0.7 + 0.3 * length / AVG_LENGTH); + } + + private double[] getTermWeights(EdgeSearchResultKeywordScore[] scores) { + double[] weights = new double[scores.length]; + + for (int i = 0; i < scores.length; i++) { + String[] parts = separator.split(scores[i].keyword); + double sumScore = 0.; + + int count = 0; + for (String part : parts) { + if (!WordPatterns.isStopWord(part)) { + sumScore += dict.getTermFreq(part); + count++; + } + } + if (count == 0) count = 1; + + weights[i] = Math.sqrt(sumScore)/count; + } + + return weights; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/UrlDeduplicator.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/UrlDeduplicator.java new file mode 100644 index 00000000..79b2647f --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/UrlDeduplicator.java @@ -0,0 +1,41 @@ +package nu.marginalia.wmsa.edge.search.results; + +import com.google.common.base.Strings; +import gnu.trove.map.hash.TObjectIntHashMap; +import gnu.trove.set.hash.TIntHashSet; +import nu.marginalia.wmsa.edge.model.search.EdgeUrlDetails; + +public class UrlDeduplicator { + private final TIntHashSet seenSuperficialhashes = new TIntHashSet(100); + private final TIntHashSet seenDataHashes = new TIntHashSet(100); + private final TObjectIntHashMap ipCount = new TObjectIntHashMap<>(100, 0.75f, 0); + + private final int resultsPerIp; + public UrlDeduplicator(int resultsPerIp) { + this.resultsPerIp = resultsPerIp; + } + + public synchronized boolean filter(EdgeUrlDetails details) { + if (!seenSuperficialhashes.add(details.getSuperficialHash())) { + return false; + } + if (!seenDataHashes.add(details.getDataHash())) { + return false; + } + if (Strings.isNullOrEmpty(details.getIp())) { + final var domain = details.getUrl().getDomain(); + final String key; + + if (!details.isSpecialDomain()) { + key = domain.getLongDomainKey(); + } + else { + key = domain.getDomainKey(); + } + + return ipCount.adjustOrPutValue(key, 1, 1) <= resultsPerIp; + } + + return ipCount.adjustOrPutValue(details.getIp(), 1, 1) < resultsPerIp; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/model/AccumulatedQueryResults.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/model/AccumulatedQueryResults.java new file mode 100644 index 00000000..ec8d1aa2 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/model/AccumulatedQueryResults.java @@ -0,0 +1,39 @@ +package nu.marginalia.wmsa.edge.search.results.model; + +import nu.marginalia.wmsa.edge.model.search.EdgeUrlDetails; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class AccumulatedQueryResults { + + private static final Logger logger = LoggerFactory.getLogger(AccumulatedQueryResults.class); + + public Set results = new HashSet<>(); + + public void add(EdgeUrlDetails details) { + results.add(details); + } + + public void append(int maxSize, List details) { + for (var result : details) { + + if (size() >= maxSize) { + break; + } + + add(result.details); + } + } + + public int size() { + return results.size(); + } + + public int count() { + return results.size(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/model/TieredSearchResult.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/model/TieredSearchResult.java new file mode 100644 index 00000000..d98ec566 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/search/results/model/TieredSearchResult.java @@ -0,0 +1,12 @@ +package nu.marginalia.wmsa.edge.search.results.model; + +import lombok.AllArgsConstructor; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.model.search.EdgeUrlDetails; + +@AllArgsConstructor +public class TieredSearchResult { + public final int length; + public final IndexBlock block; + public final EdgeUrlDetails details; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/ConverterMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/ConverterMain.java new file mode 100644 index 00000000..e5811859 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/ConverterMain.java @@ -0,0 +1,384 @@ +package nu.marginalia.wmsa.edge.tools; + + +import com.zaxxer.hikari.HikariDataSource; +import gnu.trove.map.hash.TIntObjectHashMap; +import gnu.trove.map.hash.TObjectIntHashMap; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.edge.archive.archiver.ArchiveExtractor; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.LinkParser; +import nu.marginalia.wmsa.edge.crawler.domain.language.LanguageFilter; +import nu.marginalia.wmsa.edge.crawler.domain.language.WordPatterns; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.DocumentKeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlFeature; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlStandardExtractor; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.index.service.dictionary.DictionaryWriter; +import nu.marginalia.wmsa.edge.index.service.index.SearchIndexWriterImpl; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.EdgeHtmlStandard; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageWordSet; +import nu.marginalia.wmsa.edge.model.crawl.EdgeRawPageContents; +import org.apache.commons.lang3.tuple.Pair; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.mariadb.jdbc.Driver; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; + +import static nu.marginalia.wmsa.edge.model.crawl.EdgeHtmlStandard.UNKNOWN; + +public class ConverterMain { + static LinkedBlockingQueue processQueue = new LinkedBlockingQueue<>(20); + static LinkedBlockingQueue uploadQueue = new LinkedBlockingQueue<>(2); + + static TObjectIntHashMap urlToIdMap = new TObjectIntHashMap<>(50_000_000, 0.5f, -1); + static TObjectIntHashMap domainToIdMap = new TObjectIntHashMap<>(5_000_000, 0.5f, -1); + static TIntObjectHashMap idToDomainMap = new TIntObjectHashMap<>(5_000_000, 0.5f, -1); + static HikariDataSource conn; + + private static SearchIndexWriterImpl indexWriter; + private static DictionaryWriter dictionaryWriter; + + @AllArgsConstructor + static class UploadJob { + EdgeId domainId; + EdgeId urlId; + EdgePageWordSet words; + int wordCount; + }; + static volatile boolean running = true; + + public static void main(String... args) throws IOException { + org.mariadb.jdbc.Driver driver = new Driver(); + + dictionaryWriter = new DictionaryWriter(new File(args[0]), 1L << 30, true); + indexWriter = new SearchIndexWriterImpl(dictionaryWriter, new File(args[1])); + + new Thread(ConverterMain::uploadThread, "Uploader").start(); + + for (int i = 0; i < 24; i++) { + new Thread(ConverterMain::processorThread, "Processor-"+i).start(); + } + + conn = new DatabaseModule().provideConnection(); + + System.out.println("Loading URLs and domains"); + try (var c = conn.getConnection(); + var getUrlsStmt = c.prepareStatement("SELECT EC_URL.ID, DOMAIN_ID, PROTO, URL FROM EC_URL WHERE VISITED"); + var getDomainsStmt = c.prepareStatement("SELECT ID, URL_PART FROM EC_DOMAIN WHERE INDEXED>0") + ) { + getUrlsStmt.setFetchSize(10_000); + getDomainsStmt.setFetchSize(10_000); + + System.out.println("Fetch domains"); + var domainRsp = getDomainsStmt.executeQuery(); + while (domainRsp.next()) { + domainToIdMap.put(domainRsp.getString(2), domainRsp.getInt(1)); + idToDomainMap.put(domainRsp.getInt(1), domainRsp.getString(2)); + } + + System.out.println("Fetch URLs"); + var urlRsp = getUrlsStmt.executeQuery(); + while (urlRsp.next()) { + String urlStr = urlRsp.getString(3) + "://" + idToDomainMap.get(urlRsp.getInt(2)) + urlRsp.getString(4); + urlToIdMap.put(urlStr, urlRsp.getInt(1)); + } + } catch (SQLException throwables) { + throwables.printStackTrace(); + } + +// new Thread(ConverterMain::uploadThread, "Uploader").start(); +// +// for (int i = 0; i < 24; i++) { +// new Thread(ConverterMain::processorThread, "Processor-"+i).start(); +// } + + System.out.println("Loaded URLs and domains"); + + new ArchiveExtractor(Path.of(args[2])).forEach( + page -> { + if (page.contentType.contentType.startsWith("application/xhtml") + || page.contentType.contentType.startsWith("text/html")) { + try { + int domainId = domainToIdMap.get(page.url.domain.toString()); + if (domainId >= 0 && page.redirectUrl == null) { + int urlId = urlToIdMap.get(page.url.toString()); + int dataHash = page.data.hashCode(); + try (var c = conn.getConnection(); + var updateHash = c.prepareStatement("UPDATE EC_URL SET DATA_HASH=? WHERE ID=?")) + { + updateHash.setInt(1, dataHash); + updateHash.setInt(2, urlId); + updateHash.executeUpdate(); + } + catch (Exception ex) { + ex.printStackTrace(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + + running = false; + } + + static LanguageModels lm = new LanguageModels( + Path.of("/var/lib/wmsa/model/ngrams-generous-emstr.bin"), + Path.of("/var/lib/wmsa/model/tfreq-new-algo3.bin"), + Path.of("/var/lib/wmsa/model/opennlp-sentence.bin"), + Path.of("/var/lib/wmsa/model/English.RDR"), + Path.of("/var/lib/wmsa/model/English.DICT"), + Path.of("/var/lib/wmsa/model/opennlp-tok.bin") + ); + static NGramDict dict = new NGramDict(lm); + + private static final LanguageFilter languageFilter = new LanguageFilter(); + private static final LinkParser linkParser = new LinkParser(); + public static void processorThread() { + SentenceExtractor newSe = new SentenceExtractor(lm); + DocumentKeywordExtractor documentKeywordExtractor = new DocumentKeywordExtractor(dict); + + try { + while (running || !processQueue.isEmpty()) { + var job = processQueue.take(); + if (job.data.length() > 512*1024) { + System.out.println(job.url + " too big, skipping"); + } + + var parsed = Jsoup.parse(job.data); + var text = parsed.text(); + + if (languageFilter.isBlockedUnicodeRange(text)) { + continue; + } + + var dld = newSe.extractSentences(parsed.clone()); + var keywords = documentKeywordExtractor.extractKeywords(dld); + int wc = dld.totalNumWords(); + + if (wc > 100) { + double languageAgreement = languageFilter.dictionaryAgreement(dld); + if (languageAgreement < 0.05) { + continue; + } + } + + + EdgeHtmlStandard htmlStandard = HtmlStandardExtractor.parseDocType(parsed.documentType()); + if (UNKNOWN.equals(htmlStandard)) { + htmlStandard = HtmlStandardExtractor.sniffHtmlStandard(parsed); + } + + int scriptTags = getScriptPenalty(parsed); + var featureSet = getFeatureSet(parsed, scriptTags, job.hasCookies); + addTags(keywords, htmlStandard, job.url, featureSet); + + extractLinkWords(keywords, job.getUrl(), parsed); + + uploadQueue.put(new UploadJob( + new EdgeId<>(domainToIdMap.get(job.url.domain.toString())), + new EdgeId<>(urlToIdMap.get(job.url.toString())), + keywords, + 0 + )); + + } + } + catch (InterruptedException ex) { + ex.printStackTrace(); + } + } + + + private static Map> extractLinkWords(EdgePageWordSet keywords, EdgeUrl pageUrl, Document parsed) { + + List> urls = new ArrayList<>(); + Set linkKeywords = new HashSet<>(); + Map> linkTextWords = new ConcurrentHashMap<>(); + + for (var tag : parsed.getElementsByTag("a")) { + if (!tag.hasAttr("href")) { + continue; + } + if (urls.size() > 100) { + break; + } + + var linkOpt = linkParser.parseLink(pageUrl, tag); + if (linkOpt.isEmpty()) + continue; + + var link = linkOpt.get(); + + urls.add(Pair.of(link, tag.text())); + + if (!Objects.equals(link.domain.domain, pageUrl.domain.domain) + && linkKeywords.size() <= 25) + { + linkKeywords.add("links:" + link.domain.domain); + } +// +// Set words = new HashSet<>(); +// +// for (var sent : sentenceExtractor.extractSentencesFromString(tag.text())) { +// for (var keyword : keywordExtractor.getWordsFromSentence(sent)) { +// words.add(sent.constructWordFromSpan(keyword)); +// } +// } +// +// linkTextWords.compute(link, (k, set) -> { +// if (set == null) return words; +// else { set.addAll(words); return set; } +// }); + + } + + keywords.get(IndexBlock.Meta).addAll(linkKeywords); + + if (WordPatterns.wordQualitiesPredicate.test(pageUrl.domain.domain.toLowerCase())) { + keywords.get(IndexBlock.Link).addJust(pageUrl.domain.domain.toLowerCase()); + } + + return linkTextWords; + } + + private static int getScriptPenalty(Document parsed) { + var scriptTags = parsed.getElementsByTag("script"); + String scriptText = scriptTags.html(); + int badScript = 0; + if (scriptText.contains(".createElement(")) { + badScript = 1; + } + return scriptTags.size() + badScript + (scriptText.length())/1000; + } + + static List trackers = List.of("adform.net", + "connect.facebook", + "googletagmanager.com", + "googlesyndication.com", + "google.com", + "twitter.com", + "smartadserver.com", + "doubleclick.com", + "2mdn.com", + "dmtry.com", + "bing.com", + "msn.com", + "amazon-adsystem.com", + "alexametrics.com", + "rubiconproject.com", + "chango.com", + "d5nxst8fruw4z.cloudfront.net", + "d31qbv1cthcecs.cloudfront.net", + "linkedin.com"); + + private static Set getFeatureSet(Document parsed, int scriptTags, boolean cookies) { + Set features = new HashSet<>(); + + if (scriptTags > 0) { + features.add(HtmlFeature.JS); + } + if (!parsed.getElementsByTag("object").isEmpty() + || !parsed.getElementsByTag("audio").isEmpty() + || !parsed.getElementsByTag("video").isEmpty()) { + features.add(HtmlFeature.MEDIA); + } + if (parsed.getElementsByTag("script").stream() + .filter(tag -> tag.attr("src") != null) + .anyMatch(tag -> trackers.stream().anyMatch(tracker -> tag.attr("src").contains(tracker)))) { + features.add(HtmlFeature.TRACKING); + } + if (parsed.getElementsByTag("script").html().contains("google-analytics.com")) { + features.add(HtmlFeature.TRACKING); + } + if (parsed.getElementsByTag("a").stream().map(e -> e.attr("href")) + .filter(Objects::nonNull) + .map(String::toLowerCase) + .anyMatch(href -> + href.contains("amzn.to/") || href.contains("amazon.com/"))) { + features.add(HtmlFeature.AFFILIATE_LINK); + } + if (cookies) { + features.add(HtmlFeature.COOKIES); + } + + return features; + } + + private static void addTags(EdgePageWordSet wordSet, EdgeHtmlStandard htmlStandard, EdgeUrl url, Set features) { + List tagWords = new ArrayList<>(); + tagWords.add("format:"+htmlStandard.toString().toLowerCase()); + tagWords.add("site:"+url.domain.toString().toLowerCase()); + tagWords.add("proto:"+url.proto.toLowerCase()); + tagWords.add("js:" + Boolean.toString(features.contains(HtmlFeature.JS)).toLowerCase()); + if (features.contains(HtmlFeature.MEDIA)) { + tagWords.add("special:media"); + } + if (features.contains(HtmlFeature.TRACKING)) { + tagWords.add("special:tracking"); + } + if (features.contains(HtmlFeature.AFFILIATE_LINK)) { + tagWords.add("special:affiliate"); + } + if (features.contains(HtmlFeature.COOKIES)) { + tagWords.add("special:cookies"); + } + wordSet.append(IndexBlock.Meta, tagWords); + wordSet.append(IndexBlock.Words, tagWords); + } + + @SneakyThrows + public static void uploadThread() { + + while (running || !processQueue.isEmpty() || !uploadQueue.isEmpty()) { + var data = uploadQueue.take(); + + if (!data.words.isEmpty()) { + for (var words : data.words.values()) { + if (!words.getWords().isEmpty()) { + if (words.size() < 1000) { + indexWriter.put(data.domainId, data.urlId, words.block, words.words); + } else { + chunks(words.words, 1000).forEach(chunk -> { + indexWriter.put(data.domainId, data.urlId, words.block, chunk); + }); + } + } + } + } + } + + System.out.println("Closing"); + dictionaryWriter.commitToDisk(); + indexWriter.forceWrite(); + dictionaryWriter.close(); + indexWriter.close(); + System.out.println("Done"); + } + + private static List> chunks(Collection coll, int size) { + List> ret = new ArrayList<>(); + List data = List.copyOf(coll); + + for (int i = 0; i < data.size(); i+=size) { + ret.add(data.subList(i, Math.min(data.size(), i+size))); + } + + return ret; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/CrawlDomainMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/CrawlDomainMain.java new file mode 100644 index 00000000..199dcdee --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/CrawlDomainMain.java @@ -0,0 +1,390 @@ +package nu.marginalia.wmsa.edge.tools; + + +import com.opencsv.exceptions.CsvValidationException; +import com.zaxxer.hikari.HikariDataSource; +import gnu.trove.list.array.TIntArrayList; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.edge.archive.client.ArchiveClient; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.DomainCrawlResults; +import nu.marginalia.wmsa.edge.crawler.domain.DomainCrawlerFactory; +import nu.marginalia.wmsa.edge.crawler.domain.DomainCrawlerRobotsTxt; +import nu.marginalia.wmsa.edge.crawler.domain.language.LanguageFilter; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.DocumentKeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlProcessor; +import nu.marginalia.wmsa.edge.crawler.domain.processor.PlainTextProcessor; +import nu.marginalia.wmsa.edge.crawler.fetcher.HttpFetcher; +import nu.marginalia.wmsa.edge.crawler.worker.GeoIpBlocklist; +import nu.marginalia.wmsa.edge.crawler.worker.IpBlockList; +import nu.marginalia.wmsa.edge.crawler.worker.UploaderWorker; +import nu.marginalia.wmsa.edge.crawler.worker.facade.UploadFacadeDirectImpl; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDaoImpl; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDomainBlacklistImpl; +import nu.marginalia.wmsa.edge.director.client.EdgeDirectorClient; +import nu.marginalia.wmsa.edge.index.client.EdgeIndexClient; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainLink; +import nu.marginalia.wmsa.edge.model.crawl.EdgeIndexTask; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlVisit; +import org.mariadb.jdbc.Driver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class CrawlDomainMain { + static LinkedBlockingQueue processQueue = new LinkedBlockingQueue<>(5); + static LinkedBlockingQueue uploadQueue = new LinkedBlockingQueue<>(2); + + static Logger logger = LoggerFactory.getLogger(CrawlDomainMain.class); + + static HikariDataSource conn; + + private static EdgeIndexClient indexClient; + private static IpBlockList blocklist; + private static HttpFetcher fetcher; + private static UploadFacadeDirectImpl uploadFacade; + + static { + try { + blocklist = new IpBlockList(new GeoIpBlocklist()); + } catch (IOException e) { + e.printStackTrace(); + } catch (CsvValidationException e) { + e.printStackTrace(); + } + } + + @AllArgsConstructor + static class ReindexJob { + EdgeIndexTask task; + Map hashes; + int visitedCount; + }; + + @AllArgsConstructor + static class UploadJob { + DomainCrawlResults results; + ReindexJob job; + }; + + + static volatile boolean running = true; + + public static class AbortMonitor { + private volatile boolean abort = false; + private static volatile AbortMonitor instance = null; + + public static AbortMonitor getInstance() { + if (instance == null) { + synchronized (AbortMonitor.class) { + if (instance == null) { + instance = new AbortMonitor(); + new Thread(instance::run, "AbortMon").start(); + } + } + } + return instance; + } + + private AbortMonitor() { + } + + @SneakyThrows + public void run() { + for (;;) { + Thread.sleep(1000); + if (Files.exists(Path.of("/tmp/stop"))) { + logger.warn("Abort file found"); + abort = true; + Files.delete(Path.of("/tmp/stop")); + } + } + } + public boolean isAlive() { + return !abort; + } + } + + @SneakyThrows + public static void main(String... args) throws IOException { + Driver driver = new Driver(); + + indexClient = new EdgeIndexClient(); + + + conn = new DatabaseModule().provideConnection(); + var blacklist = new EdgeDomainBlacklistImpl(conn); + + EdgeDataStoreDaoImpl dataStoreDao = new EdgeDataStoreDaoImpl(conn); + + TIntArrayList domainIndexOrder = new TIntArrayList(); + + final Thread uploadThread = new Thread(CrawlDomainMain::uploadThread, "Uploader"); + uploadThread.start(); + + SentenceExtractor newSe = new SentenceExtractor(lm); + DocumentKeywordExtractor documentKeywordExtractor = new DocumentKeywordExtractor(dict); + + uploadFacade = new UploadFacadeDirectImpl(new EdgeDataStoreDaoImpl(conn), new EdgeIndexClient(), new EdgeDirectorClient()); + + fetcher = new HttpFetcher("search.marginalia.nu"); + var dcf = new DomainCrawlerFactory(fetcher, + new HtmlProcessor(documentKeywordExtractor, newSe), + new PlainTextProcessor(documentKeywordExtractor, newSe), + new ArchiveClient(), + new DomainCrawlerRobotsTxt(fetcher, "search.marginalia.nu"), + new LanguageFilter(), + blocklist); + + for (int i = 0; i < 64; i++) { + new Thread(() -> processorThread(dcf), "Processor-"+i).start(); + } + + List urls = new ArrayList<>(); + + try (var br = new BufferedReader(new InputStreamReader(System.in))) { + for (;;) { + var urlStr = br.readLine(); + if (urlStr == null) { + break; + } else if (!urlStr.isBlank()) { + urls.add(new EdgeUrl(urlStr)); + } + } + } + uploadFacade.putUrls(urls, -5); + + try (var c = conn.getConnection(); + var fetchDomains = c.prepareStatement("select ID FROM EC_DOMAIN WHERE URL_PART=?"); + var fetchUrlsForDomain = c.prepareStatement("select ID,DATA_HASH,VISITED FROM EC_URL WHERE DOMAIN_ID=? ORDER BY VISITED DESC, DATA_HASH IS NOT NULL DESC, ID") + ) { + + fetchDomains.setFetchSize(1000); + + for (var url : urls) { + fetchDomains.setString(1, url.getDomain().toString()); + fetchDomains.executeQuery(); + + var domainRsp = fetchDomains.executeQuery(); + + logger.info("Fetched {}", url); + + while (domainRsp.next()) { + if (!blacklist.isBlacklisted(domainRsp.getInt(1))) { + domainIndexOrder.add(domainRsp.getInt(1)); + } + } + } + + fetchUrlsForDomain.setFetchSize(10_000); + + for (int i = 0; i < domainIndexOrder.size(); i++) { + if (!AbortMonitor.getInstance().isAlive()) { + break; + } + + int domainId = domainIndexOrder.getQuick(i); + var domain = dataStoreDao.getDomain(new EdgeId<>(domainId)); + + fetchUrlsForDomain.setInt(1, domainId); + var urlRsp = fetchUrlsForDomain.executeQuery(); + + EdgeIndexTask task = new EdgeIndexTask(domain, 1000, 1000, 0); + Map hashes = new HashMap<>(); + + int visitedCount = 0; + while (urlRsp.next()) { + var url = dataStoreDao.getUrl(new EdgeId<>(urlRsp.getInt(1))); + task.urls.add(url); + + if (urlRsp.getBoolean(3)) { + visitedCount++; + } + + int hash = urlRsp.getInt(2); + if (hash != 0) + hashes.put(url, hash); + } + + processQueue.put(new ReindexJob(task, hashes, visitedCount)); + } + } + catch (Exception ex) { + ex.printStackTrace(); + } + + uploadThread.join(); + System.exit(0); + } + + static LanguageModels lm = new LanguageModels( + Path.of("/var/lib/wmsa/model/ngrams-generous-emstr.bin"), + Path.of("/var/lib/wmsa/model/tfreq-new-algo3.bin"), + Path.of("/var/lib/wmsa/model/opennlp-sentence.bin"), + Path.of("/var/lib/wmsa/model/English.RDR"), + Path.of("/var/lib/wmsa/model/English.DICT"), + Path.of("/var/lib/wmsa/model/opennlp-tok.bin") + ); + static NGramDict dict = new NGramDict(lm); + + private static final Semaphore processSem = new Semaphore(500, true); + @SneakyThrows + public static void processorThread(DomainCrawlerFactory dcf) { + String name = Thread.currentThread().getName(); + try { +outer: + while (AbortMonitor.getInstance().isAlive() && (running || !processQueue.isEmpty())) { + + Thread.currentThread().setName(name); + ReindexJob job = null; + + while (job == null) { + job = processQueue.poll(30, TimeUnit.SECONDS); + if (!AbortMonitor.getInstance().isAlive()) { + break outer; + } + } + + Thread.currentThread().setName(name + ":" + job.task.domain); + if (!blocklist.isAllowed(job.task.domain)) { + setDomainError(job.task.domain, new HttpFetcher.FetchResult(HttpFetcher.FetchResultState.ERROR, job.task.domain)); + } + + var probe = fetcher.probeDomain(job.task.urls.get(0)); + + if (!AbortMonitor.getInstance().isAlive()) { + break; + } + + if (!probe.ok()) { + setDomainError(job.task.domain, probe); + } else { + var dc = dcf.domainCrawler(job.task); + + int countProp = Objects.requireNonNullElse(Integer.getInteger("wmsa.edge.crawl.maxCeiling"), 0); + + int countMaxAll = 4000; + int countVisited = (int) (job.visitedCount * 1.1); + int countHash = 100 + job.hashes.size() * 2; + + int maxCount = Math.max(countProp, Math.min(countMaxAll, Math.max(countVisited, countHash))); + + DomainCrawlResults result; + int tokens = Math.max(1,maxCount/1000); + try { + while (!processSem.tryAcquire(tokens)) + Thread.sleep((int)(100 + Math.random() * 100)); + result = dc.crawlToExhaustion(maxCount, AbortMonitor.getInstance()::isAlive); + + if (!AbortMonitor.getInstance().isAlive()) { + break; + } + } + finally { + processSem.release(tokens); + } + + uploadQueue.put(new UploadJob(result, job)); + } + } + } + catch (InterruptedException ex) { + ex.printStackTrace(); + } + + logger.warn("Terminating {}", Thread.currentThread().getName()); + } + + private static void setDomainError(EdgeDomain domain, HttpFetcher.FetchResult probe) { + List links = new ArrayList<>(1); + EdgeDomain alias = null; + if (probe.state == HttpFetcher.FetchResultState.REDIRECT) { + links.add(new EdgeDomainLink(domain, probe.domain)); + alias = probe.domain; + } + uploadFacade.putLinks(links, true); + uploadFacade.updateDomainIndexTimestamp(domain, EdgeDomainIndexingState.ERROR, alias, 1); + } + + + @SneakyThrows + public static void uploadThread() { + + + int count = 0; + long allUrls = 0; + long newUrls = 0; +outer: + while (AbortMonitor.getInstance().isAlive() && (running || !processQueue.isEmpty() || !uploadQueue.isEmpty())) { + + UploadJob job = null; + + while (job == null) { + job = uploadQueue.poll(30, TimeUnit.SECONDS); + if (!AbortMonitor.getInstance().isAlive()) { + break outer; + } + } + + UploadJob data = job; + + logger.info("{} Done - {} : {} : {}", ++count, data.results.domain, allUrls, newUrls); + + var dc = data.results; + + double avgQuality = UploaderWorker.calculateMedianQuality(dc).orElse(-5.); + + if (uploadFacade.isBlacklisted(dc.domain)) { + continue; + } + + final double linkQualityRating = -5; //(avgQuality + UNKNOWN_SITE_ATTRACTOR)/2 - extLinkPenalty; + + var visits = dc.visits(); + allUrls += visits.size(); + + var newContents = visits.stream().filter(visit -> { + var hash = data.job.hashes.get(visit.url); + return (hash == null || !Objects.equals(visit.data_hash_code, hash)); + }).collect(Collectors.toList()); + + var goodUrls = newContents.stream().map(EdgeUrlVisit::getUrl).collect(Collectors.toSet()); + + newUrls += newContents.size(); + + uploadFacade.putUrls(dc.extUrl, linkQualityRating); + uploadFacade.putUrls(dc.intUrl, linkQualityRating); + uploadFacade.putUrlVisits(newContents); + uploadFacade.putFeeds(dc.feeds); + + if (avgQuality < UploaderWorker.QUALITY_LOWER_BOUND_CUTOFF) { + uploadFacade.updateDomainIndexTimestamp(dc.domain, EdgeDomainIndexingState.ACTIVE, null, 1); + continue; + } + + uploadFacade.putLinks(dc.links, true); + uploadFacade.putWords(dc.pageContents.values().stream().filter(pc -> goodUrls.contains(pc.url)).collect(Collectors.toList()), 1); + + uploadFacade.updateDomainIndexTimestamp(dc.domain, EdgeDomainIndexingState.ACTIVE, null, 1); + } + + logger.warn("Terminating {}", Thread.currentThread().getName()); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/DomainInserterMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/DomainInserterMain.java new file mode 100644 index 00000000..28af0926 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/DomainInserterMain.java @@ -0,0 +1,83 @@ +package nu.marginalia.wmsa.edge.tools; + +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDaoImpl; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.mariadb.jdbc.Driver; + +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class DomainInserterMain { + public static void main(String... args) throws Exception { + org.mariadb.jdbc.Driver driver = new Driver(); + + var conn = new DatabaseModule().provideConnection(); + + var dao = new EdgeDataStoreDaoImpl(conn); + Set domains = new HashSet<>(); + + var connection = conn.getConnection(); + + try (var br = Files.newBufferedReader(Path.of(args[0])); + var stmt = connection.prepareStatement("SELECT ID FROM EC_DOMAIN WHERE URL_PART=?"); + var setRankStmt = connection.prepareStatement("UPDATE EC_DOMAIN SET RANK=? WHERE URL_PART=?") + ) { + String line; + + List loadUrls = new ArrayList<>(100); + + for (;;) { + loadUrls.clear(); + + double quality; + + line = br.readLine(); + if (null == line) break; + quality = Double.parseDouble(line); + + line = br.readLine(); + if (null == line) break; + var url = getUrl(line); + stmt.setString(1, url.domain.toString()); + var rsp = stmt.executeQuery(); + if (rsp.next()) { + System.out.println("Known: " + line); + while (null != (line = br.readLine()) && !line.isBlank()) { + if (".".equals(line)) break; + } + setRankStmt.setString(2, url.getDomain().toString()); + setRankStmt.setDouble(1, quality); + setRankStmt.executeUpdate(); + } + else { + loadUrls.add(url); + while (null != (line = br.readLine()) && !line.isBlank()) { + if (".".equals(line)) break; + loadUrls.add(getUrl(line)); + } + + dao.putUrl(-2*quality, loadUrls.toArray(EdgeUrl[]::new)); + + System.out.println(loadUrls); + } + } + } + } + + @SneakyThrows + static EdgeUrl getUrl(String line) { + String[] parts = line.split("/", 4); + return new EdgeUrl(parts[0]+"//"+parts[2]+"/" + URLEncoder.encode(parts[3])); + } + + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/IndexMergerMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/IndexMergerMain.java new file mode 100644 index 00000000..fafa68f1 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/IndexMergerMain.java @@ -0,0 +1,198 @@ +package nu.marginalia.wmsa.edge.tools; + +import com.google.inject.Inject; +import gnu.trove.set.hash.TIntHashSet; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDomainBlacklist; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDomainBlacklistImpl; +import nu.marginalia.wmsa.edge.index.service.SearchIndexDao; +import nu.marginalia.wmsa.edge.index.service.query.SearchIndexPartitioner; +import org.mariadb.jdbc.Driver; +import org.roaringbitmap.longlong.Roaring64Bitmap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.util.Objects; + +public class IndexMergerMain { + private static final int CHUNK_HEADER_SIZE = 16; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final SearchIndexPartitioner partitioner; + private final TIntHashSet spamDomains; + + @SneakyThrows + public static long wordCount(File inputFile) { + try (RandomAccessFile raf = new RandomAccessFile(inputFile, "r")) { + raf.readLong(); + return raf.readInt(); + } + } + + public static void main(String... args) { + Driver driver = new Driver(); + + File file1 = new File(args[0]); + File file2 = new File(args[1]); + File outputFile = new File(args[2]); + + if (!file1.exists()) { + System.err.println("File " + file1 + " does not exist"); + return; + } + if (!file2.exists()) { + System.err.println("File " + file2 + " does not exist"); + return; + } + + if (outputFile.exists()) { // Footgun prevention + System.err.println("File " + outputFile + " already exists"); + return; + } + + var hikari = new DatabaseModule().provideConnection(); + var partitioner = new SearchIndexPartitioner(new SearchIndexDao(hikari)); + var blacklist = new EdgeDomainBlacklistImpl(hikari); + + new IndexMergerMain(file1, file2, outputFile, partitioner, blacklist); + } + + + @SneakyThrows + @Inject + public IndexMergerMain(File inputFile1, File inputFile2, + File outputFile, + SearchIndexPartitioner partitioner, + EdgeDomainBlacklist blacklist) + { + this.partitioner = partitioner; + this.spamDomains = blacklist.getSpamDomains(); + + if (outputFile.exists()) { + Files.deleteIfExists(Objects.requireNonNull(outputFile).toPath()); + } + + Roaring64Bitmap secondFileIndices = findIndices(inputFile2); + + RandomAccessFile randomAccessFile = new RandomAccessFile(outputFile, "rw"); + randomAccessFile.seek(12); + + FileChannel outputFileChannel = randomAccessFile.getChannel(); + + int wc1 = copyToOutputFile(inputFile2, outputFileChannel, secondFileIndices, true); + int wc2 = copyToOutputFile(inputFile1, outputFileChannel, secondFileIndices, false); + + long pos = randomAccessFile.getFilePointer(); + + randomAccessFile.seek(0); + randomAccessFile.writeLong(pos); + randomAccessFile.writeInt(Math.max(wc1, wc2)); + outputFileChannel.force(true); + outputFileChannel.close(); + randomAccessFile.close(); + } + + private Roaring64Bitmap findIndices(File file) throws IOException { + Roaring64Bitmap ret = new Roaring64Bitmap(); + + logger.info("Mapping indices in {}", file); + + try (final RandomAccessFile raf = new RandomAccessFile(file, "r"); var channel = raf.getChannel()) { + + var fileLength = raf.readLong(); + raf.readInt(); + + ByteBuffer inByteBuffer = ByteBuffer.allocateDirect(10_000); + + while (channel.position() < fileLength) { + inByteBuffer.clear(); + inByteBuffer.limit(CHUNK_HEADER_SIZE); + channel.read(inByteBuffer); + inByteBuffer.flip(); + long urlId = inByteBuffer.getLong(); + int chunkBlock = inByteBuffer.getInt(); + int count = inByteBuffer.getInt(); + inByteBuffer.limit(count * 4 + CHUNK_HEADER_SIZE); + channel.read(inByteBuffer); + + ret.add(encodeId(urlId, chunkBlock)); + } + } + + logger.info("Cardinality = {}", ret.getLongCardinality()); + + return ret; + } + + private int copyToOutputFile(File inFile, FileChannel outFile, Roaring64Bitmap urlIdAndBlock, boolean ifInSet) throws IOException { + int wordCount = 0; + + logger.info("Copying from {}", inFile); + long skippedWrongFile = 0; + long skippedBadUrl = 0; + try (final RandomAccessFile raf = new RandomAccessFile(inFile, "r"); var channel = raf.getChannel()) { + + var fileLength = raf.readLong(); + raf.readInt(); + + ByteBuffer inByteBuffer = ByteBuffer.allocateDirect(10_000); + + while (channel.position() < fileLength) { + inByteBuffer.clear(); + inByteBuffer.limit(CHUNK_HEADER_SIZE); + channel.read(inByteBuffer); + inByteBuffer.flip(); + long urlId = inByteBuffer.getLong(); + int chunkBlock = inByteBuffer.getInt(); + int count = inByteBuffer.getInt(); + inByteBuffer.limit(count*4+CHUNK_HEADER_SIZE); + channel.read(inByteBuffer); + inByteBuffer.position(CHUNK_HEADER_SIZE); + + for (int i = 0; i < count; i++) { + wordCount = Math.max(wordCount, 1+inByteBuffer.getInt()); + } + + inByteBuffer.position(count*4+CHUNK_HEADER_SIZE); + + if (urlIdAndBlock.contains(encodeId(urlId, chunkBlock)) == ifInSet) { + if (isUrlAllowed(urlId)) { + inByteBuffer.flip(); + + while (inByteBuffer.position() < inByteBuffer.limit()) + outFile.write(inByteBuffer); + } + else { + skippedBadUrl++; + } + } + else { + skippedWrongFile++; + } + } + + } + + logger.info("Skipped {}, {}", skippedBadUrl, skippedWrongFile); + return wordCount; + } + + private long encodeId(long urlId, int chunkBlock) { + return ((urlId & 0xFFFF_FFFFL) << 4L) | chunkBlock; + } + + private boolean isUrlAllowed(long url) { + int urlId = (int)(url & 0xFFFF_FFFFL); + int domainId = (int)(url >>> 32); + + return partitioner.isGoodUrl(urlId) && !spamDomains.contains(domainId); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/ReindexMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/ReindexMain.java new file mode 100644 index 00000000..04b6056c --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/ReindexMain.java @@ -0,0 +1,354 @@ +package nu.marginalia.wmsa.edge.tools; + +import com.opencsv.exceptions.CsvValidationException; +import com.zaxxer.hikari.HikariDataSource; +import gnu.trove.list.array.TIntArrayList; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.edge.archive.client.ArchiveClient; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.*; +import nu.marginalia.wmsa.edge.crawler.domain.language.LanguageFilter; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.DocumentKeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlProcessor; +import nu.marginalia.wmsa.edge.crawler.domain.processor.PlainTextProcessor; +import nu.marginalia.wmsa.edge.crawler.fetcher.HttpFetcher; +import nu.marginalia.wmsa.edge.crawler.worker.GeoIpBlocklist; +import nu.marginalia.wmsa.edge.crawler.worker.IpBlockList; +import nu.marginalia.wmsa.edge.crawler.worker.UploaderWorker; +import nu.marginalia.wmsa.edge.crawler.worker.facade.UploadFacadeDirectImpl; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDaoImpl; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDomainBlacklistImpl; +import nu.marginalia.wmsa.edge.director.client.EdgeDirectorClient; +import nu.marginalia.wmsa.edge.index.client.EdgeIndexClient; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainLink; +import nu.marginalia.wmsa.edge.model.crawl.EdgeIndexTask; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlVisit; +import org.mariadb.jdbc.Driver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class ReindexMain { + static LinkedBlockingQueue processQueue = new LinkedBlockingQueue<>(5); + static LinkedBlockingQueue uploadQueue = new LinkedBlockingQueue<>(2); + + static Logger logger = LoggerFactory.getLogger(ReindexMain.class); + + static HikariDataSource conn; + + private static IpBlockList blocklist; + private static HttpFetcher fetcher; + private static UploadFacadeDirectImpl uploadFacade; + + static { + try { + blocklist = new IpBlockList(new GeoIpBlocklist()); + } catch (IOException | CsvValidationException e) { + e.printStackTrace(); + } + } + + @AllArgsConstructor + static class ReindexJob { + EdgeIndexTask task; + Map hashes; + int visitedCount; + }; + + @AllArgsConstructor + static class UploadJob { + DomainCrawlResults results; + ReindexJob job; + }; + + + static volatile boolean running = true; + + public static class AbortMonitor { + private volatile boolean abort = false; + private static volatile AbortMonitor instance = null; + + public static AbortMonitor getInstance() { + if (instance == null) { + synchronized (AbortMonitor.class) { + if (instance == null) { + instance = new AbortMonitor(); + new Thread(instance::run, "AbortMon").start(); + } + } + } + return instance; + } + + private AbortMonitor() { + } + + @SneakyThrows + public void run() { + for (;;) { + Thread.sleep(1000); + if (Files.exists(Path.of("/tmp/stop"))) { + logger.warn("Abort file found"); + abort = true; + Files.delete(Path.of("/tmp/stop")); + } + } + } + public boolean isAlive() { + return !abort; + } + } + + @SneakyThrows + public static void main(String... args) throws IOException { + Driver driver = new Driver(); + + conn = new DatabaseModule().provideConnection(); + var blacklist = new EdgeDomainBlacklistImpl(conn); + + EdgeDataStoreDaoImpl dataStoreDao = new EdgeDataStoreDaoImpl(conn); + + TIntArrayList domainIndexOrder = new TIntArrayList(); + + new Thread(ReindexMain::uploadThread, "Uploader").start(); + + SentenceExtractor newSe = new SentenceExtractor(lm); + DocumentKeywordExtractor documentKeywordExtractor = new DocumentKeywordExtractor(dict); + + uploadFacade = new UploadFacadeDirectImpl(new EdgeDataStoreDaoImpl(conn), new EdgeIndexClient(), new EdgeDirectorClient()); + + fetcher = new HttpFetcher("search.marginalia.nu"); + var dcf = new DomainCrawlerFactory(fetcher, + new HtmlProcessor(documentKeywordExtractor, newSe), + new PlainTextProcessor(documentKeywordExtractor, newSe), + new ArchiveClient(), + new DomainCrawlerRobotsTxt(fetcher, "search.marginalia.nu"), + new LanguageFilter(), + blocklist); + + for (int i = 0; i < 512; i++) { + new Thread(() -> processorThread(dcf), "Processor-"+i).start(); + } + + try (var c = conn.getConnection(); + var fetchDomains = c.prepareStatement("select ID, EC_DOMAIN.URL_PART from EC_DOMAIN WHERE QUALITY_RAW>-100 AND INDEXED>0 AND INDEX_DATE<'2022-03-17' AND STATE<2 ORDER BY INDEX_DATE ASC,DISCOVER_DATE ASC,STATE DESC,INDEXED DESC,EC_DOMAIN.ID"); + var fetchUrlsForDomain = c.prepareStatement("select ID,DATA_HASH,VISITED FROM EC_URL WHERE DOMAIN_ID=? ORDER BY VISITED DESC, DATA_HASH IS NOT NULL DESC, ID") + ) { + fetchDomains.setFetchSize(10_000); + fetchDomains.executeQuery(); + var domainRsp = fetchDomains.executeQuery(); + + logger.info("Fetched domains"); + while (domainRsp.next()) { + if (!blacklist.isBlacklisted(domainRsp.getInt(1))) { + domainIndexOrder.add(domainRsp.getInt(1)); + } + } + + fetchUrlsForDomain.setFetchSize(10_000); + + for (int i = 0; i < domainIndexOrder.size(); i++) { + if (!AbortMonitor.getInstance().isAlive()) { + break; + } + + int domainId = domainIndexOrder.getQuick(i); + var domain = dataStoreDao.getDomain(new EdgeId<>(domainId)); + + fetchUrlsForDomain.setInt(1, domainId); + var urlRsp = fetchUrlsForDomain.executeQuery(); + + EdgeIndexTask task = new EdgeIndexTask(domain, 1000, 1000, 0); + Map hashes = new HashMap<>(); + + int visitedCount = 0; + while (urlRsp.next()) { + var url = dataStoreDao.getUrl(new EdgeId<>(urlRsp.getInt(1))); + task.urls.add(url); + + if (urlRsp.getBoolean(3)) { + visitedCount++; + } + + int hash = urlRsp.getInt(2); + if (hash != 0) + hashes.put(url, hash); + } + + processQueue.put(new ReindexJob(task, hashes, visitedCount)); + } + } + catch (Exception ex) { + ex.printStackTrace(); + } + + + logger.warn("Terminating Main"); + } + + static LanguageModels lm = new LanguageModels( + Path.of("/var/lib/wmsa/model/ngrams-generous-emstr.bin"), + Path.of("/var/lib/wmsa/model/tfreq-new-algo3.bin"), + Path.of("/var/lib/wmsa/model/opennlp-sentence.bin"), + Path.of("/var/lib/wmsa/model/English.RDR"), + Path.of("/var/lib/wmsa/model/English.DICT"), + Path.of("/var/lib/wmsa/model/opennlp-tok.bin") + ); + static NGramDict dict = new NGramDict(lm); + + private static final Semaphore processSem = new Semaphore(500, true); + @SneakyThrows + public static void processorThread(DomainCrawlerFactory dcf) { + String name = Thread.currentThread().getName(); + try { +outer: + while (AbortMonitor.getInstance().isAlive() && (running || !processQueue.isEmpty())) { + Thread.currentThread().setName(name); + ReindexJob job = null; + + while (job == null) { + job = processQueue.poll(30, TimeUnit.SECONDS); + if (!AbortMonitor.getInstance().isAlive()) { + break outer; + } + } + + Thread.currentThread().setName(name + ":" + job.task.domain); + if (!blocklist.isAllowed(job.task.domain)) { + setDomainError(job.task.domain, new HttpFetcher.FetchResult(HttpFetcher.FetchResultState.ERROR, job.task.domain)); + } + + var probe = fetcher.probeDomain(job.task.urls.get(0)); + + if (!AbortMonitor.getInstance().isAlive()) { + break; + } + + if (!probe.ok()) { + setDomainError(job.task.domain, probe); + } else { + var dc = dcf.domainCrawler(job.task); + + int countMaxAll = 4000; + int countVisited = (int) (job.visitedCount * 1.1); + int countHash = 100 + job.hashes.size() * 2; + + int maxCount = Math.min(countMaxAll, Math.max(countVisited, countHash)); + + DomainCrawlResults result; + int tokens = Math.max(1,maxCount/1000); + try { + while (!processSem.tryAcquire(tokens)) + Thread.sleep((int)(100 + Math.random() * 100)); + result = dc.crawlToExhaustion(maxCount, AbortMonitor.getInstance()::isAlive); + + if (!AbortMonitor.getInstance().isAlive()) { + break; + } + } + finally { + processSem.release(tokens); + } + + uploadQueue.put(new UploadJob(result, job)); + } + } + } + catch (InterruptedException ex) { + ex.printStackTrace(); + } + + logger.warn("Terminating {}", Thread.currentThread().getName()); + } + + private static void setDomainError(EdgeDomain domain, HttpFetcher.FetchResult probe) { + List links = new ArrayList<>(1); + EdgeDomain alias = null; + if (probe.state == HttpFetcher.FetchResultState.REDIRECT) { + links.add(new EdgeDomainLink(domain, probe.domain)); + alias = probe.domain; + } + uploadFacade.putLinks(links, true); + uploadFacade.updateDomainIndexTimestamp(domain, EdgeDomainIndexingState.ERROR, alias, 1); + } + + @SneakyThrows + public static void uploadThread() { + int count = 0; + long allUrls = 0; + long newUrls = 0; +outer: + while (AbortMonitor.getInstance().isAlive() && (running || !processQueue.isEmpty() || !uploadQueue.isEmpty())) { + + UploadJob job = null; + + while (job == null) { + job = uploadQueue.poll(30, TimeUnit.SECONDS); + if (!AbortMonitor.getInstance().isAlive()) { + break outer; + } + } + + UploadJob data = job; + + + var dc = data.results; + + double avgQuality = UploaderWorker.calculateMedianQuality(dc).orElse(-5.); + + if (uploadFacade.isBlacklisted(dc.domain)) { + continue; + } + + final double linkQualityRating = -5; //(avgQuality + UNKNOWN_SITE_ATTRACTOR)/2 - extLinkPenalty; + + var visits = dc.visits(); + allUrls += visits.size(); + + var newContents = visits; + + /*.stream().filter(visit -> { + var hash = data.job.hashes.get(visit.url); + return (hash == null || !Objects.equals(visit.data_hash_code, hash)); + }).collect(Collectors.toList());*/ + + var goodUrls = newContents.stream().map(EdgeUrlVisit::getUrl).collect(Collectors.toSet()); + + newUrls += newContents.size(); + + uploadFacade.putUrls(dc.extUrl, linkQualityRating); + uploadFacade.putUrls(dc.intUrl, linkQualityRating); + uploadFacade.putUrlVisits(newContents); + uploadFacade.putFeeds(dc.feeds); + + if (avgQuality < UploaderWorker.QUALITY_LOWER_BOUND_CUTOFF) { + uploadFacade.updateDomainIndexTimestamp(dc.domain, EdgeDomainIndexingState.ACTIVE, null, 1); + continue; + } + + uploadFacade.putLinks(dc.links, true); + uploadFacade.putWords(dc.pageContents.values().stream().filter(pc -> goodUrls.contains(pc.url)).collect(Collectors.toList()), 1); + + uploadFacade.updateDomainIndexTimestamp(dc.domain, EdgeDomainIndexingState.ACTIVE, null, Math.max(1, visits.size()/50)); + + logger.info("{} Done - {} : {} : {}", ++count, data.results.domain, allUrls, newUrls); + } + + logger.warn("Terminating {}", Thread.currentThread().getName()); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/StackOverflowLoaderMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/StackOverflowLoaderMain.java new file mode 100644 index 00000000..8173c1a1 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/StackOverflowLoaderMain.java @@ -0,0 +1,96 @@ +package nu.marginalia.wmsa.edge.tools; + +import com.zaxxer.hikari.HikariDataSource; +import nu.marginalia.util.ParallelPipe; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.DocumentKeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlFeature; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDaoImpl; +import nu.marginalia.wmsa.edge.index.client.EdgeIndexClient; +import nu.marginalia.wmsa.edge.integration.stackoverflow.StackOverflowPostProcessor; +import nu.marginalia.wmsa.edge.integration.BasicPageUploader; +import nu.marginalia.wmsa.edge.integration.stackoverflow.StackOverflowPostsReader; +import nu.marginalia.wmsa.edge.integration.model.BasicDocumentData; +import nu.marginalia.wmsa.edge.integration.stackoverflow.model.StackOverflowPost; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.mariadb.jdbc.Driver; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.SQLException; +import java.util.EnumSet; + +public class StackOverflowLoaderMain { + public static void main(String[] args) throws InterruptedException { + String site = args[0]; + String file = args[1]; + + if (!Files.exists(Path.of(file))) { + System.err.println("Invalid file " + file); + return; + } + + org.mariadb.jdbc.Driver driver = new Driver(); + + EdgeDomain domain = new EdgeDomain(site); + + var ds = new DatabaseModule().provideConnection(); + + EdgeDataStoreDaoImpl dataStoreDao = new EdgeDataStoreDaoImpl(ds); + EdgeIndexClient indexClient = new EdgeIndexClient(); + + dataStoreDao.putUrl(-2, new EdgeUrl("https", domain, null, "/")); + setDomainToSpecial(ds, domain); + + var lm = new LanguageModels( + Path.of("/var/lib/wmsa/model/ngrams-generous-emstr.bin"), + Path.of("/var/lib/wmsa/model/tfreq-new-algo3.bin"), + Path.of("/var/lib/wmsa/model/opennlp-sentence.bin"), + Path.of("/var/lib/wmsa/model/English.RDR"), + Path.of("/var/lib/wmsa/model/English.DICT"), + Path.of("/var/lib/wmsa/model/opennlp-tok.bin") + ); + + + var documentKeywordExtractor = new DocumentKeywordExtractor(new NGramDict(lm)); + BasicPageUploader uploader = new BasicPageUploader(dataStoreDao, indexClient, + EnumSet.of(HtmlFeature.TRACKING, HtmlFeature.JS)); + ThreadLocal processor = ThreadLocal.withInitial(() -> new StackOverflowPostProcessor(new SentenceExtractor(lm), documentKeywordExtractor)); + + var pipe = new ParallelPipe("pipe", 32, 5, 2) { + @Override + public BasicDocumentData onProcess(StackOverflowPost stackOverflowPost) { + return processor.get().process(stackOverflowPost); + } + + @Override + public void onReceive(BasicDocumentData stackOverflowIndexData) { + uploader.upload(stackOverflowIndexData); + } + }; + + System.out.println(domain); + var reader = new StackOverflowPostsReader(file, domain, pipe::accept); + reader.join(); + pipe.join(); + + ds.close(); + indexClient.close(); + } + + private static void setDomainToSpecial(HikariDataSource ds, EdgeDomain domain) { + try (var conn = ds.getConnection(); var stmt = conn.prepareStatement("UPDATE EC_DOMAIN SET STATE=? WHERE URL_PART=?")) { + stmt.setInt(1, EdgeDomainIndexingState.SPECIAL.code); + stmt.setString(2, domain.toString()); + stmt.executeUpdate(); + } + catch (SQLException ex) { + ex.printStackTrace(); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/TermFrequencyCounterMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/TermFrequencyCounterMain.java new file mode 100644 index 00000000..efdc9fa6 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/TermFrequencyCounterMain.java @@ -0,0 +1,142 @@ +package nu.marginalia.wmsa.edge.tools; + + +import gnu.trove.set.hash.TLongHashSet; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.archive.archiver.ArchiveExtractor; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.KeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.wmsa.edge.model.crawl.EdgeRawPageContents; +import opennlp.tools.stemmer.PorterStemmer; +import org.jsoup.Jsoup; + +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicLong; + +public class TermFrequencyCounterMain { + + static LinkedBlockingQueue processQueue = new LinkedBlockingQueue<>(20); + + public static final String OUTPUT_FILE = "/var/lib/wmsa/archive/tfreq-2022-04-04.bin"; + public static final String ARCHIVE_PATH = "/var/lib/wmsa/archive/webpage"; // "/mnt/storage/wmsa/archive/webpage/" + + @SneakyThrows + public static void main(String... args) throws IOException { + + List pt = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + pt.add(new Thread(TermFrequencyCounterMain::processorThread)); + } + pt.forEach(Thread::start); + + AtomicLong docsTotal = new AtomicLong(); + new ArchiveExtractor(Path.of(ARCHIVE_PATH)).forEach( + page -> { + if (page.contentType.contentType.contains("html") + && page.isAfter("2022-03-15T")) { + try { + long dt = docsTotal.incrementAndGet(); + if (dt == 0) { + System.out.println(docsTotal.get() + " - " + termFreq.size()); + } + if ((dt % 5) != 0) { + processQueue.put(page); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }); + running = false; + + + System.out.println("Waiting for wrap-up"); + + Thread.sleep(36000); + + for (Thread thread : pt) { + thread.interrupt(); + } + for (Thread thread : pt) { + thread.join(); + } + System.out.println("Total documents = " + docsTotal.get()); + + System.out.println("Writing Frequencies"); + + try (var dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(OUTPUT_FILE))) + ) { + synchronized (termFreq) { + for (var entry : termFreq.entrySet()) { + + if (entry.getValue() > 5) { + dos.writeLong(entry.getKey()); + dos.writeLong(entry.getValue()); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + + + System.out.println("All done!"); + } + + public static final ConcurrentHashMap termFreq = new ConcurrentHashMap<>(); + + public static final LanguageModels lm = new LanguageModels( + Path.of("/var/lib/wmsa/model/ngrams-generous-emstr.bin"), + Path.of("/var/lib/wmsa/model/tfreq-generous-emstr.bin"), + Path.of("/var/lib/wmsa/model/opennlp-sentence.bin"), + Path.of("/var/lib/wmsa/model/English.RDR"), + Path.of("/var/lib/wmsa/model/English.DICT"), + Path.of("/var/lib/wmsa/model/opennlp-tok.bin") + ); + public static volatile boolean running = true; + + public static void processorThread() { + var ke = new KeywordExtractor(); + var se = new SentenceExtractor(lm); + var ps = new PorterStemmer(); + try { + TLongHashSet words = new TLongHashSet(10000); + while (running || !processQueue.isEmpty()) { + var job = processQueue.take(); + var sentence = se.extractSentences(Jsoup.parse(job.data)); + + for (var sent : sentence.sentences) { + var keywords = ke.getKeywordsFromSentence(sent); + for (int i = 0; i < keywords.length; i++) { + if (keywords[i].size() > 1) { + words.add(NGramDict.longHash(sent.constructStemmedWordFromSpan(keywords[i]).getBytes())); + } + } + + for (String word : sent.wordsLowerCase) { + words.add(NGramDict.longHash(ps.stem(word).getBytes())); + } + + words.forEach(l -> { + termFreq.merge(l, 1, Integer::sum); + return true; + }); + words.clear(); + } + } + } + catch (InterruptedException ex) { + ex.printStackTrace(); + } + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/ZimConverterMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/ZimConverterMain.java new file mode 100644 index 00000000..10ac9042 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/edge/tools/ZimConverterMain.java @@ -0,0 +1,211 @@ +package nu.marginalia.wmsa.edge.tools; + +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.archive.client.ArchiveClient; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.assistant.dict.WikiCleaner; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import org.jsoup.Jsoup; +import org.openzim.ZIMTypes.ZIMFile; +import org.openzim.ZIMTypes.ZIMReader; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; + +public class ZimConverterMain { + + static LinkedBlockingQueue jobQueue = new LinkedBlockingQueue<>(100); + static LinkedBlockingQueue analysisQueue = new LinkedBlockingQueue<>(100); + static boolean hasData = true; + static ArchiveClient archiveClient = new ArchiveClient(); + static NGramDict dict = new NGramDict(new LanguageModels( + Path.of("/var/lib/wmsa/model/ngrams-generous-emstr.bin"), + Path.of("/var/lib/wmsa/model/tfreq-generous-emstr.bin"), + Path.of("/var/lib/wmsa/model/opennlp-sentence.bin"), + Path.of("/var/lib/wmsa/model/English.RDR"), + Path.of("/var/lib/wmsa/model/English.DICT"), + Path.of("/var/lib/wmsa/model/opennlp-tok.bin") + ) + ); + public void extractUrlList() throws IOException { + var zr = new ZIMReader(new ZIMFile("/home/vlofgren/Work/wikipedia_en_all_nopic_2021-01.zim")); + + var urlList = zr.getURLListByURL(); + + try (PrintWriter pw = new PrintWriter(new FileOutputStream("/home/vlofgren/Work/wikiTitlesAndRedirects.sql"))) { + zr.forEachTitles( + ae -> { + pw.printf("INSERT INTO REF_WIKI_TITLE(NAME) VALUES (\"%s\");\n", ae.getUrl().replace("\\", "\\\\").replace("\"", "\\\"")); + }, + re -> { + pw.printf("INSERT INTO REF_WIKI_TITLE(NAME, REF_NAME) VALUES (\"%s\",\"%s\");\n", re.getUrl().replace("\\", "\\\\").replace("\"", "\\\""), urlList.get(re.getRedirectIndex()).replace("\\", "\\\\").replace("\"", "\\\"")); + } + ); + } + } + + public static void main(String[] args) throws IOException { +// convertJust("Aleph_number"); +// convertJust("Floyd–Steinberg_dithering"); +// convertJust("Laplace's_equation"); +// convertJust("John_Fahey"); +// convertJust("Plotinus"); +// convertJust("C++"); + convertAll(args); + archiveClient.close(); + } + + @SneakyThrows + private static void convertJust(String url) { + String newData = new WikiCleaner().cleanWikiJunk("https://en.wikipedia.org/wiki/" + url, + Files.readString(Path.of("/home/vlofgren/Work/wiki-convert/", "in-" + url + ".html"))); + Files.writeString(Path.of("/home/vlofgren/Work/wiki-convert/", "out-" + url + ".html"), newData); + } + + private static void extractOne(String which, int clusterId) throws IOException { +// var zr = new ZIMReader(new ZIMFile(args[1])); + var zr = new ZIMReader(new ZIMFile("/home/vlofgren/Work/wikipedia_en_all_nopic_2021-01.zim")); + + int[] cluster = new int[] { clusterId }; + if (clusterId == -1) { + zr.forEachTitles(ae -> { + if (ae.getUrl().equals(which)) { + System.err.print(ae.getUrl() + " " + ae.getClusterNumber()); + cluster[0] = ae.getClusterNumber(); + } + }, re -> { + }); + } + + System.err.println("Extracting cluster " + cluster[0] ); + if (cluster[0] == -1) { + return; + } + zr.forEachArticles((url, art) -> { + if (art != null) { + if (which.equals(url)) { + try { + Files.writeString(Path.of("/home/vlofgren/Work/wiki-convert/","in-" + url + ".html"), art); + String newData = new WikiCleaner().cleanWikiJunk("https://en.wikipedia.org/wiki/" + url, art); + Files.writeString(Path.of("/home/vlofgren/Work/wiki-convert/", "out-" + url + ".html"), newData); + } catch (IOException e) { + e.printStackTrace(); + } + + } + scheduleJob(url, art); + } + }, p -> p == cluster[0]); + + } + + private static void convertAll(String[] args) throws IOException { + archiveClient.setServiceRoute("127.0.0.1", Integer.parseInt(args[0])); + var zr = new ZIMReader(new ZIMFile(args[1])); +// var zr = new ZIMReader(new ZIMFile("/home/vlofgren/Work/wikipedia_en_all_nopic_2021-01.zim")); + + for (int i = 0; i < 8; i++) { + Thread t = new Thread(ZimConverterMain::jobExecutor); + t.setName("Converter"); + t.start(); + + Thread t2 = new Thread(() -> { + for (; ; ) { + String pt; + try { + pt = analysisQueue.take(); + } catch (InterruptedException e) { + e.printStackTrace(); + return; + } +// var topic = new TopicWordExtractor().extractWords(pt); +// var words = new NGramTextRankExtractor(dict, topic).extractWords(Collections.emptyList(), pt); +// System.out.println(Strings.join(words, ',')); + } + }); + t2.setName("Analysis"); + t2.start(); + } + + zr.forEachArticles((url, art) -> { + if (art != null) { + scheduleJob(url, art); + } + }, p -> true); + + hasData = false; + archiveClient.close(); + } + + @SneakyThrows + private static void jobExecutor() { + while (hasData || !jobQueue.isEmpty()) { + var job = jobQueue.take(); + try { + job.convert(); + } + catch (Exception ex) { + System.err.println("Error in " + job.url); + ex.printStackTrace(); + } + } + } + + @SneakyThrows + private static void scheduleJob(String url, String art) { + jobQueue.put(new ConversionJob(art, url)); + } + + static Map wordCount = new ConcurrentHashMap<>(); + static boolean isKeyword(String word) { + + int limit = 100_000; + long n = word.chars().filter(c -> c=='_').count(); + if (n == 0) limit = 2; + if (n == 1) limit = 1; + if (n == 2) limit = 1; + if (n >= 3) limit = 1; + + long c = word.chars().filter(ch -> ch >= 'a' && ch <= 'z').count(); + if (c-2 <= n) { + return false; + } + int hashA = word.hashCode(); + int hashB = Objects.hash(n, c, word.length(), word.charAt(0)); + long hash = (long) hashA + ((long) hashB << 32); + + return wordCount.compute(hash, (k, v) -> v == null ? 1 : v+1) == limit; + } + @AllArgsConstructor + private static class ConversionJob { + private final String data; + private final String url; + + + public void convert() throws IOException, InterruptedException { + var page = new WikiCleaner().cleanWikiJunk("https://en.wikipedia.org/wiki/" + url, data); + String pt = Jsoup.parse(page).text(); + analysisQueue.put(pt); + + /* + + String newData = new WikiCleaner().cleanWikiJunk("https://en.wikipedia.org/wiki/" + url, data); + + + if (null != newData) { + archiveClient.submitWiki(Context.internal(), url, newData) + .retry(5) + .blockingSubscribe(); + + }*/ + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/Memex.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/Memex.java new file mode 100644 index 00000000..41959971 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/Memex.java @@ -0,0 +1,244 @@ +package nu.marginalia.wmsa.memex; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import io.reactivex.rxjava3.schedulers.Schedulers; +import nu.marginalia.gemini.GeminiService; +import nu.marginalia.gemini.gmi.GemtextDatabase; +import nu.marginalia.util.graphics.dithering.FloydSteinbergDither; +import nu.marginalia.util.graphics.dithering.Palettes; +import nu.marginalia.gemini.gmi.GemtextDocument; +import nu.marginalia.wmsa.memex.change.GemtextTombstoneUpdateCaclulator; +import nu.marginalia.wmsa.memex.model.MemexImage; +import nu.marginalia.wmsa.memex.model.MemexNode; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import nu.marginalia.wmsa.memex.renderer.MemexRendererers; +import nu.marginalia.wmsa.memex.system.MemexFileSystemMonitor; +import nu.marginalia.wmsa.memex.system.MemexFileWriter; +import nu.marginalia.wmsa.memex.system.MemexGitRepo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import javax.imageio.ImageIO; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +@Singleton +public class Memex { + + private final MemexData data; + private final MemexFileSystemMonitor monitor; + private final MemexGitRepo gitRepo; + private final MemexLoader loader; + + private final MemexFileWriter resources; + private final GemtextTombstoneUpdateCaclulator tombstoneUpdateCaclulator; + + private final FloydSteinbergDither ditherer = new FloydSteinbergDither(Palettes.MARGINALIA_PALETTE, 640, 480); + private final MemexRendererers renderers; + + private static final Logger logger = LoggerFactory.getLogger(Memex.class); + + @Inject + public Memex(MemexData data, + @Nullable MemexFileSystemMonitor monitor, + MemexGitRepo gitRepo, MemexLoader loader, + @Named("html") MemexFileWriter htmlFiles, + GemtextTombstoneUpdateCaclulator tombstoneUpdateCaclulator, + MemexRendererers renderers, + GeminiService geminiService) throws IOException { + this.data = data; + this.monitor = monitor; + this.gitRepo = gitRepo; + this.loader = loader; + this.resources = htmlFiles; + this.tombstoneUpdateCaclulator = tombstoneUpdateCaclulator; + this.renderers = renderers; + + Schedulers.io().scheduleDirect(this::load); + if (monitor != null) { + Schedulers.io().schedulePeriodicallyDirect(this::refreshUpdatedUrls, 1, 1, TimeUnit.SECONDS); + } + + Schedulers.newThread().scheduleDirect(geminiService::run); + } + + private void refreshUpdatedUrls() { + var updatedUrls = monitor.getUpdatedUrls(); + for (var url : updatedUrls) { + try { + if (url.toString().endsWith(".gmi")) { + var updates = loader.reloadNode(url); + updates.forEach(renderers::render); + + if (!updates.isEmpty()) { + renderers.render(url.getParentUrl()); + } + } else if (url.toString().endsWith(".png")) { + var updates = loader.reloadImage(url); + renderers.render(url); + + if (!updates.isEmpty()) { + renderers.render(url.getParentUrl()); + } + } + + if (tombstoneUpdateCaclulator.isTombstoneFile(url)) { + loader.loadTombstones().forEach(renderers::render); + } + if (tombstoneUpdateCaclulator.isRedirectFile(url)) { + loader.loadRedirects().forEach(renderers::render); + } + } + catch (Exception ex) { + logger.error("Failed to refresh URL " + url, ex); + } + } + } + + private void load() { + copyStylesheet(); + + try { + loader.load(); + renderAll(); + } + catch (IOException ex) { + logger.error("Failed to load", ex); + } + } + + private void copyStylesheet() { + try (var resource = Objects.requireNonNull( + ClassLoader.getSystemResourceAsStream("static/memex/style-new.css"), "Could not load stylesheet")) { + resources.write(new MemexNodeUrl("/style-new.css"), resource.readAllBytes()); + } + catch (Exception ex) { + logger.error("Failed to copy stylesheet", ex); + } + + try (var resource = Objects.requireNonNull( + ClassLoader.getSystemResourceAsStream("static/memex/ico/dir.png"), "Could not copy file")) { + resources.write(new MemexNodeUrl("/ico/dir.png"), resource.readAllBytes()); + } + catch (Exception ex) { + logger.error("Failed to copy file", ex); + } + + + try (var resource = Objects.requireNonNull( + ClassLoader.getSystemResourceAsStream("static/memex/ico/file.png"), "Could not copy file")) { + resources.write(new MemexNodeUrl("/ico/file.png"), resource.readAllBytes()); + } + catch (Exception ex) { + logger.error("Failed to copy file", ex); + } + + + try (var resource = Objects.requireNonNull( + ClassLoader.getSystemResourceAsStream("static/memex/ico/root.png"), "Could not copy file")) { + resources.write(new MemexNodeUrl("/ico/root.png"), resource.readAllBytes()); + } + catch (Exception ex) { + logger.error("Failed to copy file", ex); + } + + try (var resource = Objects.requireNonNull( + ClassLoader.getSystemResourceAsStream("static/memex/ico/pic16.png"), "Could not copy file")) { + resources.write(new MemexNodeUrl("/ico/pic16.png"), resource.readAllBytes()); + } + catch (Exception ex) { + logger.error("Failed to copy file", ex); + } + } + + private void renderAll() { + data.forEach((url, doc) -> { + renderers.render(url); + }); + data.getDirectories().forEach(renderers::render); + data.getImages().forEach(img -> renderers.render(img.path)); + + data.getTombstones().ifPresent(this::renderTombstoneFromGemtextDb); + data.getRedirects().ifPresent(this::renderTombstoneFromGemtextDb); + } + + + private void renderTombstoneFromGemtextDb(GemtextDatabase db) { + db.keys() + .stream() + .map(MemexNodeUrl::new) + .filter(url -> getDocument(url) == null) + .forEach(renderers::render); + } + + public void updateNode(MemexNodeUrl node, String text) throws IOException { + var nodes = loader.updateNode(node, text); + + nodes.forEach(renderers::render); + + renderers.render(node.getParentUrl()); + } + + public GemtextDocument getDocument(MemexNodeUrl url) { + return data.getDocument(url); + } + public MemexImage getImage(MemexNodeUrl url) { + return data.getImage(url); + } + + + public void createNode(MemexNodeUrl node, String text) throws IOException { + var nodes = loader.createNode(node, text); + + nodes.forEach(renderers::render); + + renderers.render(node.getParentUrl()); + } + + + public void uploadImage(MemexNodeUrl url, byte[] bytes) throws IOException { + + var image = ImageIO.read(new ByteArrayInputStream(bytes)); + var convertedImage = ditherer.convert(image); + var baosOut = new ByteArrayOutputStream(); + ImageIO.write(convertedImage, "png", baosOut); + + loader.uploadImage(url, baosOut.toByteArray()); + + renderers.render(url); + renderers.render(url.getParentUrl()); + } + + public void delete(MemexNode node, String message) throws IOException { + tombstoneUpdateCaclulator.addTombstone(node.getUrl(), message) + .visit(this); + loader.loadTombstones(); + loader.delete(node).forEach(renderers::render); + } + + public List getDocumentsByPath(MemexNodeUrl url) { + return data.getDocumentsByPath(url); + } + + public void gitPull() { + gitRepo.pull(); + } + + public void rename(MemexNode src, MemexNodeUrl dst) throws IOException { + tombstoneUpdateCaclulator.addRedirect(src.getUrl(), dst.toString()) + .visit(this); + loader.loadRedirects(); + loader.rename(src, dst).forEach(renderers::render); + } + + public byte[] getRaw(MemexNodeUrl url) throws IOException { + return loader.getRaw(url); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexConfigurationModule.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexConfigurationModule.java new file mode 100644 index 00000000..676ebc05 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexConfigurationModule.java @@ -0,0 +1,51 @@ +package nu.marginalia.wmsa.memex; + +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.name.Named; +import com.google.inject.name.Names; +import nu.marginalia.wmsa.memex.system.MemexFileWriter; + +import java.nio.file.Path; + +public class MemexConfigurationModule extends AbstractModule { + public void configure() { + bind(Path.class).annotatedWith(Names.named("memex-root")).toInstance(Path.of("/var/lib/wmsa/memex")); + bind(Path.class).annotatedWith(Names.named("memex-html-resources")).toInstance(Path.of("/var/lib/wmsa/memex-html")); + bind(Path.class).annotatedWith(Names.named("memex-gmi-resources")).toInstance(Path.of("/var/lib/wmsa/memex-gmi")); + bind(String.class).annotatedWith(Names.named("tombestone-special-file")).toInstance("/special/tombstone.gmi"); + bind(String.class).annotatedWith(Names.named("redirects-special-file")).toInstance("/special/redirect.gmi"); + + bind(MemexFileWriter.class).annotatedWith(Names.named("html")).toProvider(MemexHtmlWriterProvider.class); + bind(MemexFileWriter.class).annotatedWith(Names.named("gmi")).toProvider(MemexGmiWriterProvider.class); + } + + + + public static class MemexHtmlWriterProvider implements Provider { + private final Path path; + + @Inject + public MemexHtmlWriterProvider(@Named("memex-html-resources") Path resources) { + this.path = resources; + } + @Override + public MemexFileWriter get() { + return new MemexFileWriter(path); + } + } + + public static class MemexGmiWriterProvider implements Provider { + private final Path path; + + @Inject + public MemexGmiWriterProvider(@Named("memex-gmi-resources") Path resources) { + this.path = resources; + } + @Override + public MemexFileWriter get() { + return new MemexFileWriter(path); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexData.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexData.java new file mode 100644 index 00000000..22c20f8f --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexData.java @@ -0,0 +1,150 @@ +package nu.marginalia.wmsa.memex; + +import com.google.inject.Singleton; +import nu.marginalia.gemini.gmi.GemtextDatabase; +import nu.marginalia.gemini.gmi.GemtextDocument; +import nu.marginalia.wmsa.memex.model.MemexLink; +import nu.marginalia.wmsa.memex.model.MemexImage; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import nu.marginalia.wmsa.memex.model.fs.MemexFileSystem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.function.BiConsumer; + +@Singleton +public class MemexData { + private final MemexLinks links = new MemexLinks(); + private final Map documents = new HashMap<>(); + + private final Map images = new HashMap<>(); + private final MemexFileSystem fileSystem = new MemexFileSystem(); + private final Logger logger = LoggerFactory.getLogger(getClass()); + private GemtextDatabase tombstones = null; + private GemtextDatabase redirects = null; + + public synchronized Collection getImages() { + return new ArrayList<>(images.values()); + } + public synchronized Collection getDocuments() { return new ArrayList<>(documents.values()); } + + public synchronized void setTombstones(GemtextDatabase tombstones) { + this.tombstones = tombstones; + } + public synchronized void setRedirects(GemtextDatabase redirects) { + this.redirects = redirects; + } + + public synchronized void addDocument(MemexNodeUrl url, GemtextDocument doc) { + logger.debug("addDocument({})", url); + documents.put(url, doc); + fileSystem.register(doc); + } + + public synchronized void addImage(MemexNodeUrl url, MemexImage img) { + images.put(url, img); + fileSystem.register(img); + } + + public Optional getTombstones() { + return Optional.ofNullable(tombstones); + } + public Optional getRedirects() { + return Optional.ofNullable(redirects); + } + + public synchronized void updateOutlinks(MemexNodeUrl url, GemtextDocument doc) { + + var linksForNode = new TreeSet<>(Comparator.comparing(MemexLink::getDest)); + + MemexNodeUrl srcUrl = "index.gmi".equals(url.getFilename()) ? url.getParentUrl() : url; + + for (var link : doc.getLinks()) { + link.getUrl().visitNodeUrl(nodeUrl -> + linksForNode.add(new MemexLink(nodeUrl, srcUrl, doc.getTitle(), doc.getHeadingForElement(link), link.getHeading())) + ); + } + + links.setOutlinks(srcUrl, linksForNode); + } + + public synchronized Set getNeighbors(MemexNodeUrl url) { + return links.getNeighbors(url); + } + + public synchronized void forEach(BiConsumer consumer) { + documents.forEach(consumer); + } + + public synchronized GemtextDocument getDocument(MemexNodeUrl url) { + return documents.get(url); + } + + public synchronized MemexImage getImage(MemexNodeUrl url) { + return images.get(url); + } + public synchronized List getBacklinks(MemexNodeUrl... urls) { + return links.getBacklinks(urls); + } + + public synchronized List getDocumentsByPath(MemexNodeUrl url) { + return fileSystem.getDocuments(url); + } + public synchronized List getImagesByPath(MemexNodeUrl url) { + return fileSystem.getImages(url); + } + public synchronized List getSubdirsByPath(MemexNodeUrl url) { + return fileSystem.getSubdirs(url); + } + + public MemexFileSystem getFilesystem() { + return fileSystem; + } + + public List getDirectories() { + return fileSystem.getAllDirectories(); + } + public boolean isDirectory(MemexNodeUrl url) { + return fileSystem.isDirectory(url); + } + + public synchronized Set deleteImage(MemexNodeUrl url) { + images.remove(url); + fileSystem.remove(url); + + Set affectedUrls = new HashSet<>(); + + affectedUrls.add(url); + affectedUrls.add(url.getParentUrl()); + + return affectedUrls; + } + + public synchronized Set deleteDocument(MemexNodeUrl url) { + Set affectedUrls = new HashSet<>(); + + affectedUrls.add(url); + affectedUrls.add(url.getParentUrl()); + + links.getOutlinks(url) + .stream() + .map(MemexLink::getDest) + .forEach(affectedUrls::add); + + documents.remove(url); + fileSystem.remove(url); + + links.remove(url); + + return affectedUrls; + } + + public boolean hasTombstone(MemexNodeUrl url) { + if (tombstones != null && tombstones.getLinkData(url).isPresent()) + return true; + if (redirects != null && redirects.getLinkData(url).isPresent()) + return true; + return false; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexLinks.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexLinks.java new file mode 100644 index 00000000..8d491494 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexLinks.java @@ -0,0 +1,54 @@ +package nu.marginalia.wmsa.memex; + +import nu.marginalia.wmsa.memex.model.MemexLink; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; + +import java.util.*; +import java.util.stream.Collectors; + +public class MemexLinks { + private Map> backLinks = new HashMap<>(); + private final Map> links = new HashMap<>(); + + public void updateBacklinks() { + backLinks.clear(); + backLinks = links.values().stream() + .flatMap(Set::stream) + .collect(Collectors.groupingBy(MemexLink::getDest)); + } + + public Set getNeighbors(MemexNodeUrl url) { + final Set neighbors = new HashSet<>(); + + links.getOrDefault(url, Collections.emptySet()).stream().map(MemexLink::getDest) + .forEach(neighbors::add); + backLinks.getOrDefault(url, Collections.emptyList()).stream() + .map(MemexLink::getSrc) + .forEach(neighbors::add); + + return neighbors; + } + + public void setOutlinks(MemexNodeUrl url, TreeSet linksForNode) { + links.put(url, linksForNode); + updateBacklinks(); + } + + public List getBacklinks(MemexNodeUrl... urls) { + return Arrays.stream(urls) + .map(backLinks::get) + .filter(Objects::nonNull) + .flatMap(List::stream) + .sorted(Comparator.comparing(MemexLink::getSrc)) + .collect(Collectors.toList()); + } + + public Set getOutlinks(MemexNodeUrl url) { + return links.getOrDefault(url, Collections.emptySet()); + } + + public void remove(MemexNodeUrl url) { + links.remove(url); + updateBacklinks(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexLoader.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexLoader.java new file mode 100644 index 00000000..f5f6b29b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexLoader.java @@ -0,0 +1,265 @@ +package nu.marginalia.wmsa.memex; + +import com.google.common.collect.Sets; +import com.google.inject.Inject; +import com.google.inject.name.Named; +import nu.marginalia.gemini.gmi.GemtextDatabase; +import nu.marginalia.gemini.gmi.GemtextDocument; +import nu.marginalia.wmsa.memex.model.MemexImage; +import nu.marginalia.wmsa.memex.model.MemexNode; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import nu.marginalia.wmsa.memex.system.MemexFileSystemModifiedTimes; +import nu.marginalia.wmsa.memex.system.MemexSourceFileSystem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.CheckReturnValue; +import java.io.File; +import java.io.IOException; +import java.nio.file.*; +import java.util.*; + +public class MemexLoader { + private final MemexData data; + private final MemexFileSystemModifiedTimes modifiedTimes; + private final Path root; + private final MemexSourceFileSystem sourceFileSystem; + + private final String tombstonePath; + private final String redirectsPath; + + private static final Logger logger = LoggerFactory.getLogger(MemexLoader.class); + + @Inject + public MemexLoader(MemexData data, + MemexFileSystemModifiedTimes modifiedTimes, + MemexSourceFileSystem sourceFileSystem, + @Named("memex-root") Path root, + @Named("tombestone-special-file") String tombstonePath, + @Named("redirects-special-file") String redirectsPath) { + + this.data = data; + this.modifiedTimes = modifiedTimes; + this.sourceFileSystem = sourceFileSystem; + this.root = root; + this.tombstonePath = tombstonePath; + this.redirectsPath = redirectsPath; + } + + + public void load() throws IOException { + + loadTombstones(); + loadRedirects(); + + try (var files = Files.walk(root)) { + files.forEach(this::loadFile); + } + + data.getFilesystem().recalculateDirectories(); + + } + + private void loadFile(Path p) { + var file = p.toFile(); + + try { + if (p.toString().contains(".git")) { + return; + } + if (file.isDirectory() && !file.getName().startsWith(".")) { + data.getFilesystem().registerDir(MemexNodeUrl.ofRelativePath(root, p)); + } else if (isGemtext(file)) { + loadNode(p); + } else if (isImage(file)) { + loadImage(p); + } + } + catch (IOException ex) { + logger.error("Failed to load file " + p, ex); + } + } + + public void loadImage(Path p) throws IOException { + if (!modifiedTimes.isFreshUpdate(p)) { + return; + } + + var url = MemexNodeUrl.ofRelativePath(root, p); + data.addImage(url, new MemexImage(url, p)); + logger.info("Loading {}", p); + } + + public Set loadTombstones() { + var oldValues = data.getTombstones(); + var newValues = loadGemtextDb(Path.of(root + tombstonePath)); + + newValues.ifPresent(data::setTombstones); + + + if (newValues.isPresent()) { + if (oldValues.isPresent()) { + var oldTs = oldValues.get(); + var newTs = newValues.get(); + return oldTs.difference(newTs); + } + } + + return Collections.emptySet(); + } + + public Set loadRedirects() { + var oldValues = data.getTombstones(); + var newValues = loadGemtextDb(Path.of(root + redirectsPath)); + + newValues.ifPresent(data::setRedirects); + + if (newValues.isPresent()) { + if (oldValues.isPresent()) { + var oldTs = oldValues.get(); + var newTs = newValues.get(); + return oldTs.difference(newTs); + } + } + + return Collections.emptySet(); + } + + private Optional loadGemtextDb(Path p) { + if (Files.exists(p)) { + try { + return Optional.of(GemtextDatabase.of(MemexNodeUrl.ofRelativePath(root, p), p)); + } catch (IOException e) { + logger.error("Failed to load database " + p, e); + } + } + return Optional.empty(); + } + + private boolean isGemtext(File f) { + return f.isFile() && f.getName().endsWith(".gmi"); + } + + private boolean isImage(File f) { + return f.isFile() && f.getName().endsWith(".png"); + } + + @CheckReturnValue + public Collection updateNode(MemexNodeUrl url, String contents) throws IOException { + sourceFileSystem.replaceFile(url, contents); + return loadNode(url); + } + + @CheckReturnValue + public Collection createNode(MemexNodeUrl url, String contents) throws IOException { + sourceFileSystem.createFile(url, contents); + return loadNode(url); + } + + + public MemexImage uploadImage(MemexNodeUrl url, byte[] bytes) throws IOException { + sourceFileSystem.createFile(url, bytes); + + var img = new MemexImage(url, url.asAbsolutePath(root)); + data.addImage(url, img); + return img; + } + + + public Set reloadImage(MemexNodeUrl url) throws IOException { + var path = url.asAbsolutePath(root); + if (!Files.exists(path)) { + return data.deleteImage(url); + } + else { + loadImage(path); + Set affectedUrls = new HashSet<>(); + affectedUrls.add(url); + + for (var u = url.getParentUrl(); u != null; u = u.getParentUrl()) { + affectedUrls.add(u); + } + + return affectedUrls; + } + } + + public Set reloadNode(MemexNodeUrl url) throws IOException { + var path = url.asAbsolutePath(root); + if (!Files.exists(path)) { + return data.deleteDocument(url); + } + else { + return loadNode(path); + } + } + + public Set loadNode(Path path) throws IOException { + + if (!modifiedTimes.isFreshUpdate(path)) { + return Set.of(MemexNodeUrl.ofRelativePath(root, path)); + } + + logger.info("Loading {}", path); + + return loadNode(MemexNodeUrl.ofRelativePath(root, path)); + } + + public Set loadNode(MemexNodeUrl url) throws IOException { + + var doc = GemtextDocument.of(url, url.asAbsolutePath(root)); + + data.addDocument(url, doc); + + Set urlsAffected = data.getNeighbors(url); + + data.updateOutlinks(url, doc); + + urlsAffected.addAll(data.getNeighbors(url)); + urlsAffected.add(url); + urlsAffected.removeIf(u -> null == data.getDocument(u)); + + for (var u = url.getParentUrl(); u != null; u = u.getParentUrl()) { + urlsAffected.add(u); + } + + return urlsAffected; + } + + public Set delete(MemexNode node) throws IOException { + sourceFileSystem.delete(node.getUrl()); + return node.visit(new MemexNode.MemexNodeVisitor<>() { + @Override + public Set onDocument(MemexNodeUrl url) { + return data.deleteDocument(url); + } + + @Override + public Set onImage(MemexNodeUrl url) { + return data.deleteImage(url); + } + }); + } + + public Set rename(MemexNode src, MemexNodeUrl dst) throws IOException { + sourceFileSystem.renameFile(src.getUrl(), dst); + return src.visit(new MemexNode.MemexNodeVisitor>() { + @Override + public Set onDocument(MemexNodeUrl url) throws IOException { + var changes = data.deleteDocument(url); + return Sets.union(changes, reloadNode(dst)); + } + + @Override + public Set onImage(MemexNodeUrl url) throws IOException { + var changes = data.deleteImage(url); + return Sets.union(changes, reloadImage(dst)); + } + }); + + } + + public byte[] getRaw(MemexNodeUrl url) throws IOException { + return sourceFileSystem.getRaw(url); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexMain.java new file mode 100644 index 00000000..9ea88d2e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexMain.java @@ -0,0 +1,32 @@ +package nu.marginalia.wmsa.memex; + +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import nu.marginalia.gemini.GeminiConfigurationModule; +import nu.marginalia.wmsa.configuration.MainClass; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.module.ConfigurationModule; +import nu.marginalia.wmsa.configuration.server.Initialization; + +import java.io.IOException; + +public class MemexMain extends MainClass { + private MemexService service; + + @Inject + public MemexMain(MemexService service) { + this.service = service; + } + + public static void main(String... args) { + init(ServiceDescriptor.EDGE_MEMEX, args); + + Injector injector = Guice.createInjector( + new MemexConfigurationModule(), + new GeminiConfigurationModule(), + new ConfigurationModule()); + injector.getInstance(MemexMain.class); + injector.getInstance(Initialization.class).setReady(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexService.java new file mode 100644 index 00000000..4d22f1af --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/MemexService.java @@ -0,0 +1,280 @@ +package nu.marginalia.wmsa.memex; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import lombok.SneakyThrows; +import nu.marginalia.gemini.gmi.renderer.GemtextRendererFactory; +import nu.marginalia.wmsa.auth.client.AuthClient; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.configuration.server.MetricsServer; +import nu.marginalia.wmsa.configuration.server.Service; +import nu.marginalia.wmsa.memex.change.GemtextMutation; +import nu.marginalia.gemini.gmi.GemtextDocument; +import nu.marginalia.wmsa.memex.change.update.GemtextDocumentUpdateCalculator; +import nu.marginalia.wmsa.memex.renderer.MemexHtmlRenderer; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import nu.marginalia.wmsa.memex.model.render.*; +import org.apache.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Request; +import spark.Response; +import spark.Spark; + +import javax.servlet.MultipartConfigElement; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.Objects; + +import static spark.Spark.*; + +public class MemexService extends Service { + private final GemtextDocumentUpdateCalculator updateCalculator; + private final Memex memex; + private final MemexHtmlRenderer renderer; + private final AuthClient authClient; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Inject + public MemexService(@Named("service-host") String ip, + @Named("service-port") Integer port, + GemtextDocumentUpdateCalculator updateCalculator, + Memex memex, + MemexHtmlRenderer renderer, + AuthClient authClient, + Initialization initialization, + MetricsServer metricsServer) { + + super(ip, port, initialization, metricsServer); + + this.updateCalculator = updateCalculator; + this.memex = memex; + this.renderer = renderer; + this.authClient = authClient; + + Spark.get("git-pull", this::gitPull); + + Spark.path("public/api", () -> { + before((req, rsp) -> { + logger.info("{} {}", req.requestMethod(), req.pathInfo()); + }); + + post("/create", this::create); + get("/create", this::createForm, this::renderModel); + post("/upload", this::upload); + get("/upload", this::uploadForm, this::renderModel); + post("/update", this::update); + get("/update", this::updateForm, this::renderModel); + post("/rename", this::rename); + get("/rename", this::renameForm, this::renderModel); + post("/delete", this::delete); + get("/delete", this::deleteForm, this::renderModel); + + get("/raw", this::raw); + }); + } + + private Object raw(Request request, Response response) throws IOException { + final MemexNodeUrl url = new MemexNodeUrl(Objects.requireNonNull(request.queryParams("url"))); + + response.type(url.toNode().getType().mime); + response.header("Content-Disposition", "attachment; filename=" + url.getFilename()); + response.raw().getOutputStream().write(memex.getRaw(url)); + + return ""; + } + + private Object renameForm(Request request, Response response) { + final String type = Objects.requireNonNull(request.queryParams("type")); + final MemexNodeUrl url = new MemexNodeUrl(Objects.requireNonNull(request.queryParams("url"))); + + authClient.redirectToLoginIfUnauthenticated("MEMEX", request, response); + + if ("gmi".equals(type)) { + var doc = memex.getDocument(url); + if (null == doc) { + Spark.halt(404); + } + + final String docHtml = doc.render(new GemtextRendererFactory("", url.toString()).htmlRendererEditable()); + return new MemexRendererRenameFormModel(docHtml, + null, url, "gmi"); + } + else if ("img".equals(type)) { + var img = memex.getImage(url); + if (null == img) { + Spark.halt(404); + } + return new MemexRendererRenameFormModel(null, + new MemexRendererImageModel(img, Collections.emptyList(), null), + url, "img"); + } + + Spark.halt(HttpStatus.SC_BAD_REQUEST); + return null; + } + + private Object rename(Request request, Response response) throws IOException { + authClient.redirectToLoginIfUnauthenticated("MEMEX", request, response); + + var url = Objects.requireNonNull(request.queryParams("url")); + var name = Objects.requireNonNull(request.queryParams("name")); + var type = Objects.requireNonNull(request.queryParams("type")); + var confirm = Objects.requireNonNull(request.queryParams("confirm")); + + if (!"on".equals(confirm)) { + logger.error("Confirm dialog not checked, was {}", confirm); + Spark.halt(HttpStatus.SC_BAD_REQUEST, "Confirm was not checked"); + } + + memex.rename(new MemexNodeUrl(url).toNode(), new MemexNodeUrl(name)); + + response.redirect("https://memex.marginalia.nu/"+name); + return null; + + } + + private Object gitPull(Request request, Response response) { + logger.info("Git pull by request"); + memex.gitPull(); + return "Ok"; + } + + private String renderModel(Object model) { + return ((MemexRendererableDirect)model).render(renderer); + } + + private MemexRendererDeleteFormModel deleteForm(Request request, Response response) { + final String type = Objects.requireNonNull(request.queryParams("type")); + final MemexNodeUrl url = new MemexNodeUrl(Objects.requireNonNull(request.queryParams("url"))); + + authClient.redirectToLoginIfUnauthenticated("MEMEX", request, response); + + if ("gmi".equals(type)) { + var doc = memex.getDocument(url); + if (null == doc) { + Spark.halt(404); + } + + final String docHtml = doc.render(new GemtextRendererFactory("", url.toString()).htmlRendererEditable()); + return new MemexRendererDeleteFormModel(docHtml, + null, url, "gmi"); + } + else if ("img".equals(type)) { + var img = memex.getImage(url); + if (null == img) { + Spark.halt(404); + } + return new MemexRendererDeleteFormModel(null, + new MemexRendererImageModel(img, Collections.emptyList(), null), + url, "img"); + } + + Spark.halt(HttpStatus.SC_BAD_REQUEST); + return null; + } + + private Object delete(Request request, Response response) throws IOException { + authClient.requireLogIn(Context.fromRequest(request)); + + var url = Objects.requireNonNull(request.queryParams("url")); + var message = Objects.requireNonNull(request.queryParams("note")); + var type = Objects.requireNonNull(request.queryParams("type")); + var confirm = Objects.requireNonNull(request.queryParams("confirm")); + + if (!"on".equals(confirm)) { + logger.error("Confirm dialog not checked, was {}", confirm); + Spark.halt(HttpStatus.SC_BAD_REQUEST, "Confirm was not checked"); + } + + memex.delete(new MemexNodeUrl(url).toNode(), message); + + response.redirect("https://memex.marginalia.nu/"+url); + return null; + } + + private Object update(Request request, Response response) throws IOException { + authClient.requireLogIn(Context.fromRequest(request)); + + String extUrl = Objects.requireNonNull(request.queryParams("url")); + String extSection = Objects.requireNonNull(request.queryParams("section")); + String newSectionText = Objects.requireNonNull(request.queryParams("text")); + + var url = new MemexNodeUrl(extUrl); + var section = MemexNodeHeadingId.parse(extSection); + var lines = Arrays.asList(newSectionText.split("\r?\n")).toArray(String[]:: new); + + var sectionGemtext = new GemtextDocument(url, lines, section); + var updates = updateCalculator.calculateUpdates(memex.getDocument(url), section, sectionGemtext); + + for (GemtextMutation mutation : updates) { + mutation.visit(memex); + } + + response.redirect("https://memex.marginalia.nu/"+extUrl); + return ""; + } + + private Object create(Request request, Response response) throws IOException { + authClient.requireLogIn(Context.fromRequest(request)); + + String directory = Objects.requireNonNull(request.queryParams("directory")); + String filename = Objects.requireNonNull(request.queryParams("filename")); + String text = Objects.requireNonNull(request.queryParams("text")); + var url = new MemexNodeUrl(Path.of(directory).resolve(filename).toString()); + + memex.createNode(url, text); + + response.redirect("https://memex.marginalia.nu/"+directory + "/" + filename); + return ""; + } + + private Object createForm(Request request, Response response) { + final MemexNodeUrl url = new MemexNodeUrl(Objects.requireNonNull(request.queryParams("url"))); + authClient.redirectToLoginIfUnauthenticated("MEMEX", request, response); + + return new MemexRenderCreateFormModel(url, memex.getDocumentsByPath(url)); + } + + private Object uploadForm(Request request, Response response) { + final MemexNodeUrl url = new MemexNodeUrl(Objects.requireNonNull(request.queryParams("url"))); + authClient.redirectToLoginIfUnauthenticated("MEMEX", request, response); + + return new MemexRenderUploadFormModel(url, memex.getDocumentsByPath(url)); + } + + private Object updateForm(Request request, Response response) { + final MemexNodeUrl url = new MemexNodeUrl(Objects.requireNonNull(request.queryParams("url"))); + authClient.redirectToLoginIfUnauthenticated("MEMEX", request, response); + + var doc = memex.getDocument(url); + + return new MemexRenderUpdateFormModel(url, doc.getTitle(), "0", doc.getSectionGemtext(MemexNodeHeadingId.ROOT)); + } + + + @SneakyThrows + private Object upload(Request request, Response response) { + authClient.requireLogIn(Context.fromRequest(request)); + + request.attribute("org.eclipse.jetty.multipartConfig", new MultipartConfigElement("/temp", 50*1024*1024, 50*1024*1024, 25*1024*1024)); + + String directory = Objects.requireNonNull(request.queryParams("directory")); + String filename = Objects.requireNonNull(request.queryParams("filename")); + var url = new MemexNodeUrl(Path.of(directory).resolve(filename).toString()); + try (InputStream input = request.raw().getPart("file").getInputStream()) { + byte[] data = input.readAllBytes(); + memex.uploadImage(url, data); + } + + response.redirect("https://memex.marginalia.nu/"+directory + "/" + filename); + return ""; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextAppend.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextAppend.java new file mode 100644 index 00000000..be9c34dd --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextAppend.java @@ -0,0 +1,70 @@ +package nu.marginalia.wmsa.memex.change; + +import lombok.AllArgsConstructor; +import lombok.ToString; +import nu.marginalia.wmsa.memex.Memex; +import nu.marginalia.gemini.gmi.GemtextDocument; +import nu.marginalia.gemini.gmi.renderer.GemtextRendererFactory; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; + +import java.io.IOException; + +@AllArgsConstructor @ToString +public class GemtextAppend implements GemtextMutation { + public final MemexNodeUrl doc; + public final MemexNodeHeadingId id; + public final String[] lines; + + @Override + public void visit(Memex memex) throws IOException { + memex.updateNode(doc, calculateAppend(memex.getDocument(doc))); + } + + public String calculateAppend(GemtextDocument document) { + + StringBuilder result = new StringBuilder(); + var renderer = new GemtextRendererFactory().gemtextRendererAsIs(); + + var lines = document.getLines(); + + int i = 0; + // Copy from before heading + for (; i < lines.length; i++) { + var item = lines[i]; + + if (item.getHeading().isChildOf(id)) { + break; + } + else { + result.append(item.visit(renderer)).append('\n'); + } + } + + // Copy contents of heading + for (; i < lines.length; i++) { + var item = lines[i]; + + if (!item.getHeading().isChildOf(id)) { + break; + } + else { + result.append(item.visit(renderer)).append('\n'); + } + } + + // Insert new lines + for (String newLine : this.lines) { + result.append(newLine).append('\n'); + } + + // Copy contents from after heading + for (;i < lines.length; i++) { + var item = lines[i]; + result.append(item.visit(renderer)).append('\n'); + } + + return result.toString(); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextCreate.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextCreate.java new file mode 100644 index 00000000..9e479376 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextCreate.java @@ -0,0 +1,20 @@ +package nu.marginalia.wmsa.memex.change; + +import lombok.AllArgsConstructor; +import lombok.ToString; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.memex.Memex; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; + +import java.io.IOException; + +@AllArgsConstructor @ToString +public class GemtextCreate implements GemtextMutation { + public final MemexNodeUrl doc; + public final String text; + + @Override + public void visit(Memex memex) throws IOException { + memex.createNode(doc, text); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextCreateOrMutate.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextCreateOrMutate.java new file mode 100644 index 00000000..1d6498c2 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextCreateOrMutate.java @@ -0,0 +1,27 @@ +package nu.marginalia.wmsa.memex.change; + +import lombok.AllArgsConstructor; +import lombok.ToString; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.memex.Memex; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; + +import java.io.IOException; + +@AllArgsConstructor @ToString +public class GemtextCreateOrMutate implements GemtextMutation { + public final MemexNodeUrl doc; + public final String text; + public final GemtextMutation mutation; + + @Override + public void visit(Memex memex) throws IOException { + if (memex.getDocument(doc) == null) { + memex.createNode(doc, text); + } + if (memex.getDocument(doc) == null) + throw new IllegalStateException(); + + mutation.visit(memex); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextMutation.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextMutation.java new file mode 100644 index 00000000..eab2e7b1 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextMutation.java @@ -0,0 +1,20 @@ +package nu.marginalia.wmsa.memex.change; + +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.memex.Memex; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import nu.marginalia.wmsa.memex.model.MemexUrl; + +import java.io.IOException; + +public interface GemtextMutation { + void visit(Memex memex) throws IOException; + + static GemtextMutation createOrAppend(MemexNodeUrl url, String template, MemexNodeHeadingId heading, String... lines) { + return new GemtextCreateOrMutate(url, template, new GemtextAppend(url, heading, lines)); + } + static GemtextMutation createOrPrepend(MemexNodeUrl url, String template, MemexNodeHeadingId heading, String... lines) { + return new GemtextCreateOrMutate(url, template, new GemtextPrepend(url, heading, lines)); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextPrepend.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextPrepend.java new file mode 100644 index 00000000..873348d3 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextPrepend.java @@ -0,0 +1,64 @@ +package nu.marginalia.wmsa.memex.change; + +import lombok.AllArgsConstructor; +import lombok.ToString; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.memex.Memex; +import nu.marginalia.gemini.gmi.GemtextDocument; +import nu.marginalia.gemini.gmi.renderer.GemtextRendererFactory; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +@AllArgsConstructor @ToString +public class GemtextPrepend implements GemtextMutation { + public final MemexNodeUrl doc; + public final MemexNodeHeadingId id; + public final String[] lines; + + private static final Logger logger = LoggerFactory.getLogger(GemtextPrepend.class); + + @Override + public void visit(Memex memex) throws IOException { + memex.updateNode(doc, calculatePrepend(memex.getDocument(doc))); + } + + public String calculatePrepend(GemtextDocument document) { + StringBuilder result = new StringBuilder(); + var renderer = new GemtextRendererFactory().gemtextRendererAsIs(); + var lines = document.getLines(); + int i = 0; + for (; i < lines.length; i++) { + var item = lines[i]; + + if (item.getHeading().isChildOf(id)) { + if (!id.equals(MemexNodeHeadingId.ROOT)) { + result.append(item.visit(renderer)).append('\n'); + i++; + } + break; + } + else { + result.append(item.visit(renderer)).append('\n'); + } + } + + if (i == lines.length) { + logger.warn("Heading not found in prepending heading {} of {}, falling back to append-like behavior", + id, document.getUrl()); + } + for (String newLine : this.lines) { + result.append(newLine).append('\n'); + } + + for (;i < lines.length; i++) { + var item = lines[i]; + result.append(item.visit(renderer)).append('\n'); + } + + return result.toString(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextReplace.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextReplace.java new file mode 100644 index 00000000..a4caf685 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextReplace.java @@ -0,0 +1,66 @@ +package nu.marginalia.wmsa.memex.change; + +import lombok.AllArgsConstructor; +import lombok.ToString; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.memex.Memex; +import nu.marginalia.gemini.gmi.GemtextDocument; +import nu.marginalia.gemini.gmi.renderer.GemtextRendererFactory; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +@AllArgsConstructor @ToString +public class GemtextReplace implements GemtextMutation { + public final MemexNodeUrl doc; + public final MemexNodeHeadingId id; + public final String[] lines; + + private static final Logger logger = LoggerFactory.getLogger(GemtextPrepend.class); + + @Override + public void visit(Memex memex) throws IOException { + memex.updateNode(doc, calculateReplace(memex.getDocument(doc))); + } + + public String calculateReplace(GemtextDocument document) { + StringBuilder result = new StringBuilder(); + var renderer = new GemtextRendererFactory().gemtextRendererAsIs(); + + var lines = document.getLines(); + int i = 0; + for (; i < lines.length; i++) { + var item = lines[i]; + + if (item.getHeading().isChildOf(id)) { + break; + } + else { + result.append(item.visit(renderer)).append('\n'); + } + } + + if (i == lines.length) { + logger.error("Heading not found in replacing heading {} of {}, writing change-data to file", + id, document.getUrl()); + result.append("# Error! Replace failed!\n"); + } + + for (;i < lines.length && lines[i].getHeading().isChildOf(id); i++) { + } + + for (String newLine : this.lines) { + result.append(newLine).append('\n'); + } + + for (;i < lines.length; i++) { + var item = lines[i]; + result.append(item.visit(renderer)).append('\n'); + } + + return result.toString(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextTombstoneUpdateCaclulator.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextTombstoneUpdateCaclulator.java new file mode 100644 index 00000000..711e1f55 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/GemtextTombstoneUpdateCaclulator.java @@ -0,0 +1,48 @@ +package nu.marginalia.wmsa.memex.change; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; + +import com.google.inject.name.Named; + +import java.util.Objects; + +@Singleton +public class GemtextTombstoneUpdateCaclulator { + private final String tombstonePath; + private final String redirectsPath; + + @Inject + public GemtextTombstoneUpdateCaclulator(@Named("tombestone-special-file") String tombstonePath, + @Named("redirects-special-file") String redirectsPath) { + + this.tombstonePath = tombstonePath; + this.redirectsPath = redirectsPath; + } + + public boolean isTombstoneFile(MemexNodeUrl url) { + return Objects.equals(url, new MemexNodeUrl(tombstonePath)); + } + public boolean isRedirectFile(MemexNodeUrl url) { + return Objects.equals(url, new MemexNodeUrl(redirectsPath)); + } + + public GemtextMutation addTombstone(MemexNodeUrl url, String message) { + var tombstoneUrl = new MemexNodeUrl(tombstonePath); + + return new GemtextCreateOrMutate(tombstoneUrl, "# Tombstones", + new GemtextAppend(tombstoneUrl, new MemexNodeHeadingId(0), + new String[] { String.format("=> %s\t%s", url, message)})); + } + + public GemtextMutation addRedirect(MemexNodeUrl url, String message) { + var redirectsUrl = new MemexNodeUrl(redirectsPath); + + return new GemtextCreateOrMutate(redirectsUrl, "# Redirects", + new GemtextAppend(redirectsUrl, new MemexNodeHeadingId(0), + new String[] { String.format("=> %s\t%s", url, message)})); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/update/GemtextDocumentUpdateCalculator.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/update/GemtextDocumentUpdateCalculator.java new file mode 100644 index 00000000..51142ed0 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/update/GemtextDocumentUpdateCalculator.java @@ -0,0 +1,109 @@ +package nu.marginalia.wmsa.memex.change.update; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import nu.marginalia.gemini.gmi.line.GemtextText; +import nu.marginalia.wmsa.memex.Memex; +import nu.marginalia.gemini.gmi.GemtextDocument; +import nu.marginalia.gemini.gmi.line.AbstractGemtextLine; +import nu.marginalia.gemini.gmi.line.GemtextHeading; +import nu.marginalia.gemini.gmi.renderer.GemtextRenderer; +import nu.marginalia.gemini.gmi.renderer.GemtextRendererFactory; +import nu.marginalia.wmsa.memex.change.*; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +@Singleton +public class GemtextDocumentUpdateCalculator { + private final GemtextRenderer rawRenderer = new GemtextRendererFactory().gemtextRendererAsIs(); + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Memex memex; + + @Inject + public GemtextDocumentUpdateCalculator(Memex memex) { + this.memex = memex; + } + + public List calculateUpdates(GemtextDocument original, + MemexNodeHeadingId destId, + GemtextDocument newSection) + { + + var rewrite = new GemtextTasksRewrite(memex, original, destId, newSection); + var lines = newSection.getLines(); + + for (int i = 0; i < lines.length; i = Math.max(i+1, rewrite.processLine(lines, i))); + + List updates = new ArrayList<>(); + + updates.addAll(createKeepUpdates(original, destId, rewrite)); + updates.addAll(createDoneUpdates(original, destId, rewrite)); + updates.addAll(createTodoUpdates(original, rewrite)); + + return updates; + } + + private Collection createTodoUpdates(GemtextDocument original, GemtextTasksRewrite rewrite) { + if (!rewrite.getPushToTodo().isEmpty()) { + var doneDoc = original.getUrl().sibling("todo.gmi"); + + var update = createTodoAction(rewrite.getPushToTodo(), doneDoc); + return List.of(update); + } + return Collections.emptyList(); + } + + private Collection createDoneUpdates(GemtextDocument original, MemexNodeHeadingId destId, GemtextTasksRewrite rewrite) { + if (!rewrite.getPushToDone().isEmpty()) { + + var doneDocUrl = original.getUrl().sibling("done.gmi"); + final String doneHeadingName = rewrite.getTodaysDoneHeadingName(); + + var newDestId = + Optional.ofNullable(memex.getDocument(doneDocUrl)) + .flatMap(dest -> dest.getHeadingByName(MemexNodeHeadingId.ROOT, doneHeadingName)); + + if (newDestId.isEmpty()) { + rewrite.getPushToDone().addAll(0, + List.of(new GemtextText("", MemexNodeHeadingId.ROOT), + new GemtextHeading(new MemexNodeHeadingId(1,1), doneHeadingName, destId)) + ); + } + + var update = createDoneAction(rewrite.getPushToDone(), doneDocUrl, newDestId.orElse(new MemexNodeHeadingId(1))); + return List.of(update); + } + return Collections.emptyList(); + } + + private Collection createKeepUpdates(GemtextDocument original, MemexNodeHeadingId destId, GemtextTasksRewrite rewrite) { + if (!rewrite.getKeep().isEmpty()) { + return List.of(new GemtextReplace(original.getUrl(), destId, rewrite.getKeep().stream().map(rawRenderer::renderLine).toArray(String[]::new))); + } + return Collections.emptyList(); + } + + + @NotNull + private GemtextCreateOrMutate createDoneAction(List pushToDone, MemexNodeUrl doneDoc, MemexNodeHeadingId newDestId) { + return new GemtextCreateOrMutate( + doneDoc, "%%% TASKS\n# Done", + new GemtextPrepend(doneDoc, newDestId, pushToDone.stream().map(rawRenderer::renderLine).toArray(String[]::new)) + ); + } + + @NotNull + private GemtextCreateOrMutate createTodoAction(List pushToTodo, MemexNodeUrl doneDoc) { + return new GemtextCreateOrMutate( + doneDoc, "%%% TASKS\n# Todo", + new GemtextAppend(doneDoc, new MemexNodeHeadingId(1), pushToTodo.stream().map(rawRenderer::renderLine).toArray(String[]::new)) + ); + } + +} + diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/update/GemtextTaskExtractor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/update/GemtextTaskExtractor.java new file mode 100644 index 00000000..4bdc1ce4 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/update/GemtextTaskExtractor.java @@ -0,0 +1,31 @@ +package nu.marginalia.wmsa.memex.change.update; + +import nu.marginalia.gemini.gmi.line.AbstractGemtextLine; +import nu.marginalia.gemini.gmi.line.GemtextTask; + +import java.util.List; + +class GemtextTaskExtractor { + + public static int extractTask(List dest, AbstractGemtextLine[] lines, int i) { + var taskId = ((GemtextTask) lines[i]).getId(); + + int j; + for (j = i; j < lines.length; j++) { + var item = lines[j]; + if (item.mapTask(GemtextTask::getId).map(id -> id.isChildOf(taskId)).orElse(false)) { + dest.add(item); + } + else if (!item.breaksTask()) { + dest.add(item); + } + else { + break; + } + } + if (j < lines.length) { + return Math.max(i+1, j-1); + } + return lines.length; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/update/GemtextTasksRewrite.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/update/GemtextTasksRewrite.java new file mode 100644 index 00000000..c2266f86 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/change/update/GemtextTasksRewrite.java @@ -0,0 +1,100 @@ +package nu.marginalia.wmsa.memex.change.update; + +import lombok.Getter; +import nu.marginalia.gemini.gmi.GemtextDocument; +import nu.marginalia.gemini.gmi.line.AbstractGemtextLine; +import nu.marginalia.gemini.gmi.line.GemtextTask; +import nu.marginalia.wmsa.memex.Memex; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import org.jetbrains.annotations.NotNull; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +@Getter +class GemtextTasksRewrite { + private final List keep = new ArrayList<>(); + private final List pushToTodo = new ArrayList<>(); + private final List pushToDone = new ArrayList<>(); + + private final String rootHeadingName = "Done"; + private final String todoHeadingName = "Todo"; + private final String backlogHeadingName = "Backlog"; + + private final boolean isDestTodo; + private final boolean isDestDone; + private final Memex memex; + private final GemtextDocument original; + private final MemexNodeHeadingId destId; + private final GemtextDocument newSection; + + GemtextTasksRewrite(Memex memex, GemtextDocument original, MemexNodeHeadingId destId, GemtextDocument newSection) { + this.memex = memex; + this.original = original; + this.destId = destId; + this.newSection = newSection; + + + isDestTodo = isDestTodo(original, destId); + isDestDone = isDestDone(original, destId); + } + + public int processLine(AbstractGemtextLine[] lines, int i) { + var line = lines[i]; + + if (!line.mapTask(GemtextTask::getLevel).map(level -> 1 == level).orElse(false)) { + keep.add(line); + return i + 1; + } + + // It's a task + + boolean isTaskDone = line.mapTask(GemtextTask::getState).map(state -> state.done).orElse(false); + boolean isChangeDestDone = matchHeadingHierarchy(newSection, line.getHeading(), heading -> heading.contains(rootHeadingName)); + boolean isChangeDestTodo = isDestTodo(newSection, line.getHeading()); + + if (isTaskDone && !isDestDone && !isChangeDestDone) { + return GemtextTaskExtractor.extractTask(pushToDone, lines, i); + } else if (!isTaskDone && !isDestTodo && !isChangeDestTodo) { + return GemtextTaskExtractor.extractTask(pushToTodo, lines, i); + } + + keep.add(line); + return i + 1; + } + + private boolean isDestDone(GemtextDocument original, MemexNodeHeadingId destId) { + + final String currentHeadingName = getTodaysDoneHeadingName(); + + return matchHeadingHierarchy(original, destId, heading -> heading.contains(currentHeadingName)) + || matchHeadingHierarchy(original, destId, heading -> heading.contains(rootHeadingName)); + } + + @NotNull + public String getTodaysDoneHeadingName() { + return "Done " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE); + } + + private boolean isDestTodo(GemtextDocument original, MemexNodeHeadingId destId) { + return matchHeadingHierarchy(original, destId, heading -> heading.contains(todoHeadingName)) + || matchHeadingHierarchy(original, destId, heading -> heading.contains(backlogHeadingName)); + } + + + boolean matchHeadingHierarchy(GemtextDocument doc, MemexNodeHeadingId heading, Predicate p) { + + for (; !heading.equals(MemexNodeHeadingId.ROOT); heading = heading.parent()) { + var maybeTitle = doc.getHeading(heading); + if (maybeTitle.map(p::test).orElse(false)) { + return true; + } + + } + return false; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/client/MemexApiClient.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/client/MemexApiClient.java new file mode 100644 index 00000000..96ca5239 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/client/MemexApiClient.java @@ -0,0 +1,16 @@ +package nu.marginalia.wmsa.memex.client; + +import com.google.inject.Inject; +import io.reactivex.rxjava3.core.Observable; +import nu.marginalia.wmsa.client.AbstractDynamicClient; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.server.Context; + + +public class MemexApiClient extends AbstractDynamicClient { + @Inject + public MemexApiClient() { + super(ServiceDescriptor.EDGE_MEMEX); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/GemtextSection.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/GemtextSection.java new file mode 100644 index 00000000..5fc3cc73 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/GemtextSection.java @@ -0,0 +1,11 @@ +package nu.marginalia.wmsa.memex.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor @Getter +public class GemtextSection { + public final MemexNodeHeadingId id; + public final GemtextSectionAction action; + public final String[] lines; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/GemtextSectionAction.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/GemtextSectionAction.java new file mode 100644 index 00000000..16cb8158 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/GemtextSectionAction.java @@ -0,0 +1,6 @@ +package nu.marginalia.wmsa.memex.model; + +public enum GemtextSectionAction { + REPLACE, + APPEND +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexExternalUrl.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexExternalUrl.java new file mode 100644 index 00000000..38775753 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexExternalUrl.java @@ -0,0 +1,18 @@ +package nu.marginalia.wmsa.memex.model; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +import java.util.Optional; + +@AllArgsConstructor @Getter @EqualsAndHashCode +public class MemexExternalUrl implements MemexUrl { + public final String url; + + public String toString() { + return url; + } + @Override + public Optional getExternUrl() { return Optional.of(this); } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexImage.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexImage.java new file mode 100644 index 00000000..e0184d03 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexImage.java @@ -0,0 +1,13 @@ +package nu.marginalia.wmsa.memex.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.nio.file.Path; + +@AllArgsConstructor @Getter +public class MemexImage { + public final MemexNodeUrl path; + public final Path realPath; + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexIndexTask.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexIndexTask.java new file mode 100644 index 00000000..38c79410 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexIndexTask.java @@ -0,0 +1,13 @@ +package nu.marginalia.wmsa.memex.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@AllArgsConstructor @ToString @Getter +public class MemexIndexTask { + public final String task; + public final String taskId; + public final String url; + public final String type; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexLink.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexLink.java new file mode 100644 index 00000000..c44eaa26 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexLink.java @@ -0,0 +1,26 @@ +package nu.marginalia.wmsa.memex.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +@Getter @AllArgsConstructor +public class MemexLink { + public final MemexNodeUrl dest; + public final MemexNodeUrl src; + public final String title; + public final String section; + public final MemexNodeHeadingId sectionId; + + public final MemexNodeUrl getUrl() { + return src; + } + + public String getDescription() { + if (Objects.equals(title, section)) { + return title; + } + return title + " - " + section; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNode.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNode.java new file mode 100644 index 00000000..ddd01f82 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNode.java @@ -0,0 +1,40 @@ +package nu.marginalia.wmsa.memex.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.SneakyThrows; + +@AllArgsConstructor @Getter +public class MemexNode { + private final MemexNodeUrl url; + + public MemexNodeType getType() { + var fn = url.getFilename(); + if (fn.endsWith(".gmi")) { + return MemexNodeType.DOCUMENT; + } + else if (fn.endsWith(".png")) { + return MemexNodeType.IMAGE; + } + else if (fn.endsWith(".txt")) { + return MemexNodeType.TEXT; + } + else if (fn.contains(".")) { + return MemexNodeType.OTHER; + } + return MemexNodeType.DIRECTORY; + } + + @SneakyThrows + public T visit(MemexNodeVisitor visitor) { + return switch (getType()) { + case DOCUMENT -> visitor.onDocument(url); + case IMAGE -> visitor.onImage(url); + default -> null; + }; + } + public interface MemexNodeVisitor { + T onDocument(MemexNodeUrl url) throws Exception; + T onImage(MemexNodeUrl url) throws Exception; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNodeHeadingId.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNodeHeadingId.java new file mode 100644 index 00000000..084e22fe --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNodeHeadingId.java @@ -0,0 +1,74 @@ +package nu.marginalia.wmsa.memex.model; + +import lombok.EqualsAndHashCode; + +import java.util.Arrays; +import java.util.stream.Collectors; + +@EqualsAndHashCode +public class MemexNodeHeadingId implements Comparable { + private final int[] ids; + + public static final MemexNodeHeadingId ROOT = new MemexNodeHeadingId(0); + + public MemexNodeHeadingId(int... ids) { + this.ids = ids; + } + + public static MemexNodeHeadingId parse(String section) { + return new MemexNodeHeadingId(Arrays.stream(section.split("\\.")).mapToInt(Integer::parseInt).toArray()); + } + + public int getLevel() { + return ids.length; + } + + public int[] getIds() { + return ids; + } + public boolean isChildOf(MemexNodeHeadingId other) { + if (other.equals(ROOT)) { + return true; + } + if (other.ids.length > ids.length) { + return false; + } + + for (int i = 0; i < other.ids.length; i++) { + if (other.ids[i] != ids[i]) { + return false; + } + } + + return true; + } + + // This does not have the same semantics as Arrays$compare + + public int compareTo(MemexNodeHeadingId other) { + for (int i = 0; i < Math.min(ids.length, other.ids.length); i++) { + if (other.ids[i] != ids[i]) { + return ids[i] - other.ids[i]; + } + } + + return other.ids.length - ids.length; + } + + public MemexNodeHeadingId parent() { + if (ids.length <= 1) + return ROOT; + else return new MemexNodeHeadingId(Arrays.copyOfRange(ids, 0, ids.length-1)); + + } + public MemexNodeHeadingId next(int level) { + int[] newIds = Arrays.copyOf(ids, level+1); + newIds[level]++; + return new MemexNodeHeadingId(newIds); + } + + @Override + public String toString() { + return Arrays.stream(ids).mapToObj(Integer::toString).collect(Collectors.joining(".")); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNodeTaskId.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNodeTaskId.java new file mode 100644 index 00000000..e40dccbf --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNodeTaskId.java @@ -0,0 +1,66 @@ +package nu.marginalia.wmsa.memex.model; + +import lombok.EqualsAndHashCode; + +import java.util.Arrays; +import java.util.stream.Collectors; + +@EqualsAndHashCode +public class MemexNodeTaskId implements Comparable { + private final int[] ids; + + public MemexNodeTaskId(int... ids) { + this.ids = ids; + } + + public static MemexNodeTaskId parse(String section) { + return new MemexNodeTaskId(Arrays.stream(section.split("\\.")).mapToInt(Integer::parseInt).toArray()); + } + + public int level() { + return ids.length; + } + + public boolean isChildOf(MemexNodeTaskId other) { + if (other.ids.length > ids.length) { + return false; + } + + for (int i = 0; i < other.ids.length; i++) { + if (other.ids[i] != ids[i]) { + return false; + } + } + + return true; + } + + // This does not have the same semantics as Arrays$compare + + public int compareTo(MemexNodeTaskId other) { + for (int i = 0; i < Math.min(ids.length, other.ids.length); i++) { + if (other.ids[i] != ids[i]) { + return ids[i] - other.ids[i]; + } + } + + return other.ids.length - ids.length; + } + + public MemexNodeTaskId parent() { + if (ids.length <= 1) + return new MemexNodeTaskId(0); + else return new MemexNodeTaskId(Arrays.copyOfRange(ids, 0, ids.length-1)); + + } + public MemexNodeTaskId next(int level) { + int[] newIds = Arrays.copyOf(ids, level+1); + newIds[level]++; + return new MemexNodeTaskId(newIds); + } + + @Override + public String toString() { + return Arrays.stream(ids).mapToObj(Integer::toString).collect(Collectors.joining(".")); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNodeType.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNodeType.java new file mode 100644 index 00000000..6c645921 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNodeType.java @@ -0,0 +1,14 @@ +package nu.marginalia.wmsa.memex.model; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public enum MemexNodeType { + DOCUMENT("text/gemini"), + IMAGE("image/png"), + DIRECTORY("other/directory"), + TEXT("text/plain"), + OTHER("application/binary"); + + public String mime; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNodeUrl.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNodeUrl.java new file mode 100644 index 00000000..96d91bd0 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexNodeUrl.java @@ -0,0 +1,98 @@ +package nu.marginalia.wmsa.memex.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.nio.file.Path; +import java.util.Optional; +import java.util.function.Consumer; + +@Getter @EqualsAndHashCode +public class MemexNodeUrl implements MemexUrl, Comparable { + private final String url; + + public MemexNodeUrl(String url) { + if (url.startsWith("//")) { + this.url = url.substring(1); + } else { + this.url = url; + } + } + public static MemexNodeUrl ofRelativePath(Path root, Path relative) { + Path path; + + if (relative.startsWith("/")) { + path = root.relativize(relative); + } + else { + path = relative; + } + + if (File.separatorChar == '\\') + return new MemexNodeUrl("/" + path.toString().replace('\\', '/')); + return new MemexNodeUrl("/" + path); + } + + public String toString() { + return url; + } + + public String getParentStr() { + var path = asRelativePath().getParent(); + if (path == null) { + return null; + } + return path.toString(); + } + public MemexNodeUrl getParentUrl() { + var str = getParentStr(); + if (str == null) { + return null; + } + return new MemexNodeUrl(str); + } + public MemexNodeUrl sibling(String name) { + return new MemexNodeUrl(asRelativePath().resolveSibling(name).toString()); + } + public MemexNodeUrl child(String name) { + return new MemexNodeUrl(asRelativePath().resolve(name).toString()); + } + + public Path asRelativePath() { + return Path.of(url); + } + + public Path asAbsolutePath(Path root) { + Path p = Path.of(root + url); + if (p.toString().contains(".git")) { + throw new IllegalStateException(url + " touched .git"); + } + if (!p.normalize().startsWith(root)) { + throw new IllegalStateException(url + " escaped Memex root as " + p); + } + return p; + } + + + public String getFilename() { return asRelativePath().toFile().getName(); } + + @Override + public void visitNodeUrl(Consumer fn) { + fn.accept(this); + } + + @Override + public Optional getNodeUrl() { + return Optional.of(this); + } + @Override + public int compareTo(@NotNull MemexNodeUrl o) { + return url.compareTo(o.getUrl()); + } + + public MemexNode toNode() { + return new MemexNode(this); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexTaskState.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexTaskState.java new file mode 100644 index 00000000..b110f878 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexTaskState.java @@ -0,0 +1,30 @@ +package nu.marginalia.wmsa.memex.model; + +public enum MemexTaskState { + DONE('/', true,"done"), + SKIP('x', true,"skip"), + SKIP2('-', true,"skip"), + UNKNOWN('?', false, "unknown"), + URGENT('!', false, "urgent"), + TODO(0, false, "todo"); + + public int key; + public String style; + public boolean done; + + MemexTaskState(int key, boolean done, String style) { + this.key = key; + this.style = style; + this.done = done; + } + + public static MemexTaskState of(MemexTaskTags tags) { + for (MemexTaskState state : values()) { + if (tags.hasTag(state.key)) { + return state; + } + } + return TODO; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexTaskTags.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexTaskTags.java new file mode 100644 index 00000000..e19819bc --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexTaskTags.java @@ -0,0 +1,40 @@ +package nu.marginalia.wmsa.memex.model; + +import lombok.Getter; + +import java.util.stream.Collectors; + +@Getter +public class MemexTaskTags { + public final String tagsCondensed; + + private static final int TAG_START = '('; + private static final int TAG_END = ')'; + + public MemexTaskTags(String text) { + tagsCondensed = getTags(text); + } + + public boolean hasTag(int tag) { + return tagsCondensed.indexOf(tag) >= 0; + } + + @Override + public String toString() { + return tagsCondensed.chars().mapToObj(c -> '(' + Character.toString(c) + ')') + .collect(Collectors.joining(" ")); + } + + private static String getTags(String task) { + StringBuilder sb = new StringBuilder(); + for (int i = task.indexOf(TAG_START); + i >= 0 && i+2 < task.length(); + i = task.indexOf(TAG_START, i+1)) + { + if (task.charAt(i+2) == TAG_END) { + sb.append(task.charAt(i+1)); + } + } + return sb.toString(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexUrl.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexUrl.java new file mode 100644 index 00000000..14cff995 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/MemexUrl.java @@ -0,0 +1,13 @@ +package nu.marginalia.wmsa.memex.model; + +import java.util.Optional; +import java.util.function.Consumer; + +public interface MemexUrl { + String getUrl(); + + default void visitNodeUrl(Consumer fn) {} + default void visitExternalUrl(Consumer fn) {} + default Optional getNodeUrl() { return Optional.empty(); } + default Optional getExternUrl() { return Optional.empty(); } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/fs/MemexDirectory.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/fs/MemexDirectory.java new file mode 100644 index 00000000..deacc639 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/fs/MemexDirectory.java @@ -0,0 +1,30 @@ +package nu.marginalia.wmsa.memex.model.fs; + +import lombok.Getter; +import nu.marginalia.gemini.gmi.GemtextDocument; +import nu.marginalia.wmsa.memex.model.MemexImage; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; + +import java.util.HashMap; +import java.util.Map; + +@Getter +public class MemexDirectory { + private final Map documents; + private final Map images; + private final Map subdirs; + + public MemexDirectory() { + documents = new HashMap<>(); + images = new HashMap<>(); + subdirs = new HashMap<>(); + } + + public void removeDocument(MemexNodeUrl url) { + documents.remove(url); + } + + public void removeImage(MemexNodeUrl url) { + images.remove(url); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/fs/MemexFileSystem.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/fs/MemexFileSystem.java new file mode 100644 index 00000000..ffd31508 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/fs/MemexFileSystem.java @@ -0,0 +1,88 @@ +package nu.marginalia.wmsa.memex.model.fs; + +import nu.marginalia.gemini.gmi.GemtextDocument; +import nu.marginalia.wmsa.memex.model.MemexImage; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class MemexFileSystem { + private final Map fileSystemContentsByDir = new ConcurrentHashMap<>(); + + public MemexFileSystem() { + } + + public Optional get(MemexNodeUrl url) { + return Optional.ofNullable(fileSystemContentsByDir.get(url)); + } + public List getDocuments(MemexNodeUrl url) { + var contents = fileSystemContentsByDir.get(url); + if (contents == null) { + return Collections.emptyList(); + } + var list = new ArrayList<>(contents.getDocuments().values()); + list.sort(Comparator.comparing(GemtextDocument::getUrl)); + return list; + } + + public List getImages(MemexNodeUrl url) { + var contents = fileSystemContentsByDir.get(url); + if (contents == null) { + return Collections.emptyList(); + } + var list = new ArrayList<>(contents.getImages().values()); + list.sort(Comparator.comparing(MemexImage::getPath)); + return list; + } + + public List getSubdirs(MemexNodeUrl url) { + var contents = fileSystemContentsByDir.get(url); + if (contents == null) { + return Collections.emptyList(); + } + var list = new ArrayList<>(contents.getSubdirs().keySet()); + list.sort(Comparator.naturalOrder()); + return list; + } + + public void recalculateDirectories() { + fileSystemContentsByDir.forEach((k, v) -> { + var parent = k.getParentUrl(); + if (parent != null) { + registerDir(k.getParentUrl()).getSubdirs().put(k, v); + } + }); + } + + public MemexDirectory registerDir(MemexNodeUrl url) { + return fileSystemContentsByDir + .computeIfAbsent(url, p -> new MemexDirectory()); + } + + public void register(MemexImage image) { + registerDir(image.path.getParentUrl()) + .getImages() + .put(image.path, image); + } + + public void register(GemtextDocument document) { + registerDir(document.getUrl().getParentUrl()) + .getDocuments() + .put(document.getUrl(), document); + } + + public void remove(MemexNodeUrl url) { + var contents = fileSystemContentsByDir.get(url.getParentUrl()); + contents.removeDocument(url); + contents.removeImage(url); + } + + public List getAllDirectories() { + return new ArrayList<>(fileSystemContentsByDir.keySet()); + } + + public boolean isDirectory(MemexNodeUrl url) { + return fileSystemContentsByDir.containsKey(url); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRenderCreateFormModel.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRenderCreateFormModel.java new file mode 100644 index 00000000..eb59bcc1 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRenderCreateFormModel.java @@ -0,0 +1,32 @@ +package nu.marginalia.wmsa.memex.model.render; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import nu.marginalia.gemini.gmi.GemtextDocument; +import nu.marginalia.wmsa.memex.renderer.MemexHtmlRenderer; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Getter +public class MemexRenderCreateFormModel implements MemexRendererableDirect { + public final MemexNodeUrl url; + public final List docs; + + public String getFilename() { + return url.getFilename(); + } + + public List getDocs() { + return docs.stream().sorted(Comparator.comparing(GemtextDocument::getUrl).reversed()).collect(Collectors.toList()); + } + + @Override + public String render(MemexHtmlRenderer renderer) { + return renderer.renderModel(this); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRenderUpdateFormModel.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRenderUpdateFormModel.java new file mode 100644 index 00000000..9e32ed95 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRenderUpdateFormModel.java @@ -0,0 +1,19 @@ +package nu.marginalia.wmsa.memex.model.render; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import nu.marginalia.wmsa.memex.renderer.MemexHtmlRenderer; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; + +@AllArgsConstructor @Getter +public class MemexRenderUpdateFormModel implements MemexRendererableDirect { + public final MemexNodeUrl url; + public final String title; + public final String section; + public final String text; + + @Override + public String render(MemexHtmlRenderer renderer) { + return renderer.renderModel(this); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRenderUploadFormModel.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRenderUploadFormModel.java new file mode 100644 index 00000000..e098af52 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRenderUploadFormModel.java @@ -0,0 +1,32 @@ +package nu.marginalia.wmsa.memex.model.render; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import nu.marginalia.gemini.gmi.GemtextDocument; +import nu.marginalia.wmsa.memex.renderer.MemexHtmlRenderer; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Getter +public class MemexRenderUploadFormModel implements MemexRendererableDirect { + public final MemexNodeUrl url; + public final List docs; + + public String getFilename() { + return url.getFilename(); + } + + public List getDocs() { + return docs.stream().sorted(Comparator.comparing(GemtextDocument::getUrl).reversed()).collect(Collectors.toList()); + } + + @Override + public String render(MemexHtmlRenderer renderer) { + return renderer.renderModel(this); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererDeleteFormModel.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererDeleteFormModel.java new file mode 100644 index 00000000..84760fc2 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererDeleteFormModel.java @@ -0,0 +1,19 @@ +package nu.marginalia.wmsa.memex.model.render; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import nu.marginalia.wmsa.memex.renderer.MemexHtmlRenderer; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; + +@AllArgsConstructor @Getter +public class MemexRendererDeleteFormModel implements MemexRendererableDirect { + private final String doc; + private final MemexRendererImageModel image; + private final MemexNodeUrl url; + private final String type; + + @Override + public String render(MemexHtmlRenderer renderer) { + return renderer.renderModel(this); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererImageModel.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererImageModel.java new file mode 100644 index 00000000..d5534117 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererImageModel.java @@ -0,0 +1,36 @@ +package nu.marginalia.wmsa.memex.model.render; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.memex.model.MemexLink; +import nu.marginalia.wmsa.memex.model.MemexImage; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; + +import java.nio.file.Files; +import java.util.Base64; +import java.util.List; + +@AllArgsConstructor @Getter +public class MemexRendererImageModel { + public final MemexImage image; + public final List backlinks; + + public final String parent; + + public String getParent() { + if ("/".equals(parent) || parent.isBlank()) { + return null; + } + return parent; + } + + public MemexNodeUrl getPath() { + return image.path; + } + + @SneakyThrows + public String getData() { + return Base64.getEncoder().encodeToString(Files.readAllBytes(image.realPath)); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererIndexModel.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererIndexModel.java new file mode 100644 index 00000000..b26c119b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererIndexModel.java @@ -0,0 +1,69 @@ +package nu.marginalia.wmsa.memex.model.render; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import nu.marginalia.gemini.gmi.GemtextDocument; +import nu.marginalia.gemini.gmi.renderer.GemtextRendererFactory; +import nu.marginalia.wmsa.memex.model.*; + +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Getter +public class MemexRendererIndexModel { + public final MemexNodeUrl url; + public final List docs; + public final List images; + public final List directories; + public final List tasks; + public final List backlinks; + + public String getFilename() { + return url.getFilename(); + } + + public MemexNodeUrl getParent() { + return url.getParentUrl(); + } + + public List getDocs() { + return docs.stream() + .filter(doc -> !doc.isIndex()) + .sorted(Comparator.comparing(GemtextDocument::getUrl).reversed()) + .collect(Collectors.toList()); + } + + public final String getTitle() { + return Optional.ofNullable(getIndexDocument()).map(GemtextDocument::getTitle).orElse(url.toString()); + } + + public GemtextDocument getDocument(String filename) { + return docs.stream().filter(doc -> doc.getUrl().getFilename().endsWith(filename)).findFirst().orElse(null); + } + + private GemtextDocument getIndexDocument() { + return getDocument("index.gmi"); + } + + public String getIndexData() { + var indexDoc = getIndexDocument(); + if (indexDoc == null) { + return null; + } + var htmlRenderer = new GemtextRendererFactory("").htmlRendererReadOnly(); + return indexDoc.render(htmlRenderer); + } + + public boolean hasPragma(String value) { + var doc = getIndexDocument(); + if (doc == null) { + return false; + } + return doc.getPragmas().contains(value); + + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererRenameFormModel.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererRenameFormModel.java new file mode 100644 index 00000000..6dcb62c4 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererRenameFormModel.java @@ -0,0 +1,19 @@ +package nu.marginalia.wmsa.memex.model.render; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import nu.marginalia.wmsa.memex.renderer.MemexHtmlRenderer; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; + +@AllArgsConstructor @Getter +public class MemexRendererRenameFormModel implements MemexRendererableDirect { + private final String doc; + private final MemexRendererImageModel image; + private final MemexNodeUrl url; + private final String type; + + @Override + public String render(MemexHtmlRenderer renderer) { + return renderer.renderModel(this); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererTombstoneModel.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererTombstoneModel.java new file mode 100644 index 00000000..60907952 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererTombstoneModel.java @@ -0,0 +1,16 @@ +package nu.marginalia.wmsa.memex.model.render; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import nu.marginalia.wmsa.memex.model.MemexLink; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; + +import java.util.List; + +@AllArgsConstructor @Getter +public class MemexRendererTombstoneModel { + private final MemexNodeUrl url; + private final String message; + private final String redirect; + public final List backlinks; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererViewModel.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererViewModel.java new file mode 100644 index 00000000..f77e4bfd --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererViewModel.java @@ -0,0 +1,24 @@ +package nu.marginalia.wmsa.memex.model.render; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import nu.marginalia.gemini.gmi.GemtextDocument; +import nu.marginalia.wmsa.memex.model.MemexLink; + +import java.util.List; + +@AllArgsConstructor @Getter +public class MemexRendererViewModel { + public final GemtextDocument baseDoc; + public final String title; + public final List backlinks; + public final String doc; + public final String parent; + + public String getParent() { + if ("/".equals(parent) || parent.isBlank()) { + return null; + } + return parent; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererableDirect.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererableDirect.java new file mode 100644 index 00000000..571d5f56 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/model/render/MemexRendererableDirect.java @@ -0,0 +1,7 @@ +package nu.marginalia.wmsa.memex.model.render; + +import nu.marginalia.wmsa.memex.renderer.MemexHtmlRenderer; + +public interface MemexRendererableDirect { + String render(MemexHtmlRenderer renderer); +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/renderer/MemexGmiRenderer.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/renderer/MemexGmiRenderer.java new file mode 100644 index 00000000..e613d746 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/renderer/MemexGmiRenderer.java @@ -0,0 +1,228 @@ +package nu.marginalia.wmsa.memex.renderer; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import nu.marginalia.gemini.gmi.GemtextDocument; +import nu.marginalia.gemini.gmi.renderer.GemtextRendererFactory; +import nu.marginalia.wmsa.memex.MemexData; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import nu.marginalia.wmsa.memex.system.MemexFileWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Comparator; + +public class MemexGmiRenderer { + private final MemexFileWriter renderedResources; + private final MemexData data; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Inject + public MemexGmiRenderer(@Named("gmi") MemexFileWriter renderedResources, + MemexData data) + { + this.renderedResources = renderedResources; + this.data = data; + } + + + public void render(MemexNodeUrl url) { + if (data.getDocument(url) != null) { + renderDocument(url); + } + else if(data.getImage(url) != null) { + renderImage(url); + } + else if(data.isDirectory(url)) { + renderIndex(url); + } + else if(data.hasTombstone(url)) { + renderTombstone(url); + } + else { + logger.warn("I don't know how to render {}", url); + } + } + + private void renderDocument(MemexNodeUrl url) { + if ("index.gmi".equals(url.getFilename())) { + return; + } + + var doc = data.getDocument(url); + var renderer = new GemtextRendererFactory().gemtextRendererPublic(); + + try { + renderedResources.write(url, (w) -> { + doc.render(renderer, w); + w.println(); + backlinks(w, url); + w.println("# Navigation\n"); + w.printf("=> %s Back to Index\n", url.getParentUrl()); + w.println("\nReach me at kontakt@marginalia.nu"); + }); + } catch (IOException e) { + logger.error("Failed to render document " + url, e); + } + + } + + private void renderImage(MemexNodeUrl url) { + try { + renderedResources.write(url, data.getImage(url).realPath); + } catch (IOException e) { + logger.error("Failed to image document " + url, e); + } + } + + private void renderIndex(MemexNodeUrl url) { + + var renderer = new GemtextRendererFactory().gemtextRendererPublic(); + + var doc = data.getDocument(url.child("index.gmi")); + boolean feed = doc != null && doc.getPragmas().contains("FEED"); + boolean listing = doc != null && doc.getPragmas().contains("LISTING"); + + try { + renderedResources.write(url.child("index.gmi"), (w) -> { + + if (null != doc) doc.render(renderer, w); + else w.printf("# %s\n", url); + + + if (listing) { + documentsInUrlListing(url, w); + } + + if (feed) { + w.printf("\n=> %s/feed.gmi Clean gemsub feed\n", url); + w.printf("=> %s/feed.xml Atom feed\n", url); + } + + w.println("\n# Directory Contents\n"); + directoriesInUrl(url, w); + if (!listing) { + documentsInUrl(url, w); + } + imagesInUrl(url, w); + backlinks(w, url, url.child("index.gmi")); + w.println("\nReach me at kontakt@marginalia.nu"); + + }); + + if (feed) { + renderedResources.write(url.child("feed.gmi"), (w) -> { + w.printf("# marginalia.nu%s\n", url); + w.println(); + var docs = data.getDocumentsByPath(url); + docs.sort(Comparator.comparing(GemtextDocument::getUrl).reversed()); + for (var d : docs) { + if (d.getUrl().getFilename().equals("index.gmi")) { + continue; + } + if (d.getPragmas().contains("DRAFT")) { + continue; + } + w.printf("=> gemini://marginalia.nu%s\t%s %s\n", d.getUrl(), d.getDate(), d.getTitle().replaceAll("\\[[^\\]]+\\]", "")); + } + }); + } + } catch (IOException e) { + logger.error("Failed to render document " + url, e); + } + } + + private void backlinks(PrintWriter w, MemexNodeUrl... urls) { + var bls = data.getBacklinks(urls); + if (!bls.isEmpty()) { + w.println("\n# Backlinks\n"); + for (var bl : bls) { + w.printf("=> %s\n", bl.src); + } + w.println(); + } + } + + private void documentsInUrl(MemexNodeUrl url, PrintWriter w) { + var docs = data.getDocumentsByPath(url); + if (docs.size() > (data.getDocument(url.child("index.gmi")) == null ? 0 : 1)) { + for (var d : docs) { + if (d.getUrl().getFilename().equals("index.gmi")) { + continue; + } + w.printf("=> %s\t\uD83D\uDDD2 ️️️%s\n", d.getUrl(), d.getTitle()); + } + w.println(); + } + } + + private void documentsInUrlListing(MemexNodeUrl url, PrintWriter w) { + var docs = data.getDocumentsByPath(url); + + docs.sort(Comparator.comparing(GemtextDocument::getUrl).reversed()); + + if (!docs.isEmpty()) { + for (var d : docs) { + if (d.getUrl().getFilename().equals("index.gmi")) { + continue; + } + w.printf("=> %s\t%s\n", d.getUrl(), d.getTitle()); + } + w.println(); + } + } + + private void imagesInUrl(MemexNodeUrl url, PrintWriter w) { + var images = data.getImagesByPath(url); + if (!images.isEmpty()) { + for (var i : images) { + w.printf("=> %s \uD83D\uDDBC️ %s\n", i.path, i.path.getFilename()); + } + w.println(); + } + } + + private void directoriesInUrl(MemexNodeUrl url, PrintWriter w) { + var dirs = data.getSubdirsByPath(url); + final boolean isRoot = url.getParentUrl() == null; + if (isRoot && dirs.isEmpty()) { + return; + } + + if (!isRoot) { + w.println("=> ../ ⬆ ../ "); + + } + if (dirs.isEmpty()) { + w.println(); + } + for (var d : dirs) { + w.printf("=> %s \uD83D\uDDC2️ %s/\n", d, d.getFilename()); + } + } + + + private void renderTombstone(MemexNodeUrl url) { + String message = data.getTombstones().flatMap(tombstones -> tombstones.getLinkData(url)).orElse(null); + String redir = data.getRedirects().flatMap(redirects -> redirects.getLinkData(url)).orElse(null); + + try { + renderedResources.write(url, w -> { + w.printf("# %s is gone\n\n", url); + if (message != null) { + w.printf("%s\n", message); + } + if (redir != null) { + w.println("Please see"); + w.printf("=> %s\n", redir); + } + backlinks(w, url); + w.println("\nReach me at kontakt@marginalia.nu"); + }); + } catch (IOException e) { + logger.error("Failed to render tombstone " + url, e); + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/renderer/MemexHtmlRenderer.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/renderer/MemexHtmlRenderer.java new file mode 100644 index 00000000..a39e5763 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/renderer/MemexHtmlRenderer.java @@ -0,0 +1,197 @@ +package nu.marginalia.wmsa.memex.renderer; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import lombok.SneakyThrows; +import nu.marginalia.gemini.gmi.renderer.GemtextRendererFactory; +import nu.marginalia.wmsa.memex.MemexData; +import nu.marginalia.wmsa.memex.model.*; +import nu.marginalia.wmsa.memex.model.render.*; +import nu.marginalia.wmsa.memex.system.MemexFileWriter; +import nu.marginalia.wmsa.renderer.mustache.MustacheRenderer; +import nu.marginalia.wmsa.renderer.mustache.RendererFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoField; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class MemexHtmlRenderer { + + private final MemexFileWriter htmlRenderedResources; + private final MemexFileWriter gmiRenderedResources; + + private final MemexData data; + + private final MustacheRenderer viewRenderer; + private final MustacheRenderer indexRenderer; + private final MustacheRenderer indexFeedRenderer; + private final MustacheRenderer imageRenderer; + private final MustacheRenderer tombstoneRenderer; + + private final MustacheRenderer updateFormRenderer; + private final MustacheRenderer uploadFormRenderer; + private final MustacheRenderer createFormRenderer; + private final MustacheRenderer deleteFormRenderer; + private final MustacheRenderer renameFormRenderer; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Inject + public MemexHtmlRenderer( + @Named("html") MemexFileWriter htmlRenderedResources, + @Named("gmi") MemexFileWriter gmiRenderedResources, + MemexData data) throws IOException { + this.htmlRenderedResources = htmlRenderedResources; + this.gmiRenderedResources = gmiRenderedResources; + this.data = data; + + final var rendererFactory = new RendererFactory(); + + viewRenderer = rendererFactory.renderer("memex/memex-view"); + indexRenderer = rendererFactory.renderer("memex/memex-index"); + indexFeedRenderer = rendererFactory.renderer("memex/memex-index-feed"); + imageRenderer = rendererFactory.renderer("memex/memex-image"); + + tombstoneRenderer = rendererFactory.renderer("memex/memex-tombstone"); + + updateFormRenderer = rendererFactory.renderer("memex/memex-update-form"); + uploadFormRenderer = rendererFactory.renderer("memex/memex-upload-form"); + deleteFormRenderer = rendererFactory.renderer("memex/memex-delete-form"); + renameFormRenderer = rendererFactory.renderer("memex/memex-rename-form"); + createFormRenderer = rendererFactory.renderer("memex/memex-create-form"); + + } + + public void render(MemexNodeUrl url) { + if (data.getDocument(url) != null) { + renderDocument(url); + } + else if(data.getImage(url) != null) { + renderImage(url); + } + else if(data.isDirectory(url)) { + renderIndex(url); + } + else if(data.hasTombstone(url)) { + renderTombstone(url); + } + else { + logger.warn("I don't know how to render {}", url); + } + } + + public void renderDocument(MemexNodeUrl url) { + var doc = Objects.requireNonNull(data.getDocument(url), "could not get document " + url); + var htmlRenderer = new GemtextRendererFactory("", url.toString()).htmlRendererEditable(); + var model = new MemexRendererViewModel(doc, + doc.getTitle(), + data.getBacklinks(url), + doc.render(htmlRenderer), + url.getParentStr() + ); + + try { + htmlRenderedResources.write(url, viewRenderer.render(model, Map.of("urlRoot", ""))); + } catch (IOException e) { + logger.error("Failed to render document " + url, e); + } + + } + + public void renderIndex(MemexNodeUrl url) { + + var docs = data.getDocumentsByPath(url); + var images = data.getImagesByPath(url); + var dirs = data.getSubdirsByPath(url); + + var tasks = docs.stream().flatMap(doc -> doc.getOpenTopTasks().entrySet() + .stream() + .sorted(Map.Entry.comparingByKey()) + .map(entry -> new MemexIndexTask(entry.getValue().getLeft(), + entry.getKey().toString(), + doc.getUrl().toString(), + entry.getValue().getRight().style)) + ).collect(Collectors.toList()); + + List backlinks = data.getBacklinks(url, url.child("index.gmi")); + var model = new MemexRendererIndexModel(url, docs, images, new ArrayList<>(dirs), tasks, backlinks); + + try { + htmlRenderedResources.write(url.child("index.html"), indexRenderer.render(model, Map.of("urlRoot", ""))); + if (model.hasPragma("FEED")) { + String nowStr = OffsetDateTime.now().with(ChronoField.MILLI_OF_SECOND, 0).format(DateTimeFormatter.ISO_DATE_TIME); + htmlRenderedResources.write(url.child("feed.xml"), indexFeedRenderer.render(model, + Map.of("domain", "https://memex.marginalia.nu", "now", nowStr))); + gmiRenderedResources.write(url.child("feed.xml"), indexFeedRenderer.render(model, + Map.of("domain", "gemini://marginalia.nu", "now", nowStr))); + } + } catch (IOException e) { + logger.error("Failed to render index model " + url, e); + } + } + + public void renderImage(MemexNodeUrl url) { + var img = data.getImage(url); + var backlinks = data.getBacklinks(img.path); + var parent = img.path.getParentStr(); + var model = new MemexRendererImageModel(img, backlinks, parent); + + try { + htmlRenderedResources.write(img.path, imageRenderer.render(model, Map.of("urlRoot", ""))); + } catch (IOException e) { + logger.error("Failed to render image model " + img.path, e); + } + } + + public void renderTombstone(MemexNodeUrl url) { + + String message = data.getTombstones().flatMap(tombstones -> tombstones.getLinkData(url)).orElse(null); + String redir = data.getRedirects().flatMap(redirects -> redirects.getLinkData(url)).orElse(null); + + var model = new MemexRendererTombstoneModel(url, + message, + redir, + data.getBacklinks(url)); + + try { + htmlRenderedResources.write(url, tombstoneRenderer.render(model, Map.of("urlRoot", ""))); + } catch (IOException e) { + logger.error("Failed to render tombstone model " + url, e); + } + } + + @SneakyThrows + public String renderModel(MemexRendererDeleteFormModel model) { + return deleteFormRenderer.render(model, Map.of("urlRoot", "")); + } + + + @SneakyThrows + public String renderModel(MemexRendererRenameFormModel model) { + return renameFormRenderer.render(model, Map.of("urlRoot", "")); + } + + @SneakyThrows + public String renderModel(MemexRenderCreateFormModel model) { + return createFormRenderer.render(model, Map.of("urlRoot", "")); + } + + @SneakyThrows + public String renderModel(MemexRenderUploadFormModel model) { + return uploadFormRenderer.render(model, Map.of("urlRoot", "")); + } + + @SneakyThrows + public String renderModel(MemexRenderUpdateFormModel model) { + return updateFormRenderer.render(model, Map.of("urlRoot", "")); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/renderer/MemexRendererers.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/renderer/MemexRendererers.java new file mode 100644 index 00000000..b033eabf --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/renderer/MemexRendererers.java @@ -0,0 +1,22 @@ +package nu.marginalia.wmsa.memex.renderer; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; + +@Singleton +public class MemexRendererers { + private final MemexGmiRenderer gmiRenderer; + private final MemexHtmlRenderer htmlRenderer; + + @Inject + public MemexRendererers(MemexGmiRenderer gmiRenderer, MemexHtmlRenderer htmlRenderer) { + this.gmiRenderer = gmiRenderer; + this.htmlRenderer = htmlRenderer; + } + + public void render(MemexNodeUrl url) { + gmiRenderer.render(url); + htmlRenderer.render(url); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexFileSystemModifiedTimes.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexFileSystemModifiedTimes.java new file mode 100644 index 00000000..e0f3428e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexFileSystemModifiedTimes.java @@ -0,0 +1,23 @@ +package nu.marginalia.wmsa.memex.system; + +import com.google.inject.Singleton; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +@Singleton +public class MemexFileSystemModifiedTimes { + + private final Map modifiedTimes = new ConcurrentHashMap<>(); + + public boolean isFreshUpdate(Path node) throws IOException { + long mtime = Files.getLastModifiedTime(node).toMillis(); + + return !Objects.equals(modifiedTimes.put(node, mtime), mtime); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexFileSystemMonitor.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexFileSystemMonitor.java new file mode 100644 index 00000000..bbcc2b4b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexFileSystemMonitor.java @@ -0,0 +1,115 @@ +package nu.marginalia.wmsa.memex.system; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +import static java.nio.file.StandardWatchEventKinds.*; + +public class MemexFileSystemMonitor { + private final WatchService watchService; + private final Set updatedUrls = new HashSet<>(); + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final Map roots = new ConcurrentHashMap<>(); + private final Path memexRoot; + + @Inject + public MemexFileSystemMonitor(@Named("memex-root") Path monitorPath) throws IOException { + this.memexRoot = monitorPath; + this.watchService = FileSystems.getDefault().newWatchService(); + + registerWatcher(monitorPath); + + try (var files = Files.walk(monitorPath)) { + files.filter(Files::isDirectory).forEach(this::registerWatcher); + } + + var monitorThread = new Thread(this::monitorWatch, getClass().getSimpleName()); + monitorThread.setDaemon(true); + monitorThread.start(); + } + + private void registerWatcher(Path path) { + if (path.toString().contains(".git")) { + return; + } + + try { + logger.info("Watching " + path); + var key = path.register(watchService, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_DELETE); + roots.put(key, path); + + } catch (IOException e) { + logger.error("Failed to register directory watcher on " + path, e); + } + } + + public List getUpdatedUrls() { + synchronized (updatedUrls) { + if (updatedUrls.isEmpty()) { + return Collections.emptyList(); + } + var ret = new ArrayList<>(updatedUrls); + updatedUrls.clear(); + return ret; + } + } + + + @SneakyThrows + private void monitorWatch() { + for (;;) { + var key = watchService.take(); + + for (var evt : key.pollEvents()) { + var kind = evt.kind(); + + if (kind == OVERFLOW) { + var root = roots.get(key); + + try (var files = Files.list(root)) { + files.forEach(file -> + updatedUrls.add(MemexNodeUrl.ofRelativePath(memexRoot, file)) + ); + } + + continue; + } + + WatchEvent ev = (WatchEvent)evt; + Path root = roots.get(key); + Path filename = ev.context(); + Path absPath = root.resolve(filename); + + + if (kind == ENTRY_CREATE && Files.isDirectory(absPath)) { + registerWatcher(absPath); + } + + MemexNodeUrl url = MemexNodeUrl.ofRelativePath(memexRoot, absPath); + synchronized (updatedUrls) { + updatedUrls.add(url); + } + + } + + boolean valid = key.reset(); + if (!valid) { + logger.info("Deregistering key for " + roots.get(key)); + roots.remove(key); + } + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexFileWriter.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexFileWriter.java new file mode 100644 index 00000000..577da3d9 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexFileWriter.java @@ -0,0 +1,120 @@ +package nu.marginalia.wmsa.memex.system; + +import nu.marginalia.util.FileSizeUtil; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Set; + +public class MemexFileWriter { + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Path renderedResourcesRoot; + + + private final Set filePermission = Set.of(PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.GROUP_READ, + PosixFilePermission.OTHERS_READ); + + private final Set dirPermission = Set.of(PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.GROUP_READ, + PosixFilePermission.GROUP_EXECUTE, + PosixFilePermission.OTHERS_READ, + PosixFilePermission.OTHERS_EXECUTE); + + public MemexFileWriter(Path renderedResourcesRoot) { + this.renderedResourcesRoot = renderedResourcesRoot; + } + + public boolean exists(MemexNodeUrl url) { + return Files.exists(getPath(url)); + } + + public void write(MemexNodeUrl url, String contents) throws IOException { + logger.info("write({},{})", url, FileSizeUtil.readableSize(contents.length())); + var destPath = getPath(url); + var tempFile = Files.createTempFile(renderedResourcesRoot, url.getFilename(), ".tmp"); + ensureDirectoryExists(destPath.getParent()); + + Files.createDirectories(destPath.getParent()); + Files.writeString(tempFile, contents); + Files.move(tempFile, destPath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + setFilePermissions(destPath); + } + + private void ensureDirectoryExists(Path dir) throws IOException { + Files.createDirectories(dir); + setDirPermissions(dir); + } + + private void setDirPermissions(Path dir) throws IOException { + for (var rel = renderedResourcesRoot.relativize(dir); rel != null; rel = rel.getParent()) { + Files.setPosixFilePermissions(renderedResourcesRoot.resolve(rel), dirPermission); + } + } + + public void write(MemexNodeUrl url, byte[] contents) throws IOException { + logger.info("write({}, {})", url, FileSizeUtil.readableSize(contents.length)); + + var destPath = getPath(url); + var tempFile = Files.createTempFile(renderedResourcesRoot, url.getFilename(), ".tmp"); + ensureDirectoryExists(destPath.getParent()); + Files.write(tempFile, contents); + Files.move(tempFile, destPath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + setFilePermissions(destPath); + } + + public void write(MemexNodeUrl url, Path realPath) throws IOException { + logger.info("copy({} from {})", url, realPath); + + var destPath = getPath(url); + var tempFile = Files.createTempFile(renderedResourcesRoot, url.getFilename(), ".tmp"); + ensureDirectoryExists(destPath.getParent()); + Files.copy(realPath, tempFile, StandardCopyOption.REPLACE_EXISTING); + Files.move(tempFile, destPath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + setFilePermissions(destPath); + } + + public void write(MemexNodeUrl url, WriteOperation wo) throws IOException { + logger.info("write({}, streamed)", url); + + var destPath = getPath(url); + var tempFile = Files.createTempFile(renderedResourcesRoot, url.getFilename(), ".tmp"); + ensureDirectoryExists(destPath.getParent()); + + try (var os = new PrintWriter(Files.newOutputStream(tempFile))) { + wo.write(os); + } + + Files.move(tempFile, destPath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + setFilePermissions(destPath); + } + + + private void setFilePermissions(Path destPath) throws IOException { + Files.setPosixFilePermissions(destPath, filePermission); + } + + private Path getPath(MemexNodeUrl url) { + final Path path = Path.of(renderedResourcesRoot + url.toString()).normalize(); + + if (!path.startsWith(renderedResourcesRoot)) { + throw new IllegalStateException("URL " + url + " resulted in a path outside of root " + renderedResourcesRoot); + } + + return path; + } + + public interface WriteOperation { + void write(PrintWriter w) throws IOException; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexGitRepo.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexGitRepo.java new file mode 100644 index 00000000..05ca6603 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexGitRepo.java @@ -0,0 +1,135 @@ +package nu.marginalia.wmsa.memex.system; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.transport.*; +import org.eclipse.jgit.util.FS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Path; + +@Singleton +public class MemexGitRepo { + + private final Git git; + private final Logger logger = LoggerFactory.getLogger(MemexGitRepo.class); + + @Inject + public MemexGitRepo(@Named("memex-root") Path root) throws IOException { + + FileRepositoryBuilder repositoryBuilder = new FileRepositoryBuilder(); + + SshSessionFactory.setInstance(new JschConfigSessionFactory() { + @Override + protected JSch createDefaultJSch(FS fs) throws JSchException { + JSch defaultJSch = super.createDefaultJSch(fs); + defaultJSch.addIdentity("/var/lib/wmsa/.ssh/id_rsa"); + return defaultJSch; + } + }); + + Repository repository = repositoryBuilder.setGitDir(root.resolve(".git").toFile()) + .readEnvironment() + .findGitDir() + .setMustExist(true) + .build(); + + git = new Git(repository); + + pull(); + } + + public void pull() { + try { + git.pull().call(); + } + catch (GitAPIException ex) { + logger.error("Git operation failed", ex); + } + } + + public void remove(MemexNodeUrl url) { + try { + git.rm() + .addFilepattern(filePattern(url)) + .call(); + + commit("Removing " + url); + push(); + } + catch (GitAPIException ex) { + logger.error("Git operation failed", ex); + } + } + + public void add(MemexNodeUrl url) { + try { + git.add() + .addFilepattern(filePattern(url)) + .call(); + + commit("Adding " + url); + push(); + + + } + catch (GitAPIException ex) { + logger.error("Git operation failed", ex); + } + } + public void update(MemexNodeUrl url) { + try { + git.add() + .setUpdate(true) + .addFilepattern(filePattern(url)) + .call(); + + commit("Update " + url); + push(); + + + } + catch (GitAPIException ex) { + logger.error("Git operation failed", ex); + } + } + + + public void rename(MemexNodeUrl src, MemexNodeUrl dst) { + try { + git.rm().addFilepattern(filePattern(src)).call(); + git.add().addFilepattern(filePattern(dst)).call(); + commit("Renaming " + src + " into " + dst); + push(); + } + catch (GitAPIException ex) { + logger.error("Git operation failed", ex); + } + } + + private void push() throws GitAPIException { + git.push().call(); + } + + private void commit(String message) throws GitAPIException { + git.commit() + .setCommitter("marginalia", "system@marginalia.nu") + .setMessage("Changes from web gui: " + message) + .call(); + } + + private String filePattern(MemexNodeUrl url) { + return url.asRelativePath().toString().replaceAll("^/+", ""); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexSourceFileSystem.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexSourceFileSystem.java new file mode 100644 index 00000000..c72e2383 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/memex/system/MemexSourceFileSystem.java @@ -0,0 +1,83 @@ +package nu.marginalia.wmsa.memex.system; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.*; + +@Singleton +public class MemexSourceFileSystem { + + private final Path root; + private final MemexGitRepo gitRepo; + + private static final Logger logger = LoggerFactory.getLogger(MemexSourceFileSystem.class); + + @Inject + public MemexSourceFileSystem(@Named("memex-root") Path root, + MemexGitRepo gitRepo) { + this.root = root; + this.gitRepo = gitRepo; + } + + public void pullChanges() { + gitRepo.pull(); + } + + public void replaceFile(MemexNodeUrl url, String text) throws IOException { + var path = url.asAbsolutePath(root); + Files.writeString(path, text, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + + gitRepo.update(url); + } + + public void createFile(MemexNodeUrl url, String text) throws IOException { + var path = url.asAbsolutePath(root); + logger.info("Writing {} ({}b)", path, text.length()); + + Files.writeString(path, text, StandardOpenOption.CREATE_NEW); + + gitRepo.add(url); + } + + public void createFile(MemexNodeUrl url, byte[] bytes) throws IOException { + var path = url.asAbsolutePath(root); + logger.info("Writing {} ({}b)", path, bytes.length); + + Files.write(path, bytes, StandardOpenOption.CREATE_NEW); + + gitRepo.add(url); + } + + public void delete(MemexNodeUrl url) throws IOException { + var path = url.asAbsolutePath(root); + + logger.info("Delete {}", path); + Files.delete(path); + + gitRepo.remove(url); + } + + public void renameFile(MemexNodeUrl src, MemexNodeUrl dst) throws IOException { + var srcPath = src.asAbsolutePath(root); + var dstPath = dst.asAbsolutePath(root); + + if (!Files.exists(srcPath) || Files.exists(dstPath)) { + throw new IOException("Could not rename " + src + " into " + dst); + } + + Files.move(srcPath, dstPath, StandardCopyOption.ATOMIC_MOVE); + gitRepo.rename(src, dst); + } + + public byte[] getRaw(MemexNodeUrl url) throws IOException { + logger.info("Getting raw file contents of {}", url); + + return Files.readAllBytes(url.asAbsolutePath(root)); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/PodcastFetcher.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/PodcastFetcher.java new file mode 100644 index 00000000..58fad4c9 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/PodcastFetcher.java @@ -0,0 +1,112 @@ +package nu.marginalia.wmsa.podcasts; + +import com.google.common.escape.Escaper; +import com.google.common.net.PercentEscaper; +import lombok.Getter; +import nu.marginalia.wmsa.podcasts.model.*; +import org.jetbrains.annotations.NotNull; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Element; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URL; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Getter +public class PodcastFetcher { + + private final List allEpisodes = new ArrayList<>(); + private final List allPodcasts = new ArrayList<>(); + private final Escaper urlEscaper = new PercentEscaper("", true); + + private final static Logger logger = LoggerFactory.getLogger(PodcastFetcher.class); + + private final DateTimeFormatter readableIsoDate = + (new DateTimeFormatterBuilder()).parseCaseInsensitive().append(DateTimeFormatter.ISO_LOCAL_DATE).appendLiteral(' ').append( + DateTimeFormatter.ISO_LOCAL_TIME).toFormatter(); + + public Optional fetchPodcast(String name, String url) { + try { + logger.info("Fetching podcast {} : {}", name, url); + + var doc = Jsoup.parse(new URL(url), 10_000); + + String title = doc.selectFirst("channel > title").text(); + String description = doc.selectFirst("channel > description").text(); + String link = doc.selectFirst("channel > link").text(); + + var podcast = new Podcast(new PodcastMetadata(title, description, name, link)); + doc.getElementsByTag("item").forEach(item -> { + try { + PodcastEpisode episode = fetchEpisode(name, title, item); + podcast.episodes.add(episode); + allEpisodes.add(episode); + } + catch (Exception ex) { + logger.error("Failed to fetch podcast episode", ex); + } + }); + + allPodcasts.add(podcast); + return Optional.of(podcast); + } catch (IOException e) { + logger.error("Failed to fetch podcast", e); + return Optional.empty(); + } + + } + + @NotNull + private PodcastEpisode fetchEpisode(String name, String title, org.jsoup.nodes.Element item) { + String epTitle = item.getElementsByTag("title").text(); + String epGuid = name+":"+escapeUrlString(item.getElementsByTag("guid").text()); + String epDescription = item.getElementsByTag("description").text(); + String epPubDate = getPubDate(item); + String epUrl = item.getElementsByTag("enclosure").attr("url"); + + return new PodcastEpisode(name, title, epGuid, epTitle, epDescription, epPubDate, epUrl); + } + + @NotNull + private String getPubDate(Element item) { + try { + return ZonedDateTime.parse(item.getElementsByTag("pubDate").text(), + DateTimeFormatter.RFC_1123_DATE_TIME) + .format(readableIsoDate); + } + catch (Exception ex) { + logger.error("Failed to parse date", ex); + return item.getElementsByTag("pubDate").text(); + } + } + + private String escapeUrlString(String s) { + return urlEscaper.escape(s).replace("%", "_"); + } + + public PodcastNewEpisodes getNewEpisodes() { + return new PodcastNewEpisodes(allEpisodes + .stream() + .sorted(Comparator.comparing(PodcastEpisode::getDateUploaded).reversed()).limit(10) + .collect(Collectors.toList())); + } + + public PodcastListing getListing() { + final var metadatas = allPodcasts.stream() + .map(Podcast::getMetadata) + .sorted(Comparator.comparing(PodcastMetadata::getTitle)) + .collect(Collectors.toList()); + + return + new PodcastListing(metadatas); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/PodcastScraperMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/PodcastScraperMain.java new file mode 100644 index 00000000..f3850679 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/PodcastScraperMain.java @@ -0,0 +1,31 @@ +package nu.marginalia.wmsa.podcasts; + +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import nu.marginalia.wmsa.configuration.MainClass; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.module.ConfigurationModule; +import nu.marginalia.wmsa.configuration.server.Initialization; + +import java.io.IOException; + +public class PodcastScraperMain extends MainClass { + + private final PodcastScraperService service; + + @Inject + public PodcastScraperMain(PodcastScraperService service) throws IOException { + this.service = service; + } + + public static void main(String... args) { + init(ServiceDescriptor.PODCST_SCRAPER, args); + + Injector injector = Guice.createInjector( + new ConfigurationModule()); + injector.getInstance(PodcastScraperMain.class); + injector.getInstance(Initialization.class).setReady(); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/PodcastScraperService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/PodcastScraperService.java new file mode 100644 index 00000000..a36ec3ce --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/PodcastScraperService.java @@ -0,0 +1,78 @@ +package nu.marginalia.wmsa.podcasts; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import io.reactivex.rxjava3.schedulers.Schedulers; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.configuration.server.MetricsServer; +import nu.marginalia.wmsa.configuration.server.Service; +import nu.marginalia.wmsa.podcasts.model.Podcast; +import nu.marginalia.wmsa.podcasts.model.PodcastEpisode; +import nu.marginalia.wmsa.renderer.client.RendererClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Spark; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class PodcastScraperService extends Service { + + private final Map podcastUrls = Map.of( + "SBS", "https://feeds.simplecast.com/Fxu1mrhe", + "hopwag", "https://feed.podbean.com/hopwag/feed.xml", + "philosophizethis", "https://philosophizethis.libsyn.com/rss", + "PEL", "https://partiallyexaminedlife.libsyn.com/rss", + "IOT", "https://podcasts.files.bbci.co.uk/b006qykl.rss", + "SaturaLanx", "https://anchor.fm/s/2c536214/podcast/rss", + "ControversiesInChurchHistory", "https://anchor.fm/s/9b43760/podcast/rss", + "readmeapoem", "https://rss.acast.com/readmeapoem", + "HoL", "https://feeds.megaphone.fm/history-of-literature", + "Revolutions", "https://revolutionspodcast.libsyn.com/rss" + ); + + private final RendererClient rendererClient; + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Initialization initialization; + + @Inject + public PodcastScraperService(@Named("service-host") String ip, + @Named("service-port") Integer port, + RendererClient rendererClient, + Initialization initialization, + MetricsServer metricsServer) { + super(ip, port, initialization, metricsServer); + this.rendererClient = rendererClient; + this.initialization = initialization; + + Spark.awaitInitialization(); + + Schedulers.io().schedulePeriodicallyDirect(this::fetchPods, 0, 1, TimeUnit.HOURS); + } + + private void fetchPods() { + try { + PodcastFetcher fetcher = new PodcastFetcher(); + + podcastUrls.forEach(fetcher::fetchPodcast); + + rendererClient.render(Context.internal("podcast"), fetcher.getNewEpisodes()).blockingSubscribe(); + rendererClient.render(Context.internal("podcast"), fetcher.getListing()).blockingSubscribe(); + + for (Podcast podcast : fetcher.getAllPodcasts()) { + rendererClient.render(Context.internal("podcast"), podcast).blockingSubscribe(); + } + for (PodcastEpisode episode : fetcher.getAllEpisodes()) { + rendererClient.render(Context.internal("podcast"), episode).blockingSubscribe(); + } + } + catch (RuntimeException ex) { + logger.error("Uncaught exception", ex); + } + } + + public void start() { + logger.info("Started"); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/Podcast.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/Podcast.java new file mode 100644 index 00000000..7c3ae0ae --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/Podcast.java @@ -0,0 +1,16 @@ +package nu.marginalia.wmsa.podcasts.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.util.ArrayList; +import java.util.List; + +@AllArgsConstructor @Getter @Setter @ToString +public class Podcast { + public final PodcastMetadata metadata; + + public final List episodes = new ArrayList<>(); +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/PodcastEpisode.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/PodcastEpisode.java new file mode 100644 index 00000000..255de6a0 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/PodcastEpisode.java @@ -0,0 +1,16 @@ +package nu.marginalia.wmsa.podcasts.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@AllArgsConstructor @Getter @ToString +public class PodcastEpisode { + public final String podcastId; + public final String podcastName; + public final String guid; + public final String title; + public final String description; + public final String dateUploaded; + public final String mp3url; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/PodcastListing.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/PodcastListing.java new file mode 100644 index 00000000..3a3e917a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/PodcastListing.java @@ -0,0 +1,16 @@ +package nu.marginalia.wmsa.podcasts.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@AllArgsConstructor +@Getter +@Setter +@ToString +public class PodcastListing { + public final List podcasts; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/PodcastMetadata.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/PodcastMetadata.java new file mode 100644 index 00000000..8f6aad6c --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/PodcastMetadata.java @@ -0,0 +1,17 @@ +package nu.marginalia.wmsa.podcasts.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@AllArgsConstructor +@Getter +@Setter +@ToString +public class PodcastMetadata { + public final String title; + public final String description; + public final String id; + public final String extLink; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/PodcastNewEpisodes.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/PodcastNewEpisodes.java new file mode 100644 index 00000000..c7562e60 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/podcasts/model/PodcastNewEpisodes.java @@ -0,0 +1,11 @@ +package nu.marginalia.wmsa.podcasts.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@AllArgsConstructor @Getter +public class PodcastNewEpisodes { + public final List episodes; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/PodcastRendererService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/PodcastRendererService.java new file mode 100644 index 00000000..b22386a1 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/PodcastRendererService.java @@ -0,0 +1,130 @@ +package nu.marginalia.wmsa.renderer; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.inject.Inject; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.podcasts.model.Podcast; +import nu.marginalia.wmsa.podcasts.model.PodcastEpisode; +import nu.marginalia.wmsa.podcasts.model.PodcastListing; +import nu.marginalia.wmsa.podcasts.model.PodcastNewEpisodes; +import nu.marginalia.wmsa.renderer.mustache.MustacheRenderer; +import nu.marginalia.wmsa.renderer.mustache.RendererFactory; +import nu.marginalia.wmsa.resource_store.ResourceStoreClient; +import nu.marginalia.wmsa.resource_store.model.RenderedResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Request; +import spark.Response; +import spark.Spark; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; + +public class PodcastRendererService { + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Gson gson = new GsonBuilder().create(); + + private final RendererFactory rendererFactory = new RendererFactory(); + + private final MustacheRenderer newsRenderer; + private final MustacheRenderer episodeRenderer; + private final MustacheRenderer listingRenderer; + private final MustacheRenderer podcastRenderer; + + private ResourceStoreClient resourceStoreClient; + + + @Inject @SneakyThrows + public PodcastRendererService(ResourceStoreClient resourceStoreClient) { + this.resourceStoreClient = resourceStoreClient; + newsRenderer = rendererFactory.renderer( "podcast/new"); + episodeRenderer = rendererFactory.renderer( "podcast/episode"); + listingRenderer = rendererFactory.renderer( "podcast/listing"); + podcastRenderer = rendererFactory.renderer( "podcast/podcast"); + } + + public void start() { + Spark.post("/render/podcast", this::renderPodcast); + Spark.post("/render/podcast/episode", this::renderPodcastEpisode); + Spark.post("/render/podcast/new", this::renderPodcastNew); + Spark.post("/render/podcast/listing", this::renderPodcastListing); + } + + private Object renderPodcastListing(Request request, Response response) throws IOException { + var requestText = request.body(); + var req = gson.fromJson(requestText, PodcastListing.class); + + logger.info("renderPodcastListing()"); + + var resource = new RenderedResource("list.html", + getRetentionTime(), + listingRenderer.render(req)); + + storeResource(request, resource); + + return ""; + } + + + private Object renderPodcast(Request request, Response response) throws IOException { + var requestText = request.body(); + var req = gson.fromJson(requestText, Podcast.class); + + logger.info("renderPodcast({})", req.metadata.id); + + var resource = new RenderedResource(req.metadata.id+".html", + getRetentionTime(), + podcastRenderer.render(req)); + + storeResource(request, resource); + + return ""; + } + + private Object renderPodcastEpisode(Request request, Response response) throws IOException { + var requestText = request.body(); + var req = gson.fromJson(requestText, PodcastEpisode.class); + Context.fromRequest(request); + + logger.info("renderPodcastEpisode({}/{})", req.podcastName, req.guid); + var resource = new RenderedResource(req.guid+".html", + getRetentionTime(), + episodeRenderer.render(req)); + + storeResource(request, resource); + + return ""; + } + + private Object renderPodcastNew(Request request, Response response) throws IOException { + var requestText = request.body(); + var req = gson.fromJson(requestText, PodcastNewEpisodes.class); + + logger.info("renderPodcastNew()"); + + var resource = new RenderedResource("new.html", + getRetentionTime(), + newsRenderer.render(req)); + + storeResource(request, resource); + + return ""; + } + + + private LocalDateTime getRetentionTime() { + return LocalDateTime.now().plus(24, ChronoUnit.HOURS); + } + + private void storeResource(Request request, RenderedResource resource) { + resourceStoreClient.putResource(Context.fromRequest(request), "podcast", resource) + .timeout(10, TimeUnit.SECONDS) + .blockingSubscribe(); + } + + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/RendererMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/RendererMain.java new file mode 100644 index 00000000..3425416b --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/RendererMain.java @@ -0,0 +1,31 @@ +package nu.marginalia.wmsa.renderer; + +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import nu.marginalia.wmsa.configuration.MainClass; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.module.ConfigurationModule; +import nu.marginalia.wmsa.configuration.server.Initialization; + +import java.io.IOException; + +public class RendererMain extends MainClass { + private RendererService service; + + @Inject + public RendererMain(RendererService service + ) throws IOException { + this.service = service; + } + + public static void main(String... args) { + init(ServiceDescriptor.RENDERER, args); + + Injector injector = Guice.createInjector( + new RendererModule(), + new ConfigurationModule()); + injector.getInstance(RendererMain.class); + injector.getInstance(Initialization.class).setReady(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/RendererModule.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/RendererModule.java new file mode 100644 index 00000000..99422709 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/RendererModule.java @@ -0,0 +1,8 @@ +package nu.marginalia.wmsa.renderer; + +import com.google.inject.AbstractModule; + +public class RendererModule extends AbstractModule { + public void configure() { + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/RendererService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/RendererService.java new file mode 100644 index 00000000..b1606b6d --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/RendererService.java @@ -0,0 +1,46 @@ +package nu.marginalia.wmsa.renderer; + + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.inject.Inject; +import com.google.inject.name.Named; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.configuration.server.MetricsServer; +import nu.marginalia.wmsa.configuration.server.Service; +import nu.marginalia.wmsa.resource_store.ResourceStoreClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class RendererService extends Service { + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Gson gson = new GsonBuilder().create(); + + private final ResourceStoreClient resourceStoreClient; + + + @Inject + public RendererService(ResourceStoreClient resourceStoreClient, + @Named("service-host") String ip, + @Named("service-port") Integer port, + SmhiRendererService smhiRendererService, + PodcastRendererService podcastRendererService, + StatusRendererService statusRendererService, + Initialization initialization, + MetricsServer metricsServer + ) { + super(ip, port, initialization, metricsServer); + + this.resourceStoreClient = resourceStoreClient; + + smhiRendererService.start(); + podcastRendererService.start(); + statusRendererService.start(); + } + + public boolean isReady() { + return resourceStoreClient.isAccepting(); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/ServerStatusModel.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/ServerStatusModel.java new file mode 100644 index 00000000..fea3ee41 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/ServerStatusModel.java @@ -0,0 +1,10 @@ +package nu.marginalia.wmsa.renderer; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor @Getter +public class ServerStatusModel { + public final String server; + public final String status; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/SmhiRendererService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/SmhiRendererService.java new file mode 100644 index 00000000..ba270308 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/SmhiRendererService.java @@ -0,0 +1,83 @@ +package nu.marginalia.wmsa.renderer; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.inject.Inject; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.renderer.mustache.MustacheRenderer; +import nu.marginalia.wmsa.renderer.mustache.RendererFactory; +import nu.marginalia.wmsa.renderer.request.smhi.RenderSmhiIndexReq; +import nu.marginalia.wmsa.renderer.request.smhi.RenderSmhiPrognosReq; +import nu.marginalia.wmsa.resource_store.ResourceStoreClient; +import nu.marginalia.wmsa.resource_store.model.RenderedResource; +import nu.marginalia.wmsa.smhi.model.PrognosData; +import nu.marginalia.wmsa.smhi.model.index.IndexPlatser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Request; +import spark.Response; +import spark.Spark; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +public class SmhiRendererService { + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Gson gson = new GsonBuilder().create(); + + private final RendererFactory rendererFactory = new RendererFactory(); + + private final MustacheRenderer indexRenderer; + private final MustacheRenderer prognosRenderer; + + private ResourceStoreClient resourceStoreClient; + + + @Inject @SneakyThrows + public SmhiRendererService(ResourceStoreClient resourceStoreClient) { + this.resourceStoreClient = resourceStoreClient; + indexRenderer = rendererFactory.renderer( "smhi/index"); + prognosRenderer = rendererFactory.renderer( "smhi/prognos"); + } + + public void start() { + Spark.post("/render/smhi/index", this::renderSmhiIndex); + Spark.post("/render/smhi/prognos", this::renderSmhiPrognos); + } + + + private Object renderSmhiIndex(Request request, Response response) throws IOException { + var requestText = request.body(); + var req = gson.fromJson(requestText, RenderSmhiIndexReq.class); + + logger.info("renderSmhiIndex()"); + var resource = new RenderedResource("index.html", + LocalDateTime.MAX, + indexRenderer.render(new IndexPlatser(req.platser))); + + resourceStoreClient.putResource(Context.fromRequest(request), "smhi", resource) + .timeout(10, TimeUnit.SECONDS) + .blockingSubscribe(); + + return ""; + } + + private Object renderSmhiPrognos(Request request, Response response) throws IOException { + var requestText = request.body(); + var req = gson.fromJson(requestText, RenderSmhiPrognosReq.class); + + logger.info("renderSmhiPrognos({})", req.data.plats.namn); + var resource = new RenderedResource(req.data.plats.getUrl(), + LocalDateTime.now().plusHours(3), + prognosRenderer.render(req.data)); + + resourceStoreClient.putResource(Context.fromRequest(request), "smhi", resource) + .timeout(10, TimeUnit.SECONDS) + .blockingSubscribe(); + + return ""; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/StatusRendererService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/StatusRendererService.java new file mode 100644 index 00000000..1b6d5592 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/StatusRendererService.java @@ -0,0 +1,82 @@ +package nu.marginalia.wmsa.renderer; + +import com.google.inject.Inject; +import io.reactivex.rxjava3.schedulers.Schedulers; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.renderer.mustache.MustacheRenderer; +import nu.marginalia.wmsa.renderer.mustache.RendererFactory; +import nu.marginalia.wmsa.resource_store.ResourceStoreClient; +import nu.marginalia.wmsa.resource_store.model.RenderedResource; +import okhttp3.OkHttpClient; +import okhttp3.Request; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class StatusRendererService { + private final MustacheRenderer statusRenderer; + private ResourceStoreClient resourceStoreClient; + + private final OkHttpClient client; + + private final RendererFactory rendererFactory = new RendererFactory(); + + @Inject + @SneakyThrows + public StatusRendererService(ResourceStoreClient resourceStoreClient) { + this.resourceStoreClient = resourceStoreClient; + + client = new OkHttpClient.Builder() + .connectTimeout(50, TimeUnit.MILLISECONDS) + .readTimeout(1, TimeUnit.SECONDS) + .retryOnConnectionFailure(false) + .followRedirects(false) + .build(); + statusRenderer = rendererFactory.renderer( "status/server-status"); + } + + public void start() { + Schedulers.io().schedulePeriodicallyDirect(this::renderStatusPage, 1, 60, TimeUnit.SECONDS); + } + public void renderStatusPage() { + try { + var status = getStatus(); + var page = statusRenderer.render(Map.of("status", status)); + resourceStoreClient + .putResource(Context.internal(), "status", + new RenderedResource("index.html", LocalDateTime.now().plus(2, ChronoUnit.MINUTES), page)) + .blockingSubscribe(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private List getStatus() { + List status = new ArrayList<>(ServiceDescriptor.values().length); + + for (ServiceDescriptor sd : ServiceDescriptor.values()) { + if (sd.port == 0) { + continue; + } + try { + var req = new Request.Builder().url("http://127.0.0.1:" + sd.port + "/internal/ping").get().build(); + var call = client.newCall(req); + + call.execute().close(); + status.add(new ServerStatusModel(sd.name, "UP")); + + } catch (Exception e) { + status.add(new ServerStatusModel(sd.name, "DOWN")); + } + } + return status; + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/client/RendererClient.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/client/RendererClient.java new file mode 100644 index 00000000..e398f8b7 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/client/RendererClient.java @@ -0,0 +1,65 @@ +package nu.marginalia.wmsa.renderer.client; + +import io.reactivex.rxjava3.core.Observable; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.client.AbstractDynamicClient; +import nu.marginalia.wmsa.client.HttpStatusCode; +import nu.marginalia.wmsa.client.exception.TimeoutException; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.podcasts.model.Podcast; +import nu.marginalia.wmsa.podcasts.model.PodcastEpisode; +import nu.marginalia.wmsa.podcasts.model.PodcastListing; +import nu.marginalia.wmsa.podcasts.model.PodcastNewEpisodes; +import nu.marginalia.wmsa.renderer.request.smhi.RenderSmhiIndexReq; +import nu.marginalia.wmsa.renderer.request.smhi.RenderSmhiPrognosReq; + +import javax.inject.Inject; +import java.util.concurrent.TimeUnit; + + +public class RendererClient extends AbstractDynamicClient{ + @Inject + public RendererClient() { + super(ServiceDescriptor.RENDERER); + } + + @SneakyThrows + public Observable render(Context ctx, RenderSmhiPrognosReq req) { + return post(ctx, "/render/smhi/prognos", req) + .timeout(5, TimeUnit.SECONDS, Observable.error(new TimeoutException("RendererClient.renderSmhiPrognos()"))); + } + + + @SneakyThrows + public Observable render(Context ctx, RenderSmhiIndexReq req) { + return post(ctx, "/render/smhi/index", req) + .timeout(5, TimeUnit.SECONDS, Observable.error(new TimeoutException("RendererClient.renderSmhiIndex()"))); + } + + @SneakyThrows + public Observable render(Context ctx, PodcastNewEpisodes req) { + return post(ctx, "/render/podcast/new", req) + .timeout(5, TimeUnit.SECONDS, Observable.error(new TimeoutException("RendererClient.renderPodcastNew()"))); + } + + + @SneakyThrows + public Observable render(Context ctx, PodcastEpisode req) { + return post(ctx, "/render/podcast/episode", req) + .timeout(5, TimeUnit.SECONDS, Observable.error(new TimeoutException("RendererClient.renderPodcastEpisode()"))); + } + + @SneakyThrows + public Observable render(Context ctx, PodcastListing req) { + return post(ctx, "/render/podcast/listing", req) + .timeout(5, TimeUnit.SECONDS, Observable.error(new TimeoutException("RendererClient.renderPodcastListing()"))); + } + + + @SneakyThrows + public Observable render(Context ctx, Podcast req) { + return post(ctx, "/render/podcast", req) + .timeout(5, TimeUnit.SECONDS, Observable.error(new TimeoutException("RendererClient.renderPodcastEpisode()"))); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/mustache/MustacheRenderer.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/mustache/MustacheRenderer.java new file mode 100644 index 00000000..ae3eadb0 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/mustache/MustacheRenderer.java @@ -0,0 +1,166 @@ +package nu.marginalia.wmsa.renderer.mustache; + +import com.github.jknack.handlebars.*; +import com.github.jknack.handlebars.helper.ConditionalHelpers; +import com.github.jknack.handlebars.io.ClassPathTemplateLoader; +import com.github.jknack.handlebars.io.TemplateLoader; +import lombok.SneakyThrows; +import nu.marginalia.gemini.gmi.GemtextDocument; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import nu.marginalia.wmsa.memex.model.render.MemexRendererIndexModel; +import nu.marginalia.wmsa.memex.model.render.MemexRendererViewModel; +import org.apache.logging.log4j.util.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class MustacheRenderer { + Template template; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + MustacheRenderer(String templateFile) throws IOException { + + TemplateLoader loader = new ClassPathTemplateLoader(); + loader.setPrefix("/templates"); + loader.setSuffix(".hdb"); + + var handlebars = new Handlebars(loader); + handlebars.registerHelpers(ConditionalHelpers.class); + handlebars.registerHelpers(new GeminiHelpers()); + handlebars.registerHelper("md", new MarkdownHelper()); + handlebars.registerHelper("gen-url", this::genereraUrl); + handlebars.registerHelper("gen-thread-url", this::genereraTradUrl); + handlebars.registerHelper("gen-author-url", this::generereraAuthorUrl); + + + + try { + template = handlebars.compile(templateFile); + } + catch (FileNotFoundException ex) { + logger.error("Kunde inte ladda template " + templateFile, ex); + System.exit(2); + } + catch (HandlebarsException ex) { + logger.error("Kunde inte instantiera mall " + templateFile, ex); + System.exit(2); + } + } + + public static final class GeminiHelpers { + + public CharSequence pragma(Options options) throws IOException { + var model = options.context.model(); + GemtextDocument doc; + if (model instanceof MemexRendererIndexModel) { + doc = ((MemexRendererIndexModel) model).getDocument("index.gmi"); + } + else if (model instanceof MemexRendererViewModel) { + doc = ((MemexRendererViewModel)model).baseDoc; + } + else { + doc = null; + } + + if (doc != null && doc.getPragmas().contains((String) options.param(0))) { + return options.fn(options.context); + } + return null; + } + public CharSequence amgarp(Options options) throws IOException { + var model = options.context.model(); + GemtextDocument doc; + if (model instanceof MemexRendererIndexModel) { + doc = ((MemexRendererIndexModel) model).getDocument("index.gmi"); + } + else if (model instanceof MemexRendererViewModel) { + doc = ((MemexRendererViewModel)model).baseDoc; + } + else { + doc = null; + } + + if (doc == null || !doc.getPragmas().contains((String) options.param(0))) { + return options.fn(options.context); + } + return null; + } + + public CharSequence topbar(MemexNodeUrl url, Options options) throws IOException { + var path = url.asRelativePath(); + LinkedList nodes = new LinkedList<>(); + + for (Path p = path; p != null; p = p.getParent()) { + nodes.addFirst(p); + } + StringBuilder sb = new StringBuilder(); + for (var p : nodes) { + String name = p.toFile().getName(); + String type = "dir"; + if ("".equals(name)) { + name = "marginalia"; + type = "root"; + } + if (p.equals(path) && name.contains(".")) { + type = "file"; + } + Context newCtx = Context.newBlockParamContext(options.context, + List.of("url", "name", "type"), + List.of(p, name, type) + ); + sb.append(options.fn(newCtx)); + }; + return sb.toString(); + } + } + + @SneakyThrows + public String render(T model) { + return template.apply(model); + } + + @SneakyThrows + public String render(T model, String name, List children) { + Context ctx = Context.newBuilder(model).combine(name, children).build(); + + return template.apply(ctx); + } + + @SneakyThrows + public String render(T model, Map children) { + Context ctx = Context.newBuilder(model).combine(children).build(); + return template.apply(ctx); + } + + private Object genereraUrl(Object context, Options options) { + if (null != context) { + return context.toString().toLowerCase() + ".html"; + } else { + logger.error("Kunde inte generera URL, blockParams {}", options.blockParams); + return ""; + } + } + private Object genereraTradUrl(Object context, Options options) { + if (null != context) { + return context.toString().toLowerCase() + "/view.html"; + } else { + logger.error("Kunde inte generera URL, blockParams {}", options.blockParams); + return ""; + } + } + private Object generereraAuthorUrl(Object context, Options options) { + if (null != context) { + return "u_" + context.toString().toLowerCase() + ".html"; + } else { + logger.error("Kunde inte generera URL, blockParams {}", options.blockParams); + return ""; + } + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/mustache/RendererFactory.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/mustache/RendererFactory.java new file mode 100644 index 00000000..0fe81abf --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/mustache/RendererFactory.java @@ -0,0 +1,13 @@ +package nu.marginalia.wmsa.renderer.mustache; + +import java.io.IOException; + +public class RendererFactory { + + public RendererFactory() { + } + + public MustacheRenderer renderer(String template) throws IOException { + return new MustacheRenderer<>(template); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/request/smhi/RenderSmhiIndexReq.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/request/smhi/RenderSmhiIndexReq.java new file mode 100644 index 00000000..d585d56f --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/request/smhi/RenderSmhiIndexReq.java @@ -0,0 +1,13 @@ +package nu.marginalia.wmsa.renderer.request.smhi; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import nu.marginalia.wmsa.smhi.model.Plats; + +import java.util.List; + +@NoArgsConstructor @AllArgsConstructor @Getter +public class RenderSmhiIndexReq { + public List platser; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/request/smhi/RenderSmhiPrognosReq.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/request/smhi/RenderSmhiPrognosReq.java new file mode 100644 index 00000000..ba1746db --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/renderer/request/smhi/RenderSmhiPrognosReq.java @@ -0,0 +1,11 @@ +package nu.marginalia.wmsa.renderer.request.smhi; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import nu.marginalia.wmsa.smhi.model.PrognosData; + +@NoArgsConstructor @AllArgsConstructor @Getter +public class RenderSmhiPrognosReq { + public PrognosData data; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceEntityStore.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceEntityStore.java new file mode 100644 index 00000000..024cec46 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceEntityStore.java @@ -0,0 +1,248 @@ +package nu.marginalia.wmsa.resource_store; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.inject.name.Named; +import io.prometheus.client.Counter; +import io.reactivex.rxjava3.schedulers.Schedulers; +import nu.marginalia.wmsa.resource_store.model.RenderedResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; + + +public class ResourceEntityStore { + private final Map resources = new HashMap<>(); + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Path dataPath; + private final Gson gson = new GsonBuilder().create(); + private final Base64.Encoder b64encoder = Base64.getEncoder(); + + private final static Counter wmsa_resource_store_count + = Counter.build("wmsa_resource_store_count", "number of items in the resource store") + .register(); + private final static Counter wmsa_resource_store_eviction_count + = Counter.build("wmsa_resource_store_eviction_count", "evicted items") + .register(); + + @Inject + public ResourceEntityStore(@Named("data-path") Path dataPath) { + this.dataPath = dataPath; + + Schedulers.io().scheduleDirect(() -> loadResourcesFromDisk(dataPath)); + Schedulers.io().schedulePeriodicallyDirect(() -> purgeFileSystem(dataPath), 1, 1, TimeUnit.HOURS); + } + + public ResourceEntityStore(@Named("data-path") Path dataPath, boolean immediate) { + this.dataPath = dataPath; + + loadResourcesFromDisk(dataPath); + } + + public ResourceEntityStore() { + this.dataPath = null; + } + + public RenderedResource getResource(String domain, String resource) { + Lock readLock = lock.readLock(); + try { + readLock.lock(); + return resources.get(getKey(domain, resource)); + } + finally { + readLock.unlock(); + } + } + + public void putResource(String domain, String resource, RenderedResource data) { + RenderedResource oldResource = loadResource(domain, resource, data); + + wmsa_resource_store_count.inc(); + if (dataPath != null) { + Path domainPath = dataPath.resolve(domain); + if (!domainPath.toFile().isDirectory()) { + domainPath.toFile().mkdir(); + } + + if (oldResource != null) { + try { + Path oldResourcePath = domainPath.resolve(oldResource.diskFileName()); + oldResourcePath.toFile().delete(); + } + catch (Exception ex) { + logger.error("Failed to remove old resource {}/{}", domain, oldResource.diskFileName()); + } + } + + Path resourcePath = domainPath.resolve(data.diskFileName()); + try { + Files.writeString(resourcePath, gson.toJson(data)); + } catch (IOException e) { + logger.error("Failed to write resource {}/{}", domain, resource); + logger.error("Exception", e); + } + + + } + } + + @Nullable + private RenderedResource loadResource(String domain, String resource, RenderedResource data) { + Lock writeLock = lock.writeLock(); + RenderedResource oldResource; + try { + writeLock.lock(); + oldResource = resources.put(getKey(domain, resource), data); + } + finally { + writeLock.unlock(); + } + return oldResource; + } + + private String getKey(String domain, String resource) { + return domain + "/" + resource; + } + + + public void reapStaleResources() { + Lock writeLock = lock.writeLock(); + try { + writeLock.lock(); + List expiredResources = resources.entrySet().stream() + .filter(entry -> entry.getValue().isExpired()) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + for (String resource : expiredResources) { + logger.info("Reaping expired resource \"{}\"", resource); + var res = resources.remove(resource); + wmsa_resource_store_eviction_count.inc(); + + if (dataPath != null) { + File resourceFile = dataPath.resolve(res.diskFileName()).toFile(); + if (resourceFile.exists()) { + resourceFile.delete(); + } + } + } + } + finally { + writeLock.unlock(); + } + } + + public int numResources() { + Lock readLock = lock.readLock(); + try { + readLock.lock(); + return resources.size(); + } + finally { + readLock.unlock(); + } + } + + public long resourceSize() { + Lock readLock = lock.readLock(); + try { + readLock.lock(); + return resources.values().stream().mapToLong(RenderedResource::size).sum(); + } + finally { + readLock.unlock(); + } + } + + public void loadResourcesFromDisk(Path dataPath) { + File dataDir = dataPath.toFile(); + + for (var dir : dataDir.listFiles()) { + if (!dir.isDirectory()) { + logger.warn("Junk file {} in data directory", dir); + } + else { + for (var file : dir.listFiles()) { + try { + loadFromFile(dir.getName(), file); + } + catch (Exception ex) { + logger.error("Failed to load file {}", file); + logger.error("Failed to load resource from disk", ex); + } + } + } + } + } + + public void purgeFileSystem(Path dataPath) { + File dataDir = dataPath.toFile(); + + for (var dir : dataDir.listFiles()) { + if (!dir.isDirectory()) { + logger.warn("Junk file {} in data directory", dir); + } + else { + for (var file : dir.listFiles()) { + try { + purgeFile(file); + } + catch (Exception ex) { + logger.error("Failed to purge resource from disk", ex); + } + } + } + } + } + + private void purgeFile(File file) throws IOException { + String json = Files.readString(file.toPath(), Charset.defaultCharset()); + var resource = gson.fromJson(json, RenderedResource.class); + + if (resource.isExpired()) { + logger.info("Deleting expired resource {}", file); + + file.delete(); + } + + } + + + private void loadFromFile(String domain, File file) { + try { + String json = Files.readString(file.toPath(), Charset.defaultCharset()); + var resource = gson.fromJson(json, RenderedResource.class); + + if (resource.isExpired() || resources.containsKey(getKey(domain, resource.getFilename()))) { + logger.info("Deleting expired resource {}", file); + + file.delete(); + } + else { + logger.info("Re-loading resource {}", file); + loadResource(domain, resource.getFilename(), resource); + } + } catch (IOException e) { + logger.error("Could not read file {}", file.toString()); + } + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceStoreClient.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceStoreClient.java new file mode 100644 index 00000000..b057d450 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceStoreClient.java @@ -0,0 +1,46 @@ +package nu.marginalia.wmsa.resource_store; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import nu.marginalia.wmsa.client.AbstractDynamicClient; +import nu.marginalia.wmsa.client.HttpStatusCode; +import nu.marginalia.wmsa.client.exception.TimeoutException; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.resource_store.model.RenderedResource; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +@Singleton +public class ResourceStoreClient extends AbstractDynamicClient{ + + @Inject + public ResourceStoreClient() { + super(ServiceDescriptor.RESOURCE_STORE); + } + + public Observable getResource(Context ctx, String domain, String resource) { + return get(ctx, "/"+domain+"/"+resource) + .timeout(5, TimeUnit.SECONDS, Observable.error(new TimeoutException("ResourceStoreClient.getResource()"))) + ; + } + + public Observable putResource(Context ctx, String domain, RenderedResource data) { + return post(ctx, "/"+domain, data) + .timeout(5, TimeUnit.SECONDS, Observable.error(new TimeoutException("ResourceStoreClient.putResource()"))); + + } + + public Observable cacheResource(Context ctx, String domain, String resource, Supplier generator, LocalDateTime expiry) { + return getResource(ctx, domain, resource) + .onErrorReturn(e -> { + var renderedResource = new RenderedResource(resource, expiry, generator.get()); + putResource(ctx, "wiki", renderedResource).subscribeOn(Schedulers.io()).blockingSubscribe(); + return renderedResource.data; + }); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceStoreMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceStoreMain.java new file mode 100644 index 00000000..ddcb8c4d --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceStoreMain.java @@ -0,0 +1,32 @@ +package nu.marginalia.wmsa.resource_store; + +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import nu.marginalia.wmsa.configuration.MainClass; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.module.ConfigurationModule; +import nu.marginalia.wmsa.configuration.server.Initialization; + +import java.io.IOException; + +public class ResourceStoreMain extends MainClass { + private ResourceStoreService service; + + @Inject + public ResourceStoreMain(ResourceStoreService service) throws IOException { + this.service = service; + + } + + public static void main(String... args) { + init(ServiceDescriptor.RESOURCE_STORE, args); + + Injector injector = Guice.createInjector( + new ResourceStoreModule(), + new ConfigurationModule() + ); + injector.getInstance(ResourceStoreMain.class); + injector.getInstance(Initialization.class).setReady(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceStoreModule.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceStoreModule.java new file mode 100644 index 00000000..2de9e931 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceStoreModule.java @@ -0,0 +1,15 @@ +package nu.marginalia.wmsa.resource_store; + +import com.google.inject.AbstractModule; +import com.google.inject.name.Names; + +import java.nio.file.Path; + +public class ResourceStoreModule extends AbstractModule { + public void configure() { + bind(String.class).annotatedWith(Names.named("external-url")).toInstance("https://reddit.marginalia.nu/"); + bind(Path.class).annotatedWith(Names.named("data-path")).toInstance(Path.of("/var/lib/wmsa/archive.fast/resources")); + } + + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceStoreService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceStoreService.java new file mode 100644 index 00000000..2870c4d6 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/ResourceStoreService.java @@ -0,0 +1,191 @@ +package nu.marginalia.wmsa.resource_store; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.inject.Inject; +import com.google.inject.name.Named; +import io.reactivex.rxjava3.schedulers.Schedulers; +import kotlin.text.Charsets; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.auth.client.AuthClient; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.configuration.server.MetricsServer; +import nu.marginalia.wmsa.configuration.server.Service; +import nu.marginalia.wmsa.resource_store.model.RenderedResource; +import org.apache.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Request; +import spark.Response; +import spark.Spark; +import spark.resource.ClassPathResource; +import spark.staticfiles.MimeType; + +import java.io.FileNotFoundException; +import java.net.URLEncoder; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.concurrent.TimeUnit; + +public class ResourceStoreService extends Service { + private Gson gson = new GsonBuilder().create(); + private Logger logger = LoggerFactory.getLogger(getClass()); + private final long startTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC); + + private AuthClient authClient; + private final ResourceEntityStore resourceStore; + + @Inject + public ResourceStoreService(@Named("service-host") String ip, + @Named("service-port") Integer port, + AuthClient authClient, + ResourceEntityStore resourceStore, + Initialization initialization, + MetricsServer metricsServer + ) { + super(ip, port, initialization, metricsServer); + this.authClient = authClient; + this.resourceStore = resourceStore; + + Schedulers.io().schedulePeriodicallyDirect(resourceStore::reapStaleResources, + 5, 5, TimeUnit.MINUTES); + + Spark.get("/public/*", this::getDefaultResource); + + Spark.get("/:domain/*", this::getResource); + Spark.post("/:domain", this::storeResource); + + } + + private Object getDefaultResource(Request request, Response response) { + String headerDomain = request.headers("X-Domain"); + + if (headerDomain == null) { + Spark.halt(404); + } + + var splat = request.splat(); + var resource = splat.length == 0 ? "index.html" : splat[0]; + + return getResource(request, response, headerDomain, resource); + + } + + private Object storeResource(Request request, Response response) { + var domain = request.params("domain"); + var data = gson.fromJson(request.body(), RenderedResource.class); + + logger.info("storeResource({}/{}, {})", domain, data.filename, data.etag()); + + resourceStore.putResource(domain, data.filename, data); + + Spark.halt(HttpStatus.SC_ACCEPTED); + return null; + } + + private Object getResource(Request request, Response response) { + String headerDomain = request.headers("X-Domain"); + var domain = request.params("domain"); + + if (headerDomain != null && !domain.equals(headerDomain)) { + logger.warn("{} - domain mismatch: Header = {}, request = {}", Context.fromRequest(request), headerDomain, domain); + Spark.halt(403); + } + + var splat = request.splat(); + var resource = splat.length == 0 ? "index.html" : splat[0]; + + return getResource(request, response, domain, resource); + } + + private String getResource(Request request, Response response, String domain, String resource) { + + var data = resourceStore.getResource(domain, resource); + + if (data != null) { + logger.info("getResource({}/{}, {})", domain, resource, data.etag()); + validatePermission(Context.fromRequest(request), request, response, domain, data); + + return serveDynamic(data, request, response); + } + else if (serveStatic(domain + "/" + resource, request, response)) { + logger.info("getResource({}/{}, static)", domain, resource); + } + else { + logger.info("Could not serve {}/{}", domain, resource); + Spark.halt(404, "Not Found"); + } + return ""; + } + + + private void validatePermission(Context ctx, Request req, Response rsp, String domain, RenderedResource resource) { + if ("memex".equals(domain)) { + if (resource.requireLogin && !memexIsLoggedIn(ctx)) { + rsp.redirect("https://www.marginalia.nu/auth/login?service=MEMEX&redirect="+ URLEncoder.encode(req.headers("X-Extern-Url"), Charsets.UTF_8)); + Spark.halt(); + } + } + } + + private boolean memexIsLoggedIn(Context ctx) { + return authClient.isLoggedIn(ctx).timeout(1, TimeUnit.SECONDS).blockingFirst(); + } + private String serveDynamic(RenderedResource data, Request request, Response response) { + handleEtag(data, request, response); + + return data.data; + } + + @SneakyThrows + private boolean serveStatic(String path, Request req, Response rsp) { + try { + ClassPathResource resource = new ClassPathResource("static/" + path); + handleEtagStatic(resource, req, rsp); + resource.getInputStream().transferTo(rsp.raw().getOutputStream()); + } + catch (IllegalArgumentException|FileNotFoundException ex) { + return false; + } + + return true; + } + + @SneakyThrows + private void handleEtag(RenderedResource page, Request req, Response rsp) { + rsp.header("Cache-Control", "private, must-revalidate"); + + if (!page.filename.endsWith(".txt")) { + rsp.type("text/html"); + } + else { + rsp.type(MimeType.fromResource(new ClassPathResource(page.filename))); + } + final String etag = page.etag(); + + if (etag.equals(req.headers("If-None-Match"))) { + Spark.halt(304); + } + + rsp.header("ETag", etag); + } + + @SneakyThrows + private void handleEtagStatic(ClassPathResource resource, Request req, Response rsp) { + rsp.header("Cache-Control", "public,max-age=3600"); + rsp.type(MimeType.fromResource(resource)); + + final String etag = staticResourceEtag(resource.getFilename()); + + if (etag.equals(req.headers("If-None-Match"))) { + Spark.halt(304); + } + + rsp.header("ETag", etag); + } + + private String staticResourceEtag(String resource) { + return "\"" + resource.hashCode() + "-" + startTime + "\""; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/model/RenderedResource.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/model/RenderedResource.java new file mode 100644 index 00000000..f445a6c7 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/resource_store/model/RenderedResource.java @@ -0,0 +1,46 @@ +package nu.marginalia.wmsa.resource_store.model; + +import lombok.Getter; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Getter +public class RenderedResource { + public final String filename; + public final String data; + public final String genTime = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + public final long genTimeMillis = System.currentTimeMillis(); + public final String expiry; + public final boolean requireLogin; + + public RenderedResource(String filename, LocalDateTime expiryDate, String data) { + this.filename = filename; + this.data = data; + this.expiry = expiryDate.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + this.requireLogin = false; + } + public RenderedResource(String filename, LocalDateTime expiryDate, String data, boolean requireLogin) { + this.filename = filename; + this.data = data; + this.expiry = expiryDate.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + this.requireLogin = requireLogin; + } + public boolean isExpired() { + var expiryDate = LocalDateTime.parse(expiry, DateTimeFormatter.ISO_LOCAL_DATE_TIME); + return expiryDate.isBefore(LocalDateTime.now()); + + } + + public String etag() { + return "\"" + genTime.hashCode() + "-" + data.hashCode() + "\""; + } + + public String diskFileName() { + return filename.hashCode() + "-" + data.hashCode() + ".html"; + } + + public long size() { + return 2L*(data.length()+filename.length()); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/SmhiScraperService.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/SmhiScraperService.java new file mode 100644 index 00000000..2b074efb --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/SmhiScraperService.java @@ -0,0 +1,79 @@ +package nu.marginalia.wmsa.smhi; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import io.reactivex.rxjava3.schedulers.Schedulers; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.configuration.server.MetricsServer; +import nu.marginalia.wmsa.configuration.server.Service; +import nu.marginalia.wmsa.renderer.client.RendererClient; +import nu.marginalia.wmsa.renderer.request.smhi.RenderSmhiIndexReq; +import nu.marginalia.wmsa.renderer.request.smhi.RenderSmhiPrognosReq; +import nu.marginalia.wmsa.smhi.model.Plats; +import nu.marginalia.wmsa.smhi.model.PrognosData; +import nu.marginalia.wmsa.smhi.scraper.crawler.SmhiCrawler; +import nu.marginalia.wmsa.smhi.scraper.crawler.entity.SmhiEntityStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Spark; + +import java.util.Comparator; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class SmhiScraperService extends Service { + + private final SmhiCrawler crawler; + private final SmhiEntityStore entityStore; + private final RendererClient rendererClient; + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Initialization initialization; + @Inject + public SmhiScraperService(@Named("service-host") String ip, + @Named("service-port") Integer port, + SmhiCrawler crawler, + SmhiEntityStore entityStore, + RendererClient rendererClient, + Initialization initialization, + MetricsServer metricsServer) { + super(ip, port, initialization, metricsServer); + this.crawler = crawler; + this.entityStore = entityStore; + this.rendererClient = rendererClient; + this.initialization = initialization; + + Spark.awaitInitialization(); + + Schedulers.newThread().scheduleDirect(this::start); + } + + private void start() { + initialization.waitReady(); + rendererClient.waitReady(); + + entityStore.platser.debounce(6, TimeUnit.SECONDS) + .subscribe(this::updateIndex); + entityStore.prognosdata.subscribe(this::updatePrognos); + + crawler.start(); + } + + private void updatePrognos(PrognosData prognosData) { + rendererClient + .render(Context.internal(), new RenderSmhiPrognosReq(prognosData)) + .timeout(30, TimeUnit.SECONDS) + .blockingSubscribe(); + } + + private void updateIndex(Plats unused) { + var platser = entityStore.platser().stream() + .sorted(Comparator.comparing(plats -> plats.namn)) + .collect(Collectors.toList()); + + rendererClient + .render(Context.internal(), new RenderSmhiIndexReq(platser)) + .timeout(30, TimeUnit.SECONDS) + .blockingSubscribe(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/Parameter.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/Parameter.java new file mode 100644 index 00000000..012e9c24 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/Parameter.java @@ -0,0 +1,9 @@ +package nu.marginalia.wmsa.smhi.model; + +public class Parameter { + public String name; + public String levelType; + public String level; + public String unit; + public String[] values; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/Plats.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/Plats.java new file mode 100644 index 00000000..7ae39675 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/Plats.java @@ -0,0 +1,42 @@ +package nu.marginalia.wmsa.smhi.model; + +import lombok.Getter; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +@Getter +public class Plats { + public final String namn; + public final double latitud; + public final double longitud; + + public String getUrl() { + return namn.toLowerCase()+".html"; + } + + public Plats(String namn, String latitud, String longitud) { + this.namn = namn; + this.longitud = Double.parseDouble(longitud); + this.latitud = Double.parseDouble(latitud); + } + + public String toString() { + return String.format("Plats[%s %s %s]", namn, longitud, latitud); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + Plats plats = (Plats) o; + + return new EqualsBuilder().append(latitud, plats.latitud).append(longitud, plats.longitud).append(namn, plats.namn).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(namn).append(latitud).append(longitud).toHashCode(); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/Platser.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/Platser.java new file mode 100644 index 00000000..c0f7c15f --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/Platser.java @@ -0,0 +1,16 @@ +package nu.marginalia.wmsa.smhi.model; + + +import java.util.List; + +public class Platser { + private final List platser; + + public Platser(List platser) { + this.platser = platser; + } + + public List getPlatser() { + return platser; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/PrognosData.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/PrognosData.java new file mode 100644 index 00000000..7b7d0516 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/PrognosData.java @@ -0,0 +1,41 @@ +package nu.marginalia.wmsa.smhi.model; + +import nu.marginalia.wmsa.smhi.model.dyn.Dygnsdata; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class PrognosData { + + public String crawlTime = LocalDateTime.now().toString(); + + public String approvedTime; + public String referenceTime; + public String expires; + + public Plats plats; + + public List timeSeries = new ArrayList<>(); + + public String getBastFore() { + return LocalDateTime.parse(crawlTime).atZone(ZoneId.of("Europe/Stockholm")) + .plusHours(3) + .format(DateTimeFormatter.ISO_TIME); + } + public Plats getPlats() { + return plats; + } + + public List getTidpunkter() { + return timeSeries; + } + public List getDygn() { + return timeSeries.stream().map(Tidpunkt::getDate).distinct() + .map(datum -> new Dygnsdata(datum, this)) + .collect(Collectors.toList()); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/Tidpunkt.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/Tidpunkt.java new file mode 100644 index 00000000..d83ee7c4 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/Tidpunkt.java @@ -0,0 +1,75 @@ +package nu.marginalia.wmsa.smhi.model; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; +import java.util.ArrayList; +import java.util.List; + +public class Tidpunkt { + + private static final ZoneId serverZoneId = ZoneId.of("GMT"); + private static final ZoneId localZoneId = ZoneId.of("Europe/Stockholm"); + private static DateTimeFormatter timeFormatter = (new DateTimeFormatterBuilder()) + .appendValue(ChronoField.HOUR_OF_DAY, 2) + .appendLiteral(':') + .appendValue(ChronoField.MINUTE_OF_HOUR, 2) + .toFormatter(); + + public String validTime; + + public List parameters = new ArrayList<>(); + + + private String getParam(String name) { + var data = parameters.stream().filter(p -> name.equals(p.name)).map(p->p.values).findFirst().orElseGet(() -> new String[0]); + if (data.length > 0) { + return data[0]; + } + return null; + } + public String getDate() { + return ZonedDateTime.parse(validTime).toLocalDateTime().atZone(serverZoneId).toOffsetDateTime().atZoneSameInstant(localZoneId).format(DateTimeFormatter.ISO_LOCAL_DATE); + } + + public String getTime() { + return ZonedDateTime.parse(validTime).toLocalDateTime().atZone(serverZoneId).toOffsetDateTime().atZoneSameInstant(localZoneId).format(timeFormatter); + } + + public String getTemp() { + return getParam("t"); + } + public String getMoln() { + return getParam("tcc_mean"); + } + public String getVind() { + return getParam("ws"); + } + public String getByvind() { + return getParam("gust"); + } + public String getNederbord() { + return getParam("pmedian"); + } + public String getNederbordTyp() { + switch(getParam("pcat")) { + case "1": return "S"; + case "2": return "SB"; + case "3": return "R"; + case "4": return "D"; + case "5": return "UKR"; + case "6": return "UKD"; + default: + return ""; + + } + } + public String getVindRiktning() { + return getParam("wd"); + } + public String toString() { + return String.format("Tidpunkt[%s %s]", validTime, getTemp()); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/dyn/Dygnsdata.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/dyn/Dygnsdata.java new file mode 100644 index 00000000..05f2246a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/dyn/Dygnsdata.java @@ -0,0 +1,40 @@ +package nu.marginalia.wmsa.smhi.model.dyn; + +import nu.marginalia.wmsa.smhi.model.PrognosData; +import nu.marginalia.wmsa.smhi.model.Tidpunkt; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +public class Dygnsdata { + public final String date; + private final PrognosData data; + + public Dygnsdata(String date, PrognosData data) { + this.date = date; + this.data = data; + } + + public String getDate() { + return date; + } + public List getData() { + String d = getDate(); + return data.timeSeries.stream().filter(p -> d.equals(p.getDate())).collect(Collectors.toList()); + } + + public String getVeckodag() { + switch (LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE).getDayOfWeek()) { + case MONDAY: return "Måndag"; + case TUESDAY: return "Tisdag"; + case WEDNESDAY: return "Onsdag"; + case THURSDAY: return "Torsdag"; + case FRIDAY: return "Fredag"; + case SATURDAY: return "Lördag"; + case SUNDAY: return "Söndag"; + } + return "Annandag"; + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/index/IndexPlats.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/index/IndexPlats.java new file mode 100644 index 00000000..5e3f3a19 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/index/IndexPlats.java @@ -0,0 +1,13 @@ +package nu.marginalia.wmsa.smhi.model.index; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import nu.marginalia.wmsa.smhi.model.Plats; + +import java.util.List; + +@Getter @AllArgsConstructor +public class IndexPlats { + String nyckel; + List platser; +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/index/IndexPlatser.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/index/IndexPlatser.java new file mode 100644 index 00000000..b67e7817 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/model/index/IndexPlatser.java @@ -0,0 +1,28 @@ +package nu.marginalia.wmsa.smhi.model.index; + +import lombok.Getter; +import nu.marginalia.wmsa.smhi.model.Plats; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Getter +public class IndexPlatser { + List platserPerNyckel = new ArrayList<>(); + + public IndexPlatser(List platser) { + var platsMap = kategoriseraEfterNyckel(platser); + + platsMap.keySet().stream().sorted() + .forEach(p -> platserPerNyckel.add(new IndexPlats(p, platsMap.get(p)))); + } + + private Map> kategoriseraEfterNyckel(List platser) { + return platser.stream().collect( + Collectors.groupingBy(p -> + p.namn.substring(0, 1) + .toUpperCase())); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/PlatsReader.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/PlatsReader.java new file mode 100644 index 00000000..3ea0d8cc --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/PlatsReader.java @@ -0,0 +1,44 @@ +package nu.marginalia.wmsa.smhi.scraper; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import com.opencsv.CSVReader; +import nu.marginalia.wmsa.smhi.model.Plats; + +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Singleton +public class PlatsReader { + private final String fileName; + + @Inject + public PlatsReader(@Named("plats-csv-file") String fileName) { + this.fileName = fileName; + } + + public List readPlatser() throws Exception { + List platser = new ArrayList<>(); + + var resource = Objects.requireNonNull(ClassLoader.getSystemResourceAsStream(fileName), + "Kunde inte ladda " + fileName); + try (var reader = new CSVReader(new InputStreamReader(resource, StandardCharsets.UTF_8))) { + for (;;) { + String[] strings = reader.readNext(); + if (strings == null) { + return platser; + } + platser.add(skapaPlats(strings)); + } + } + + } + + private Plats skapaPlats(String[] strings) { + return new Plats(strings[0], strings[1], strings[2]); + } +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/SmhiScraperMain.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/SmhiScraperMain.java new file mode 100644 index 00000000..8898247e --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/SmhiScraperMain.java @@ -0,0 +1,32 @@ +package nu.marginalia.wmsa.smhi.scraper; + +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import nu.marginalia.wmsa.configuration.MainClass; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.module.ConfigurationModule; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.smhi.SmhiScraperService; + +import java.io.IOException; + +public class SmhiScraperMain extends MainClass { + private final SmhiScraperService service; + + @Inject + public SmhiScraperMain(SmhiScraperService service) throws IOException { + this.service = service; + } + + public static void main(String... args) { + init(ServiceDescriptor.SMHI_SCRAPER, args); + + Injector injector = Guice.createInjector( + new SmhiScraperModule(), + new ConfigurationModule()); + injector.getInstance(SmhiScraperMain.class); + injector.getInstance(Initialization.class).setReady(); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/SmhiScraperModule.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/SmhiScraperModule.java new file mode 100644 index 00000000..ffb1793a --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/SmhiScraperModule.java @@ -0,0 +1,12 @@ +package nu.marginalia.wmsa.smhi.scraper; + +import com.google.inject.AbstractModule; +import com.google.inject.name.Names; + +public class SmhiScraperModule extends AbstractModule { + public void configure() { + bind(String.class).annotatedWith(Names.named("plats-csv-file")).toInstance("data/smhi/stader.csv"); + bind(String.class).annotatedWith(Names.named("smhi-user-agent")).toInstance("kontakt@marginalia.nu"); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/crawler/SmhiBackendApi.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/crawler/SmhiBackendApi.java new file mode 100644 index 00000000..9880e317 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/crawler/SmhiBackendApi.java @@ -0,0 +1,88 @@ +package nu.marginalia.wmsa.smhi.scraper.crawler; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import nu.marginalia.wmsa.smhi.model.Plats; +import org.apache.http.Header; +import org.apache.http.HttpHost; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.conn.routing.HttpRoute; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Locale; + +@Singleton +public class SmhiBackendApi { + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final String server = "https://opendata-download-metfcst.smhi.se/api"; + private final PoolingHttpClientConnectionManager connectionManager; + private final String userAgent; + + @Inject + public SmhiBackendApi(@Named("smhi-user-agent") String userAgent) { + this.userAgent = userAgent; + + connectionManager = new PoolingHttpClientConnectionManager(); + connectionManager.setMaxTotal(200); + connectionManager.setDefaultMaxPerRoute(20); + HttpHost host = new HttpHost("https://opendata-download-metfcst.smhi.se"); + connectionManager.setMaxPerRoute(new HttpRoute(host), 50); + } + + public SmhiApiRespons hamtaData(Plats plats) throws Exception { + var client = HttpClients.custom() + .setConnectionManager(connectionManager) + .build(); + + String url = String.format(Locale.US, "%s/category/pmp3g/version/2/geotype/point/lon/%f/lat/%f/data.json", + server, plats.longitud, plats.latitud); + + Thread.sleep(100); + + logger.info("Fetching {} - {}", plats, url); + + HttpGet get = new HttpGet(url); + get.addHeader("User-Agent", userAgent); + + try (var rsp = client.execute(get)) { + var entity = rsp.getEntity(); + String content = new String(entity.getContent().readAllBytes()); + int statusCode = rsp.getStatusLine().getStatusCode(); + + var expires = + Arrays.stream(rsp.getHeaders("Expires")) + .map(Header::getValue) + .map(DateTimeFormatter.RFC_1123_DATE_TIME::parse) + .map(LocalDateTime::from) + .findFirst().map(Object::toString).orElse(""); + + + if (statusCode == 200) { + return new SmhiApiRespons(content, expires, plats); + } + throw new IllegalStateException("Fel i backend " + statusCode + " " + content); + } + + } + +} + +class SmhiApiRespons { + public final String jsonContent; + public final String expiryDate; + public final Plats plats; + + SmhiApiRespons(String jsonContent, String expiryDate, Plats plats) { + this.jsonContent = jsonContent; + this.expiryDate = expiryDate; + this.plats = plats; + } +} \ No newline at end of file diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/crawler/SmhiCrawler.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/crawler/SmhiCrawler.java new file mode 100644 index 00000000..b9b97fb5 --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/crawler/SmhiCrawler.java @@ -0,0 +1,106 @@ +package nu.marginalia.wmsa.smhi.scraper.crawler; + +import com.google.gson.*; +import com.google.inject.Inject; +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.smhi.model.Plats; +import nu.marginalia.wmsa.smhi.model.PrognosData; +import nu.marginalia.wmsa.smhi.scraper.PlatsReader; +import nu.marginalia.wmsa.smhi.scraper.crawler.entity.SmhiEntityStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Type; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +public class SmhiCrawler { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final Gson gson; + private SmhiBackendApi api; + private SmhiEntityStore store; + private final List platser; + private Disposable job; + + @Inject @SneakyThrows + public SmhiCrawler(SmhiBackendApi backendApi, SmhiEntityStore store, PlatsReader platsReader) { + this.api = backendApi; + this.store = store; + this.platser = platsReader.readPlatser(); + + class LocalDateAdapter implements JsonDeserializer { + @Override + public LocalDateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return LocalDateTime + .parse(json.getAsString(), DateTimeFormatter.ISO_ZONED_DATE_TIME); + } + } + + gson = new GsonBuilder() + .registerTypeAdapter(LocalDateTime.class, new LocalDateAdapter()) + .create(); + } + + public void start() { + job = Observable + .fromIterable(new ArrayList<>(platser)) + .subscribeOn(Schedulers.io()) + .filter(this::isNeedsUpdate) + .take(5) + .flatMapMaybe(this::hamtaData) + .repeatWhen(this::repeatDelay) + .doOnError(this::handleError) + .subscribe(store::offer); + } + public void stop() { + Optional.ofNullable(job).ifPresent(Disposable::dispose); + } + + private Observable repeatDelay(Observable completed) { + return completed.delay(1, TimeUnit.SECONDS); + } + + protected void handleError(Throwable throwable) { + logger.error("Caught error", throwable); + } + + public Maybe hamtaData(Plats plats) { + try { + var data = api.hamtaData(plats); + + PrognosData model = gson.fromJson(data.jsonContent, PrognosData.class); + + model.expires = data.expiryDate; + model.plats = plats; + + return Maybe.just(model); + } + catch (Exception ex) { + logger.error("Failed to fetch data", ex); + return Maybe.empty(); + } + } + + + boolean isNeedsUpdate(Plats plats) { + var prognos = store.prognos(plats); + + if (null == prognos) { + return true; + } + + LocalDateTime crawlTime = LocalDateTime.parse(prognos.crawlTime); + return crawlTime.plusHours(1).isBefore(LocalDateTime.now()); + } + +} diff --git a/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/crawler/entity/SmhiEntityStore.java b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/crawler/entity/SmhiEntityStore.java new file mode 100644 index 00000000..d2f608aa --- /dev/null +++ b/marginalia_nu/src/main/java/nu/marginalia/wmsa/smhi/scraper/crawler/entity/SmhiEntityStore.java @@ -0,0 +1,62 @@ +package nu.marginalia.wmsa.smhi.scraper.crawler.entity; + +import com.google.inject.Singleton; +import io.reactivex.rxjava3.subjects.PublishSubject; +import nu.marginalia.wmsa.smhi.model.Plats; +import nu.marginalia.wmsa.smhi.model.PrognosData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +@Singleton +public class SmhiEntityStore { + private final ReadWriteLock rwl = new ReentrantReadWriteLock(); + private final Map data = new HashMap<>(); + + public final PublishSubject platser = PublishSubject.create(); + public final PublishSubject prognosdata = PublishSubject.create(); + Logger logger = LoggerFactory.getLogger(getClass()); + public boolean offer(PrognosData modell) { + Lock lock = this.rwl.writeLock(); + try { + lock.lock(); + if (data.put(modell.plats, modell) == null) { + platser.onNext(modell.plats); + } + prognosdata.onNext(modell); + } + finally { + lock.unlock(); + } + return true; + } + + public List platser() { + Lock lock = this.rwl.readLock(); + try { + lock.lock(); + return new ArrayList<>(data.keySet()); + } + finally { + lock.unlock(); + } + } + + public PrognosData prognos(Plats plats) { + Lock lock = this.rwl.readLock(); + try { + lock.lock(); + return data.get(plats); + } + finally { + lock.unlock(); + } + } +} diff --git a/marginalia_nu/src/main/nlp-models/README.md b/marginalia_nu/src/main/nlp-models/README.md new file mode 100644 index 00000000..3f46af3a --- /dev/null +++ b/marginalia_nu/src/main/nlp-models/README.md @@ -0,0 +1,3 @@ +# OpenNLP models + +[https://opennlp.apache.org/](https://opennlp.apache.org/) \ No newline at end of file diff --git a/marginalia_nu/src/main/nlp-models/en-token.bin b/marginalia_nu/src/main/nlp-models/en-token.bin new file mode 100644 index 0000000000000000000000000000000000000000..c417277ca704a01ed7dc3ccb34d65dc3205317ce GIT binary patch literal 439890 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↢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

    UTGR$QVdE2ysug|uk1U9S(% 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]Po=3G5#y^zQGG0jpMEXL{*2GESb_ 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

  • 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

    q%f7GGPeGSU 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 O0D1rb00UYB0001$8f(D- literal 0 HcmV?d00001 diff --git a/marginalia_nu/src/main/resources/data/smhi/stader.csv b/marginalia_nu/src/main/resources/data/smhi/stader.csv new file mode 100644 index 00000000..0dc649c4 --- /dev/null +++ b/marginalia_nu/src/main/resources/data/smhi/stader.csv @@ -0,0 +1,134 @@ +"Åkersberga",59.47944,18.29967 +"Alby",59.2335,17.8538 +"Alingsås",57.93033,12.53345 +"Ängelholm",56.2428,12.86219 +"Arboga",59.39387,15.83882 +"Årsta",59.2978,18.0514 +"Arvika",59.65528,12.58518 +"Avesta",60.14274,16.16295 +"Bålsta",59.5671,17.52781 +"Boden",65.82518,21.68864 +"Bollnäs",61.34817,16.39464 +"Boo",59.33333,18.28333 +"Borås",57.72101,12.9401 +"Borlänge",60.4858,15.43714 +"Bromma",59.34,17.94 +"Enköping",59.63607,17.07768 +"Eskilstuna",59.36661,16.5077 +"Eslöv",55.83928,13.30393 +"Fagersta",60.00418,15.79316 +"Falkenberg",56.90552,12.49118 +"Falköping",58.17347,13.55068 +"Falun",60.60357,15.62597 +"Finspång",58.70578,15.76739 +"Gävle",60.67452,17.14174 +"Gislaved",57.3044,13.54078 +"Göteborg",57.70716,11.96679 +"Hallstahammar",59.61395,16.22846 +"Halmstad",56.67446,12.85676 +"Handen",59.16809,18.13796 +"Haninge",59.16775,18.14478 +"Härnösand",62.63228,17.93794 +"Hässleholm",56.15905,13.76638 +"Helsingborg",56.04673,12.69437 +"Höganäs",56.19971,12.55795 +"Höllviken",55.40982,12.9558 +"Huddinge",59.23705,17.98192 +"Hudiksvall",61.72897,17.10358 +"Huskvarna",57.78596,14.30214 +"Jakobsberg",59.42268,17.83508 +"Jönköping",57.78145,14.15618 +"Kalmar",56.66157,16.36163 +"Karlshamn",56.1706,14.86188 +"Karlskoga",59.32667,14.52386 +"Karlskrona",56.16156,15.58661 +"Karlstad",59.3793,13.50357 +"Katrineholm",58.99587,16.20721 +"Kävlinge",55.79188,13.11021 +"Kinna",57.50728,12.69463 +"Kiruna",67.85572,20.22513 +"Kista",59.40316,17.94479 +"Köping",59.51404,15.99255 +"Kristianstad",56.03129,14.15242 +"Kristinehamn",59.30978,14.10808 +"Kumla",59.1277,15.14341 +"Kungälv",57.87096,11.98054 +"Kungsbacka",57.48719,12.07612 +"Landskrona",55.8708,12.83016 +"Lerum",57.77051,12.26904 +"Lidingö",59.36667,18.13333 +"Lidköping",58.50517,13.15765 +"Lindome",57.56667,12.08333 +"Linköping",58.41086,15.62157 +"Ljungby",56.83324,13.94082 +"Ludvika",60.14959,15.18776 +"Luleå",65.58415,22.15465 +"Lund",55.70584,13.19321 +"Majorna",57.69195,11.91605 +"Malmö",55.60587,13.00073 +"Mariestad",58.70971,13.82367 +"Märsta",59.62157,17.85476 +"Mjölby",58.32595,15.12365 +"Mölndal",57.6554,12.01378 +"Mölnlycke",57.65893,12.11792 +"Mora",61.00704,14.54316 +"Motala",58.53706,15.03649 +"Nacka",59.31053,18.16372 +"Nässjö",57.65307,14.69676 +"Norrköping",58.59419,16.1826 +"Norrtälje",59.75799,18.70496 +"Nybro",56.74461,15.90714 +"Nyköping",58.753,17.00788 +"Nynäshamn",58.90337,17.94793 +"Onsala",57.42531,12.02903 +"Örebro",59.27412,15.2066 +"Örnsköldsvik",63.29091,18.71525 +"Oskarshamn",57.26455,16.44837 +"Östermalm",59.33879,18.08487 +"Östersund",63.1792,14.63566 +"Oxelösund",58.67057,17.10152 +"Partille",57.7395,12.10642 +"Piteå",65.31717,21.47944 +"Råsunda",59.36667,17.98333 +"Ronneby",56.20999,15.27602 +"Sala",59.91993,16.60655 +"Salem",59.20186,17.76646 +"Sandviken",60.61667,16.76667 +"Segeltorp",59.27597,17.93072 +"Skara",58.38659,13.43836 +"Skellefteå",64.75067,20.95279 +"Skoghall",59.32324,13.46552 +"Skövde",58.39118,13.84506 +"Söderhamn",61.30373,17.05921 +"Södertälje",59.19554,17.62525 +"Sollentuna",59.42804,17.95093 +"Solna",59.36004,18.00086 +"Staffanstorp",55.64277,13.20638 +"Stenungsund",58.07046,11.8181 +"Stockholm",59.33258,18.0649 +"Strängnäs",59.37741,17.03119 +"Sundbyberg",59.36128,17.97114 +"Sundsvall",62.39129,17.3063 +"Täby",59.4439,18.06872 +"Timrå",62.48703,17.3257 +"Torslanda",57.72432,11.77013 +"Tranås",58.03717,14.9782 +"Trelleborg",55.37514,13.15691 +"Trollhättan",58.28365,12.28864 +"Tullinge",59.2,17.9 +"Tumba",59.19858,17.83317 +"Uddevalla",58.34784,11.9424 +"Umeå",63.82842,20.25972 +"Upplands Väsby",59.51839,17.91128 +"Uppsala",59.85882,17.63889 +"Vallentuna",59.53436,18.07758 +"Vänersborg",58.38075,12.3234 +"Varberg",57.10557,12.25078 +"Värnamo",57.18604,14.04001 +"Västerås",59.61617,16.55276 +"Västerhaninge",59.11667,18.1 +"Västervik",57.7584,16.63733 +"Växjö",56.87767,14.80906 +"Vetlanda",57.42887,15.07762 +"Visby",57.64089,18.29602 +"Ystad",55.42966,13.82041 diff --git a/marginalia_nu/src/main/resources/dictionary/en-1000 b/marginalia_nu/src/main/resources/dictionary/en-1000 new file mode 100644 index 00000000..f5f8eda9 --- /dev/null +++ b/marginalia_nu/src/main/resources/dictionary/en-1000 @@ -0,0 +1,1003 @@ +the +of +and +in +to +was +is +for +on +as +with +by +he +that +at +from +his +it +an +were +which +are +this +also +be +had +or +has +first +their +after +its +one +new +but +who +her +not +she +they +have +two +been +other +when +during +all +into +there +time +may +more +school +years +over +only +would +later +most +where +between +some +up +world +city +national +about +such +him +then +made +out +state +three +while +used +university +can +united +under +known +season +many +year +part +became +born +film +these +than +team +no +second +including +states +being +through +before +both +american +south +early +war +history +against +however +family +until +well +since +them +work +life +following +area +people +series +north +name +career +album +music +played +group +district +number +several +high +released +county +de +company +called +will +league +won +four +house +government +each +march +same +game +international +september +january +club +found +june +october +began +located +july +so +west +use +august +now +college +john +station +population +april +public +home +end +november +member +place +general +town +former +december +church +if +age +held +named +system +because +york +took +day +river +around +football +british +line +east +local +any +song +due +along +service +party +best +february +served +did +back +another +based +could +within +received +century +village +built +like +members +building +major +final +show +games +although +include +species +death +band +small +main +left +president +said +published +died +large +last +five +couldn't +what +me +order +st +single +set +third +own +those +education +according +included +long +very +park +still +road +army +division +book +development +among +law +often +french +moved +times +what +community +central +led +english +original +old +son +children +million +different +near +just +top +late +again +water +air +great +center +form +much +research +side +us +art +court +play +down +country +off +even +council +german +street +record +power +established +ii +london +land +cup +having +title +started +support +political +students +award +military +period +came +went +production +white +way +given +island +make +next +role +television +king +region +works +total +championship +using +various +head +office +six +do +player +become +father +list +business +western +produced +director +married +program +association +england +field +worked +election +black +department +joined +announced +created +point +returned +professional +union +written +few +you +young +without +take +described +site +royal +services +radio +together +social +force +northern +per +founded +act +though +society +wrote +further +women +days +lost +continued +design +william +every +version +project +summer +live +men +man +european +we +southern +position +board +india +france +round +railway +open +level +considered +control +opened +run +australia +recorded +important +san +once +video +california +special +win +popular +appeared +match +release +common +battle +areas +hall +event +working +records +james +formed +right +playing +see +average +others +short +similar +teams +elected +george +currently +making +example +awards +construction +story +living +red +originally +debut +race +language +forces +lead +la +signed +developed +modern +appointed +case +addition +police +wife +result +minister +schools +events +america +route +little +lake +canada +himself +songs +current +upon +how +points +rock +present +never +free +science +information +health +training +class +throughout +track +good +media +museum +across +australian +human +census +indian +style +personal +love +germany +available +province +tour +away +eventually +body +despite +eastern +sold +committee +performance +players +features +festival +coach +should +return +taken +sea +seven +centre +followed +designed +performed +official +david +less +gave +months +finished +daughter +process +refer +study +europe +institute +stage +term +range +chief +fire +does +rights +completed +arts +half +remained +largest +mother +character +includes +civil +private +light +leading +reported +network +help +usually +seen +groups +studies +featured +federal +full +episode +thus +academy +night +competition +women's +space +get +instead +china +must +robert +japanese +go +washington +front +uk +directed +tournament +my +thomas +news +books +brother +involved +campaign +independent +either +model +countries +awarded +able +japan +sports +charles +gold +section +capital +kingdom +close +middle +added +fourth +sent +movement +eight +studio +previous +provided +conference +above +soon +today +grand +magazine +canadian +replaced +aircraft +change +films +ten +medical +organization +bank +historic +coast +killed +management +degree +rather +industry +russian +professor +chinese +action +car +senior +systems +green +bridge +technology +almost +shows +big +lower +week +success +writing +base +data +families +post +least +market +primary +female +reached +beginning +valley +ground +type +stated +tv +operations +attack +hospital +saw +approximately +paul +culture +republic +size +previously +decided +introduced +hill +buildings +championships +provide +native +successful +outside +parts +via +theatre +placed +behind +bay +sometimes +los +prior +whose +natural +active +future +scored +italian +africa +spanish +attended +put +listed +brought +regional +structure +units +michael +possible +henry +municipality +higher +start +collection +regular +star +results +square +interest +leader +economic +especially +contract +too +trade +texas +goal +below +winning +officer +foreign +generally +operation +runs +medal +changed +taking +novel +staff +significant +real +standard +far +limited +traditional +african +come +initially +itself +location +commission +roman +me +artist +christian +plays +money +parliament +food +hit +governor +low +defeated +energy +student +strong +towards +notable +child +assembly +owned +catholic +course +commercial +ship +foundation +channel +allowed +represented +property +places +navy +unit +ended +annual +command +paris +km +library +companies +whom +met +ever +activities +spent +plan +numerous +blue +earlier +means +highway +dr +required +musical +additional +practice +bill +noted +mountain +airport +ireland +plant +security +income +issues +associated +manager +artists +related +access +brown +running +peter +individual +richard +older +victory +opening +programs +past +report +sound +press +woman +finally +find +background +policy +our +youth +financial +date +executive +launched +soviet +administration +historical +closed +here +mark +captain +basketball +lived +ran +better +edition +famous +contains +chicago +subsequently +move +selected +already +legal +rural +religious +studied +entered +cultural +lines +person +test +appearance +complete +increased +rest +men's +zealand +secretary +complex +seat +changes +matches +majority +room +loss +terms +review +empire +mission +virginia +angeles +olympics +italy +highest +stadium +becoming +goals +starting +wide +characters +writer +particularly +fact +mostly +mexico +thought +hours +stone +retired +recording +going +give +feature +cross +smith +author +operated +sir +recent +status +chart +theory +greek +islands +caused +entire +got +remains +nine +engine +source +genus +forced +issue +singles +evidence +meeting +congress +port +variety +pennsylvania +forest +passed +lord +uses +particular +key +supported +word +create +relationship +overall +hand +democratic +certain +castle +biography +nature +mary +names +fort +parish +decision +serving +score +cover +wales +singer +need +material +shown +florida +upper +referred +larger +marriage +length +leaving +weeks +movie +raised +rate +justice +fall +always +minutes +junior +competed +stations +turn +irish +temple +cases +era +individuals +township +claimed +friends +van \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/dictionary/en-stopwords b/marginalia_nu/src/main/resources/dictionary/en-stopwords new file mode 100644 index 00000000..cdcd342d --- /dev/null +++ b/marginalia_nu/src/main/resources/dictionary/en-stopwords @@ -0,0 +1,181 @@ +i +a +e.g +i.e +the +of +and +in +to +was +is +for +on +as +with +by +he +that +at +from +his +it +an +were +we've +we're +which +are +this +also +be +had +or +has +first +their +after +its +new +but +who +her +not +she +she's +they +have +been +other +when +during +all +into +there +time +may +more +school +years +over +only +would +later +most +where +between +some +up +city +about +such +him +then +made +out +state +three +while +used +can +under +known +many +year +part +became +these +than +team +no +second +including +being +through +before +both +however +how +until +well +since +them +de +each +same +found +so +use +now +end +if +age +day +any +due +did +own +led +off +do +you +you're +young +without +take +described +site +royal +services +radio +together +social +force +northern +per +we +my +want +your +seem +else's +don't +me +couldn't +what +me +doesn't +can't +isn't +i've +it's +it +i'm +1 +2 +3 +4 +5 +6 +7 +8 +9 +. +.. +... +.... +..... +...... +....... +........ +......... +.......... +will +us +much +our +what +what's +often +few +lot \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/dictionary/en-words b/marginalia_nu/src/main/resources/dictionary/en-words new file mode 100644 index 00000000..01807612 --- /dev/null +++ b/marginalia_nu/src/main/resources/dictionary/en-words @@ -0,0 +1,102401 @@ +A +A's +AMD +AMD's +AOL +AOL's +AWS +AWS's +Aachen +Aachen's +Aaliyah +Aaliyah's +Aaron +Aaron's +Abbas +Abbas's +Abbasid +Abbasid's +Abbott +Abbott's +Abby +Abby's +Abdul +Abdul's +Abe +Abe's +Abel +Abel's +Abelard +Abelard's +Abelson +Abelson's +Aberdeen +Aberdeen's +Abernathy +Abernathy's +Abidjan +Abidjan's +Abigail +Abigail's +Abilene +Abilene's +Abner +Abner's +Abraham +Abraham's +Abram +Abram's +Abrams +Abrams's +Absalom +Absalom's +Abuja +Abuja's +Abyssinia +Abyssinia's +Abyssinian +Abyssinian's +Ac +Ac's +Acadia +Acadia's +Acapulco +Acapulco's +Accenture +Accenture's +Accra +Accra's +Acevedo +Acevedo's +Achaean +Achaean's +Achebe +Achebe's +Achernar +Achernar's +Acheson +Acheson's +Achilles +Achilles's +Aconcagua +Aconcagua's +Acosta +Acosta's +Acropolis +Acrux +Acrux's +Actaeon +Actaeon's +Acton +Acton's +Acts +Acts's +Acuff +Acuff's +Ada +Ada's +Adam +Adam's +Adams +Adams's +Adan +Adan's +Adana +Adana's +Adar +Adar's +Addams +Addams's +Adderley +Adderley's +Addie +Addie's +Addison +Addison's +Adela +Adela's +Adelaide +Adelaide's +Adele +Adele's +Adeline +Adeline's +Aden +Aden's +Adenauer +Adenauer's +Adhara +Adhara's +Adidas +Adidas's +Adirondack +Adirondack's +Adirondacks +Adirondacks's +Adkins +Adkins's +Adler +Adler's +Adolf +Adolf's +Adolfo +Adolfo's +Adolph +Adolph's +Adonis +Adonis's +Adonises +Adrian +Adrian's +Adriana +Adriana's +Adriatic +Adriatic's +Adrienne +Adrienne's +Advent +Advent's +Adventist +Adventist's +Advents +Advil +Advil's +Aegean +Aegean's +Aelfric +Aelfric's +Aeneas +Aeneas's +Aeneid +Aeneid's +Aeolus +Aeolus's +Aeroflot +Aeroflot's +Aeschylus +Aeschylus's +Aesculapius +Aesculapius's +Aesop +Aesop's +Afghan +Afghan's +Afghani +Afghani's +Afghanistan +Afghanistan's +Afghans +Africa +Africa's +African +African's +Africans +Afrikaans +Afrikaans's +Afrikaner +Afrikaner's +Afrikaners +Afro +Afro's +Afrocentrism +Afrocentrism's +Afros +Ag +Ag's +Agamemnon +Agamemnon's +Agassi +Agassi's +Agassiz +Agassiz's +Agatha +Agatha's +Aggie +Aggie's +Aglaia +Aglaia's +Agnes +Agnes's +Agnew +Agnew's +Agni +Agni's +Agra +Agra's +Agricola +Agricola's +Agrippa +Agrippa's +Agrippina +Agrippina's +Aguilar +Aguilar's +Aguinaldo +Aguinaldo's +Aguirre +Aguirre's +Agustin +Agustin's +Ahab +Ahab's +Ahmad +Ahmad's +Ahmadabad +Ahmadabad's +Ahmadinejad +Ahmadinejad's +Ahmed +Ahmed's +Ahriman +Ahriman's +Aida +Aida's +Aiken +Aiken's +Aileen +Aileen's +Aimee +Aimee's +Ainu +Ainu's +Airedale +Airedale's +Airedales +Aires +Aires's +Aisha +Aisha's +Ajax +Ajax's +Akbar +Akbar's +Akhmatova +Akhmatova's +Akihito +Akihito's +Akita +Akita's +Akiva +Akiva's +Akkad +Akkad's +Akron +Akron's +Al +Al's +Alabama +Alabama's +Alabaman +Alabaman's +Alabamans +Alabamian +Alabamian's +Alabamians +Aladdin +Aladdin's +Alamo +Alamo's +Alamogordo +Alamogordo's +Alan +Alan's +Alana +Alana's +Alar +Alar's +Alaric +Alaric's +Alaska +Alaska's +Alaskan +Alaskan's +Alaskans +Alba +Alba's +Albania +Albania's +Albanian +Albanian's +Albanians +Albany +Albany's +Albee +Albee's +Alberio +Alberio's +Albert +Albert's +Alberta +Alberta's +Alberto +Alberto's +Albigensian +Albigensian's +Albion +Albion's +Albireo +Albireo's +Albuquerque +Albuquerque's +Alcatraz +Alcatraz's +Alcestis +Alcestis's +Alcibiades +Alcibiades's +Alcindor +Alcindor's +Alcmena +Alcmena's +Alcoa +Alcoa's +Alcott +Alcott's +Alcuin +Alcuin's +Alcyone +Alcyone's +Aldan +Aldan's +Aldebaran +Aldebaran's +Alden +Alden's +Alderamin +Alderamin's +Aldo +Aldo's +Aldrin +Aldrin's +Alec +Alec's +Aleichem +Aleichem's +Alejandra +Alejandra's +Alejandro +Alejandro's +Alembert +Alembert's +Aleppo +Aleppo's +Aleut +Aleut's +Aleutian +Aleutian's +Alex +Alex's +Alexander +Alexander's +Alexandra +Alexandra's +Alexandria +Alexandria's +Alexei +Alexei's +Alexis +Alexis's +Alfonso +Alfonso's +Alfonzo +Alfonzo's +Alford +Alford's +Alfred +Alfred's +Alfreda +Alfreda's +Alfredo +Alfredo's +Algenib +Algenib's +Alger +Alger's +Algeria +Algeria's +Algerian +Algerian's +Algerians +Algieba +Algieba's +Algiers +Algiers's +Algol +Algol's +Algonquian +Algonquian's +Algonquians +Algonquin +Algonquin's +Alhambra +Alhambra's +Alhena +Alhena's +Ali +Ali's +Alice +Alice's +Alicia +Alicia's +Alighieri +Alighieri's +Aline +Aline's +Alioth +Alioth's +Alisa +Alisa's +Alisha +Alisha's +Alison +Alison's +Alissa +Alissa's +Alistair +Alistair's +Alkaid +Alkaid's +Allah +Allah's +Allahabad +Allahabad's +Allan +Allan's +Alleghenies +Alleghenies's +Allegheny +Allegheny's +Allegra +Allegra's +Allen +Allen's +Allende +Allende's +Allentown +Allentown's +Allie +Allie's +Allison +Allison's +Allstate +Allstate's +Allyson +Allyson's +Alma +Alma's +Almach +Almach's +Almaty +Almaty's +Almighty +Almighty's +Almohad +Almohad's +Almoravid +Almoravid's +Alnilam +Alnilam's +Alnitak +Alnitak's +Alonzo +Alonzo's +Alpert +Alpert's +Alphard +Alphard's +Alphecca +Alphecca's +Alpheratz +Alpheratz's +Alphonse +Alphonse's +Alphonso +Alphonso's +Alpine +Alpine's +Alpo +Alpo's +Alps +Alps's +Alsace +Alsace's +Alsatian +Alsatian's +Alsop +Alsop's +Alston +Alston's +Altaba +Altaba's +Altai +Altai's +Altaic +Altaic's +Altair +Altair's +Altamira +Altamira's +Althea +Althea's +Altiplano +Altiplano's +Altman +Altman's +Altoids +Altoids's +Alton +Alton's +Aludra +Aludra's +Alva +Alva's +Alvarado +Alvarado's +Alvarez +Alvarez's +Alvaro +Alvaro's +Alvin +Alvin's +Alyce +Alyce's +Alyson +Alyson's +Alyssa +Alyssa's +Alzheimer +Alzheimer's +Am +Am's +Amadeus +Amadeus's +Amado +Amado's +Amalia +Amalia's +Amanda +Amanda's +Amarillo +Amarillo's +Amaru +Amaru's +Amaterasu +Amaterasu's +Amati +Amati's +Amazon +Amazon's +Amazons +Amber +Amber's +Amelia +Amelia's +Amenhotep +Amenhotep's +Amerasian +Amerasian's +America +America's +American +American's +Americana +Americana's +Americanism +Americanism's +Americanisms +Americanization +Americanization's +Americanizations +Americanize +Americanized +Americanizes +Americanizing +Americans +Americas +Amerind +Amerind's +Amerindian +Amerindian's +Amerindians +Amerinds +Ameslan +Ameslan's +Amgen +Amgen's +Amharic +Amharic's +Amherst +Amherst's +Amie +Amie's +Amiga +Amiga's +Amish +Amish's +Amman +Amman's +Amoco +Amoco's +Amos +Amos's +Amparo +Amparo's +Ampere +Ampere's +Amritsar +Amritsar's +Amsterdam +Amsterdam's +Amtrak +Amtrak's +Amundsen +Amundsen's +Amur +Amur's +Amway +Amway's +Amy +Amy's +Ana +Ana's +Anabaptist +Anabaptist's +Anabel +Anabel's +Anacin +Anacin's +Anacreon +Anacreon's +Anaheim +Anaheim's +Analects +Analects's +Ananias +Ananias's +Anasazi +Anasazi's +Anastasia +Anastasia's +Anatole +Anatole's +Anatolia +Anatolia's +Anatolian +Anatolian's +Anaxagoras +Anaxagoras's +Anchorage +Anchorage's +Andalusia +Andalusia's +Andalusian +Andalusian's +Andaman +Andaman's +Andean +Andean's +Andersen +Andersen's +Anderson +Anderson's +Andes +Andes's +Andorra +Andorra's +Andre +Andre's +Andrea +Andrea's +Andrei +Andrei's +Andres +Andres's +Andretti +Andretti's +Andrew +Andrew's +Andrews +Andrews's +Andrianampoinimerina +Andrianampoinimerina's +Android +Android's +Andromache +Andromache's +Andromeda +Andromeda's +Andropov +Andropov's +Andy +Andy's +Angara +Angara's +Angel +Angel's +Angela +Angela's +Angeles +Angeles's +Angelia +Angelia's +Angelica +Angelica's +Angelico +Angelico's +Angelina +Angelina's +Angeline +Angeline's +Angelique +Angelique's +Angelita +Angelita's +Angelo +Angelo's +Angelou +Angelou's +Angevin +Angevin's +Angie +Angie's +Angkor +Angkor's +Anglia +Anglia's +Anglican +Anglican's +Anglicanism +Anglicanism's +Anglicanisms +Anglicans +Anglicize +Anglo +Anglo's +Anglophile +Anglophile's +Angola +Angola's +Angolan +Angolan's +Angolans +Angora +Angora's +Angoras +Anguilla +Anguilla's +Angus +Angus's +Aniakchak +Aniakchak's +Anibal +Anibal's +Anita +Anita's +Ankara +Ankara's +Ann +Ann's +Anna +Anna's +Annabel +Annabel's +Annabelle +Annabelle's +Annam +Annam's +Annapolis +Annapolis's +Annapurna +Annapurna's +Anne +Anne's +Annette +Annette's +Annie +Annie's +Annmarie +Annmarie's +Anouilh +Anouilh's +Anselm +Anselm's +Anselmo +Anselmo's +Anshan +Anshan's +Antaeus +Antaeus's +Antananarivo +Antananarivo's +Antarctic +Antarctic's +Antarctica +Antarctica's +Antares +Antares's +Anthony +Anthony's +Anthropocene +Antichrist +Antichrist's +Antichrists +Antietam +Antietam's +Antigone +Antigone's +Antigua +Antigua's +Antilles +Antilles's +Antioch +Antioch's +Antipas +Antipas's +Antofagasta +Antofagasta's +Antoine +Antoine's +Antoinette +Antoinette's +Anton +Anton's +Antone +Antone's +Antonia +Antonia's +Antoninus +Antoninus's +Antonio +Antonio's +Antonius +Antonius's +Antony +Antony's +Antwan +Antwan's +Antwerp +Antwerp's +Anubis +Anubis's +Anzac +Anzac's +Apache +Apache's +Apaches +Apalachicola +Apalachicola's +Apatosaurus +Apennines +Apennines's +Aphrodite +Aphrodite's +Apia +Apia's +Apocrypha +Apocrypha's +Apollinaire +Apollinaire's +Apollo +Apollo's +Apollonian +Apollonian's +Apollos +Appalachia +Appalachia's +Appalachian +Appalachian's +Appalachians +Appalachians's +Appaloosa +Appaloosa's +Apple +Apple's +Appleseed +Appleseed's +Appleton +Appleton's +Appomattox +Appomattox's +Apr +Apr's +April +April's +Aprils +Apuleius +Apuleius's +Aquafresh +Aquafresh's +Aquarius +Aquarius's +Aquariuses +Aquila +Aquila's +Aquinas +Aquinas's +Aquino +Aquino's +Aquitaine +Aquitaine's +Ara +Ara's +Arab +Arab's +Arabia +Arabia's +Arabian +Arabian's +Arabians +Arabic +Arabic's +Arabs +Araby +Araby's +Araceli +Araceli's +Arafat +Arafat's +Araguaya +Araguaya's +Aral +Aral's +Aramaic +Aramaic's +Aramco +Aramco's +Arapaho +Arapaho's +Ararat +Ararat's +Araucanian +Araucanian's +Arawak +Arawak's +Arawakan +Arawakan's +Arbitron +Arbitron's +Arcadia +Arcadia's +Arcadian +Arcadian's +Archean +Archean's +Archibald +Archibald's +Archie +Archie's +Archimedes +Archimedes's +Arctic +Arctic's +Arcturus +Arcturus's +Arden +Arden's +Arduino +Arduino's +Arequipa +Arequipa's +Ares +Ares's +Argentina +Argentina's +Argentine +Argentine's +Argentinian +Argentinian's +Argentinians +Argo +Argo's +Argonaut +Argonaut's +Argonne +Argonne's +Argos +Argos's +Argus +Argus's +Ariadne +Ariadne's +Arianism +Arianism's +Ariel +Ariel's +Aries +Aries's +Arieses +Ariosto +Ariosto's +Aristarchus +Aristarchus's +Aristides +Aristides's +Aristophanes +Aristophanes's +Aristotelian +Aristotelian's +Aristotle +Aristotle's +Arius +Arius's +Arizona +Arizona's +Arizonan +Arizonan's +Arizonans +Arizonian +Arizonian's +Arizonians +Arjuna +Arjuna's +Arkansan +Arkansan's +Arkansas +Arkansas's +Arkhangelsk +Arkhangelsk's +Arkwright +Arkwright's +Arlene +Arlene's +Arline +Arline's +Arlington +Arlington's +Armageddon +Armageddon's +Armageddons +Armagnac +Armagnac's +Armand +Armand's +Armando +Armando's +Armani +Armani's +Armenia +Armenia's +Armenian +Armenian's +Armenians +Arminius +Arminius's +Armonk +Armonk's +Armour +Armour's +Armstrong +Armstrong's +Arneb +Arneb's +Arnhem +Arnhem's +Arno +Arno's +Arnold +Arnold's +Arnulfo +Arnulfo's +Aron +Aron's +Arrhenius +Arrhenius's +Arron +Arron's +Art +Art's +Artaxerxes +Artaxerxes's +Artemis +Artemis's +Arthur +Arthur's +Arthurian +Arthurian's +Artie +Artie's +Arturo +Arturo's +Aruba +Aruba's +Aryan +Aryan's +Aryans +As +As's +Asama +Asama's +Ascella +Ascella's +Asgard +Asgard's +Ashanti +Ashanti's +Ashcroft +Ashcroft's +Ashe +Ashe's +Ashikaga +Ashikaga's +Ashkenazim +Ashkenazim's +Ashkhabad +Ashkhabad's +Ashlee +Ashlee's +Ashley +Ashley's +Ashmolean +Ashmolean's +Ashurbanipal +Ashurbanipal's +Asia +Asia's +Asiago +Asian +Asian's +Asians +Asiatic +Asiatic's +Asiatics +Asimov +Asimov's +Asmara +Asmara's +Asoka +Asoka's +Aspell +Aspell's +Aspen +Aspen's +Asperger +Asperger's +Aspidiske +Aspidiske's +Asquith +Asquith's +Assad +Assad's +Assam +Assam's +Assamese +Assamese's +Assisi +Assisi's +Assyria +Assyria's +Assyrian +Assyrian's +Assyrians +Astaire +Astaire's +Astana +Astana's +Astarte +Astarte's +Aston +Aston's +Astor +Astor's +Astoria +Astoria's +Astrakhan +Astrakhan's +AstroTurf +AstroTurf's +Asturias +Asturias's +Asunción +Asunción's +Aswan +Aswan's +Atacama +Atacama's +Atahualpa +Atahualpa's +Atalanta +Atalanta's +Atari +Atari's +Atatürk +Atatürk's +Athabasca +Athabasca's +Athabascan +Athabascan's +Athena +Athena's +Athenian +Athenian's +Athenians +Athens +Athens's +Atkins +Atkins's +Atkinson +Atkinson's +Atlanta +Atlanta's +Atlantes +Atlantic +Atlantic's +Atlantis +Atlantis's +Atlas +Atlas's +Atlases +Atman +Atman's +Atreus +Atreus's +Atria +Atria's +Atropos +Atropos's +Ats +Attic +Attic's +Attica +Attica's +Attila +Attila's +Attlee +Attlee's +Attucks +Attucks's +Atwood +Atwood's +Au +Au's +Aubrey +Aubrey's +Auckland +Auckland's +Auden +Auden's +Audi +Audi's +Audion +Audion's +Audra +Audra's +Audrey +Audrey's +Audubon +Audubon's +Aug +Aug's +Augean +Augean's +Augsburg +Augsburg's +August +August's +Augusta +Augusta's +Augustan +Augustan's +Augustine +Augustine's +Augusts +Augustus +Augustus's +Aurangzeb +Aurangzeb's +Aurelia +Aurelia's +Aurelio +Aurelio's +Aurelius +Aurelius's +Aureomycin +Aureomycin's +Auriga +Auriga's +Aurora +Aurora's +Auschwitz +Auschwitz's +Aussie +Aussie's +Aussies +Austen +Austen's +Austerlitz +Austerlitz's +Austin +Austin's +Austins +Australasia +Australasia's +Australia +Australia's +Australian +Australian's +Australians +Australoid +Australoid's +Australopithecus +Australopithecus's +Austria +Austria's +Austrian +Austrian's +Austrians +Austronesian +Austronesian's +Autumn +Autumn's +Ava +Ava's +Avalon +Avalon's +Aventine +Aventine's +Avernus +Avernus's +Averroes +Averroes's +Avery +Avery's +Avesta +Avesta's +Avicenna +Avicenna's +Avignon +Avignon's +Avila +Avila's +Avior +Avior's +Avis +Avis's +Avogadro +Avogadro's +Avon +Avon's +Axum +Axum's +Ayala +Ayala's +Ayers +Ayers's +Aymara +Aymara's +Ayrshire +Ayrshire's +Ayurveda +Ayurveda's +Ayyubid +Ayyubid's +Azana +Azana's +Azania +Azania's +Azazel +Azazel's +Azerbaijan +Azerbaijan's +Azerbaijani +Azerbaijani's +Azores +Azores's +Azov +Azov's +Aztec +Aztec's +Aztecan +Aztecan's +Aztecs +Aztlan +Aztlan's +B +B's +BBB +BBB's +BMW +BMW's +BP +BP's +BSD +BSD's +Ba +Ba's +Baal +Baal's +Baath +Baath's +Baathist +Baathist's +Babar +Babar's +Babbage +Babbage's +Babbitt +Babbitt's +Babel +Babel's +Babels +Babur +Babur's +Babylon +Babylon's +Babylonian +Babylonian's +Babylons +Bacall +Bacall's +Bacardi +Bacardi's +Bacchanalia +Bacchanalia's +Bacchus +Bacchus's +Bach +Bach's +Backus +Backus's +Bacon +Bacon's +Bactria +Bactria's +Baden +Baden's +Badlands +Badlands's +Baedeker +Baedeker's +Baez +Baez's +Baffin +Baffin's +Baggies +Baggies's +Baghdad +Baghdad's +Baguio +Baguio's +Baha'i +Baha'i's +Baha'ullah +Baha'ullah's +Bahama +Bahama's +Bahamas +Bahamas's +Bahamian +Bahamian's +Bahamians +Bahia +Bahia's +Bahrain +Bahrain's +Baidu +Baidu's +Baikal +Baikal's +Bailey +Bailey's +Baird +Baird's +Bakelite +Bakelite's +Baker +Baker's +Bakersfield +Bakersfield's +Baku +Baku's +Bakunin +Bakunin's +Balanchine +Balanchine's +Balaton +Balaton's +Balboa +Balboa's +Balder +Balder's +Baldwin +Baldwin's +Balearic +Balearic's +Balfour +Balfour's +Bali +Bali's +Balinese +Balinese's +Balkan +Balkan's +Balkans +Balkans's +Balkhash +Balkhash's +Ball +Ball's +Ballard +Ballard's +Balthazar +Balthazar's +Baltic +Baltic's +Baltimore +Baltimore's +Baluchistan +Baluchistan's +Balzac +Balzac's +Bamako +Bamako's +Bambi +Bambi's +Banach +Banach's +Bancroft +Bancroft's +Bandung +Bandung's +Bangalore +Bangalore's +Bangkok +Bangkok's +Bangladesh +Bangladesh's +Bangladeshi +Bangladeshi's +Bangladeshis +Bangor +Bangor's +Bangui +Bangui's +Banjarmasin +Banjarmasin's +Banjul +Banjul's +Banks +Banks's +Banneker +Banneker's +Bannister +Bannister's +Banting +Banting's +Bantu +Bantu's +Bantus +Baotou +Baotou's +Baptist +Baptist's +Baptiste +Baptiste's +Baptists +Barabbas +Barabbas's +Barack +Barack's +Barbadian +Barbadian's +Barbadians +Barbados +Barbados's +Barbara +Barbara's +Barbarella +Barbarella's +Barbarossa +Barbarossa's +Barbary +Barbary's +Barber +Barber's +Barbie +Barbie's +Barbour +Barbour's +Barbra +Barbra's +Barbuda +Barbuda's +Barcelona +Barcelona's +Barclay +Barclay's +Bardeen +Bardeen's +Barents +Barents's +Barker +Barker's +Barkley +Barkley's +Barlow +Barlow's +Barnabas +Barnabas's +Barnaby +Barnaby's +Barnard +Barnard's +Barnaul +Barnaul's +Barnes +Barnes's +Barnett +Barnett's +Barney +Barney's +Barnum +Barnum's +Baroda +Baroda's +Barquisimeto +Barquisimeto's +Barr +Barr's +Barranquilla +Barranquilla's +Barrera +Barrera's +Barrett +Barrett's +Barrie +Barrie's +Barron +Barron's +Barry +Barry's +Barrymore +Barrymore's +Barth +Barth's +Bartholdi +Bartholdi's +Bartholomew +Bartholomew's +Bartlett +Bartlett's +Barton +Barton's +Bartók +Bartók's +Baruch +Baruch's +Baryshnikov +Baryshnikov's +Basel +Basel's +Basho +Basho's +Basie +Basie's +Basil +Basil's +Basque +Basque's +Basques +Basra +Basra's +Bass +Bass's +Basseterre +Basseterre's +Bastille +Bastille's +Bataan +Bataan's +Bates +Bates's +Bathsheba +Bathsheba's +Batista +Batista's +Batman +Batman's +Battle +Battle's +Batu +Batu's +Baudelaire +Baudelaire's +Baudouin +Baudouin's +Bauer +Bauer's +Bauhaus +Bauhaus's +Baum +Baum's +Bavaria +Bavaria's +Bavarian +Bavarian's +Baxter +Baxter's +Bayer +Bayer's +Bayes +Bayes's +Bayesian +Bayesian's +Bayeux +Bayeux's +Baylor +Baylor's +Bayonne +Bayonne's +Bayreuth +Bayreuth's +Baywatch +Baywatch's +Beach +Beach's +Beadle +Beadle's +Bean +Bean's +Beard +Beard's +Beardmore +Beardmore's +Beardsley +Beardsley's +Bearnaise +Bearnaise's +Beasley +Beasley's +Beatlemania +Beatlemania's +Beatles +Beatles's +Beatrice +Beatrice's +Beatrix +Beatrix's +Beatriz +Beatriz's +Beau +Beau's +Beaufort +Beaufort's +Beaujolais +Beaujolais's +Beaumarchais +Beaumarchais's +Beaumont +Beaumont's +Beauregard +Beauregard's +Beauvoir +Beauvoir's +Bechtel +Bechtel's +Beck +Beck's +Becker +Becker's +Becket +Becket's +Beckett +Beckett's +Becky +Becky's +Becquerel +Becquerel's +Bede +Bede's +Bedouin +Bedouin's +Bedouins +Beebe +Beebe's +Beecher +Beecher's +Beefaroni +Beefaroni's +Beelzebub +Beelzebub's +Beerbohm +Beerbohm's +Beethoven +Beethoven's +Beeton +Beeton's +Begin +Begin's +Behan +Behan's +Behring +Behring's +Beiderbecke +Beiderbecke's +Beijing +Beijing's +Beirut +Beirut's +Bekesy +Bekesy's +Bela +Bela's +Belarus +Belarus's +Belau +Belau's +Belem +Belem's +Belfast +Belfast's +Belgian +Belgian's +Belgians +Belgium +Belgium's +Belgrade +Belgrade's +Belinda +Belinda's +Belize +Belize's +Bell +Bell's +Bella +Bella's +Bellamy +Bellamy's +Bellatrix +Bellatrix's +Belleek +Belleek's +Bellini +Bellini's +Bellow +Bellow's +Belmont +Belmont's +Belmopan +Belmopan's +Belshazzar +Belshazzar's +Beltane +Beltane's +Belushi +Belushi's +Ben +Ben's +Benacerraf +Benacerraf's +Benares +Benares's +Benchley +Benchley's +Bender +Bender's +Bendix +Bendix's +Benedict +Benedict's +Benedictine +Benedictine's +Benelux +Benelux's +Benet +Benet's +Benetton +Benetton's +Bengal +Bengal's +Bengali +Bengali's +Benghazi +Benghazi's +Benin +Benin's +Benita +Benita's +Benito +Benito's +Benjamin +Benjamin's +Bennett +Bennett's +Bennie +Bennie's +Benny +Benny's +Benson +Benson's +Bentham +Bentham's +Bentley +Bentley's +Benton +Benton's +Benz +Benz's +Benzedrine +Benzedrine's +Beowulf +Beowulf's +Berber +Berber's +Berbers +Berenice +Berenice's +Beretta +Beretta's +Berg +Berg's +Bergen +Bergen's +Berger +Berger's +Bergerac +Bergerac's +Bergman +Bergman's +Bergson +Bergson's +Beria +Beria's +Bering +Bering's +Berkeley +Berkeley's +Berkshire +Berkshire's +Berkshires +Berkshires's +Berle +Berle's +Berlin +Berlin's +Berliner +Berliner's +Berlins +Berlioz +Berlioz's +Berlitz +Berlitz's +Bermuda +Bermuda's +Bermudas +Bern +Bern's +Bernadette +Bernadette's +Bernadine +Bernadine's +Bernanke +Bernanke's +Bernard +Bernard's +Bernardo +Bernardo's +Bernays +Bernays's +Bernbach +Bernbach's +Berne +Berne's +Bernhardt +Bernhardt's +Bernice +Bernice's +Bernie +Bernie's +Bernini +Bernini's +Bernoulli +Bernoulli's +Bernstein +Bernstein's +Berra +Berra's +Berry +Berry's +Bert +Bert's +Berta +Berta's +Bertelsmann +Bertelsmann's +Bertha +Bertha's +Bertie +Bertie's +Bertillon +Bertillon's +Bertram +Bertram's +Bertrand +Bertrand's +Beryl +Beryl's +Berzelius +Berzelius's +Bess +Bess's +Bessel +Bessel's +Bessemer +Bessemer's +Bessie +Bessie's +Best +Best's +Betelgeuse +Betelgeuse's +Beth +Beth's +Bethany +Bethany's +Bethe +Bethe's +Bethesda +Bethesda's +Bethlehem +Bethlehem's +Bethune +Bethune's +Betsy +Betsy's +Bette +Bette's +Bettie +Bettie's +Betty +Betty's +Bettye +Bettye's +Beulah +Beulah's +Beverley +Beverley's +Beverly +Beverly's +Beyer +Beyer's +Bhopal +Bhopal's +Bhutan +Bhutan's +Bhutto +Bhutto's +Bialystok +Bialystok's +Bianca +Bianca's +Bible +Bible's +Bibles +Biblical +Biblical's +Bic +Bic's +Biddle +Biddle's +Biden +Biden's +Bierce +Bierce's +BigQuery +BigQuery's +Bigfoot +Bigfoot's +Biggles +Biggles's +Biko +Biko's +Bilbao +Bilbao's +Bilbo +Bilbo's +Bill +Bill's +Billie +Billie's +Billings +Billings's +Billy +Billy's +Bimini +Bimini's +Biogen +Biogen's +Bioko +Bioko's +Bird +Bird's +Birdseye +Birdseye's +Birkenstock +Birkenstock's +Birmingham +Birmingham's +Biro +Biro's +Biscay +Biscay's +Biscayne +Biscayne's +Bishkek +Bishkek's +Bishop +Bishop's +Bismarck +Bismarck's +Bismark +Bismark's +Bisquick +Bisquick's +Bissau +Bissau's +BitTorrent +BitTorrent's +Bizet +Bizet's +Bjerknes +Bjerknes's +Bjork +Bjork's +Black +Black's +Blackbeard +Blackbeard's +Blackburn +Blackburn's +Blackfoot +Blackfoot's +Blacks +Blackshirt +Blackshirt's +Blackstone +Blackstone's +Blackwell +Blackwell's +Blaine +Blaine's +Blair +Blair's +Blake +Blake's +Blanca +Blanca's +Blanchard +Blanchard's +Blanche +Blanche's +Blankenship +Blankenship's +Blantyre +Blantyre's +Blatz +Blatz's +Blavatsky +Blavatsky's +Blenheim +Blenheim's +Blevins +Blevins's +Bligh +Bligh's +Bloch +Bloch's +Blockbuster +Blockbuster's +Bloemfontein +Bloemfontein's +Blondel +Blondel's +Blondie +Blondie's +Bloom +Bloom's +Bloomer +Bloomer's +Bloomfield +Bloomfield's +Bloomingdale +Bloomingdale's +Bloomsbury +Bloomsbury's +Blu +Blucher +Blucher's +Bluebeard +Bluebeard's +Bluetooth +Bluetooth's +Blythe +Blythe's +Boas +Boas's +Bob +Bob's +Bobbi +Bobbi's +Bobbie +Bobbie's +Bobbitt +Bobbitt's +Bobby +Bobby's +Boccaccio +Boccaccio's +Bodhidharma +Bodhidharma's +Bodhisattva +Bodhisattva's +Boeing +Boeing's +Boeotia +Boeotia's +Boeotian +Boeotian's +Boer +Boer's +Boers +Boethius +Boethius's +Bogart +Bogart's +Bogotá +Bogotá's +Bohemia +Bohemia's +Bohemian +Bohemian's +Bohemians +Bohr +Bohr's +Boise +Boise's +Bojangles +Bojangles's +Boleyn +Boleyn's +Bolivar +Bolivar's +Bolivia +Bolivia's +Bolivian +Bolivian's +Bolivians +Bollywood +Bollywood's +Bologna +Bologna's +Bolshevik +Bolshevik's +Bolsheviks +Bolshevism +Bolshevism's +Bolshevist +Bolshevist's +Bolshoi +Bolshoi's +Bolton +Bolton's +Boltzmann +Boltzmann's +Bombay +Bombay's +Bonaparte +Bonaparte's +Bonaventure +Bonaventure's +Bond +Bond's +Bonhoeffer +Bonhoeffer's +Boniface +Boniface's +Bonita +Bonita's +Bonn +Bonn's +Bonner +Bonner's +Bonneville +Bonneville's +Bonnie +Bonnie's +Bono +Bono's +Booker +Booker's +Boole +Boole's +Boolean +Boolean's +Boone +Boone's +Booth +Booth's +Bordeaux +Bordeaux's +Borden +Borden's +Bordon +Bordon's +Boreas +Boreas's +Borg +Borg's +Borges +Borges's +Borgia +Borgia's +Borglum +Borglum's +Boris +Boris's +Bork +Bork's +Borlaug +Borlaug's +Born +Born's +Borneo +Borneo's +Borobudur +Borobudur's +Borodin +Borodin's +Boru +Boru's +Bosch +Bosch's +Bose +Bose's +Bosnia +Bosnia's +Bosporus +Bosporus's +Boston +Boston's +Bostonian +Bostonian's +Bostons +Boswell +Boswell's +Botox +Botswana +Botswana's +Botticelli +Botticelli's +Boulder +Boulder's +Boulez +Boulez's +Bourbaki +Bourbaki's +Bourbon +Bourbon's +Bournemouth +Bournemouth's +Bovary +Bovary's +Bowditch +Bowditch's +Bowell +Bowell's +Bowen +Bowen's +Bowers +Bowers's +Bowery +Bowery's +Bowie +Bowie's +Bowman +Bowman's +Boyd +Boyd's +Boyer +Boyer's +Boyle +Boyle's +Boötes +Boötes's +Brad +Brad's +Bradbury +Bradbury's +Braddock +Braddock's +Bradford +Bradford's +Bradley +Bradley's +Bradly +Bradly's +Bradshaw +Bradshaw's +Bradstreet +Bradstreet's +Brady +Brady's +Bragg +Bragg's +Brahe +Brahe's +Brahma +Brahma's +Brahmagupta +Brahmagupta's +Brahman +Brahman's +Brahmanism +Brahmanism's +Brahmanisms +Brahmans +Brahmaputra +Brahmaputra's +Brahmas +Brahmin +Brahmin's +Brahmins +Brahms +Brahms's +Braille +Braille's +Brailles +Brain +Brain's +Brampton +Brampton's +Bran +Bran's +Branch +Branch's +Brandeis +Brandeis's +Branden +Branden's +Brandenburg +Brandenburg's +Brandi +Brandi's +Brandie +Brandie's +Brando +Brando's +Brandon +Brandon's +Brandt +Brandt's +Brandy +Brandy's +Brant +Brant's +Braque +Braque's +Brasilia +Brasilia's +Bratislava +Bratislava's +Brattain +Brattain's +Bray +Bray's +Brazil +Brazil's +Brazilian +Brazilian's +Brazilians +Brazos +Brazos's +Brazzaville +Brazzaville's +Breakspear +Breakspear's +Brecht +Brecht's +Breckenridge +Breckenridge's +Bremen +Bremen's +Brenda +Brenda's +Brendan +Brendan's +Brennan +Brennan's +Brenner +Brenner's +Brent +Brent's +Brenton +Brenton's +Brest +Brest's +Bret +Bret's +Breton +Breton's +Brett +Brett's +Brewer +Brewer's +Brewster +Brewster's +Brexit +Brezhnev +Brezhnev's +Brian +Brian's +Briana +Briana's +Brianna +Brianna's +Brice +Brice's +Bridalveil +Bridalveil's +Bridgeport +Bridgeport's +Bridger +Bridger's +Bridges +Bridges's +Bridget +Bridget's +Bridgetown +Bridgetown's +Bridgett +Bridgett's +Bridgette +Bridgette's +Bridgman +Bridgman's +Brie +Brie's +Brigadoon +Brigadoon's +Briggs +Briggs's +Brigham +Brigham's +Bright +Bright's +Brighton +Brighton's +Brigid +Brigid's +Brigitte +Brigitte's +Brillo +Brillo's +Brinkley +Brinkley's +Brisbane +Brisbane's +Bristol +Bristol's +Brit +Brit's +Britain +Britain's +Britannia +Britannia's +Britannic +Britannic's +Britannica +Britannica's +British +British's +Britisher +Britney +Britney's +Briton +Briton's +Britons +Brits +Britt +Britt's +Brittany +Brittany's +Britten +Britten's +Brittney +Brittney's +Brno +Brno's +Broadway +Broadway's +Broadways +Brobdingnag +Brobdingnag's +Brobdingnagian +Brobdingnagian's +Brock +Brock's +Brokaw +Brokaw's +Bronson +Bronson's +Bronte +Bronte's +Brontosaurus +Bronx +Bronx's +Brooke +Brooke's +Brooklyn +Brooklyn's +Brooks +Brooks's +Brown +Brown's +Browne +Browne's +Brownian +Brownian's +Brownie +Brownies +Browning +Browning's +Brownshirt +Brownshirt's +Brownsville +Brownsville's +Brubeck +Brubeck's +Bruce +Bruce's +Bruckner +Bruckner's +Brueghel +Brueghel's +Brummel +Brummel's +Brunei +Brunei's +Brunelleschi +Brunelleschi's +Brunhilde +Brunhilde's +Bruno +Bruno's +Brunswick +Brunswick's +Brussels +Brussels's +Brut +Brut's +Brutus +Brutus's +Bryan +Bryan's +Bryant +Bryant's +Bryce +Bryce's +Brynner +Brynner's +Bryon +Bryon's +Brzezinski +Brzezinski's +Btu +Btu's +Buber +Buber's +Buchanan +Buchanan's +Bucharest +Bucharest's +Buchenwald +Buchenwald's +Buchwald +Buchwald's +Buck +Buck's +Buckingham +Buckingham's +Buckley +Buckley's +Buckner +Buckner's +Bud +Bud's +Budapest +Budapest's +Buddha +Buddha's +Buddhas +Buddhism +Buddhism's +Buddhisms +Buddhist +Buddhist's +Buddhists +Buddy +Buddy's +Budweiser +Budweiser's +Buffalo +Buffalo's +Buffy +Buffy's +Buford +Buford's +Bugatti +Bugatti's +Bugzilla +Bugzilla's +Buick +Buick's +Bujumbura +Bujumbura's +Bukhara +Bukhara's +Bukharin +Bukharin's +Bulawayo +Bulawayo's +Bulfinch +Bulfinch's +Bulganin +Bulganin's +Bulgar +Bulgar's +Bulgari +Bulgari's +Bulgaria +Bulgaria's +Bulgarian +Bulgarian's +Bulgarians +Bullock +Bullock's +Bullwinkle +Bullwinkle's +Bultmann +Bultmann's +Bumppo +Bumppo's +Bunche +Bunche's +Bundesbank +Bundesbank's +Bundestag +Bundestag's +Bunin +Bunin's +Bunker +Bunker's +Bunsen +Bunsen's +Bunyan +Bunyan's +Burbank +Burbank's +Burberry +Burberry's +Burch +Burch's +Burger +Burger's +Burgess +Burgess's +Burgoyne +Burgoyne's +Burgundian +Burgundian's +Burgundies +Burgundy +Burgundy's +Burke +Burke's +Burks +Burks's +Burl +Burl's +Burma +Burma's +Burmese +Burmese's +Burnett +Burnett's +Burns +Burns's +Burnside +Burnside's +Burr +Burr's +Burris +Burris's +Burroughs +Burroughs's +Bursa +Bursa's +Burt +Burt's +Burton +Burton's +Burundi +Burundi's +Busch +Busch's +Bush +Bush's +Bushido +Bushido's +Bushnell +Bushnell's +Butler +Butler's +Butterfingers +Butterfingers's +Buxtehude +Buxtehude's +Buñuel +Buñuel's +Byblos +Byblos's +Byelorussia +Byelorussia's +Byers +Byers's +Byrd +Byrd's +Byron +Byron's +Byronic +Byronic's +Byzantine +Byzantine's +Byzantines +Byzantium +Byzantium's +C +C's +CSS +CSS's +CVS +CVS's +Ca +Ca's +Cabernet +Cabernet's +Cabinet +Cabot +Cabot's +Cabral +Cabral's +Cabrera +Cabrera's +Cabrini +Cabrini's +Cadillac +Cadillac's +Cadiz +Cadiz's +Caedmon +Caedmon's +Caerphilly +Caerphilly's +Caesar +Caesar's +Caesarean +Caesars +Cage +Cage's +Cagney +Cagney's +Cahokia +Cahokia's +Caiaphas +Caiaphas's +Cain +Cain's +Cains +Cairo +Cairo's +Caitlin +Caitlin's +Cajun +Cajun's +Cajuns +Calais +Calais's +Calcutta +Calcutta's +Calder +Calder's +Calderon +Calderon's +Caldwell +Caldwell's +Caleb +Caleb's +Caledonia +Caledonia's +Calgary +Calgary's +Calhoun +Calhoun's +Cali +Cali's +Caliban +Caliban's +California +California's +Californian +Californian's +Californians +Caligula +Caligula's +Callaghan +Callaghan's +Callahan +Callahan's +Callao +Callao's +Callas +Callas's +Callie +Callie's +Calliope +Calliope's +Callisto +Callisto's +Caloocan +Caloocan's +Calvary +Calvary's +Calvert +Calvert's +Calvin +Calvin's +Calvinism +Calvinism's +Calvinisms +Calvinist +Calvinist's +Calvinistic +Calvinists +Camacho +Camacho's +Cambodia +Cambodia's +Cambodian +Cambodian's +Cambodians +Cambrian +Cambrian's +Cambridge +Cambridge's +Camel +Camel's +Camelopardalis +Camelopardalis's +Camelot +Camelot's +Camembert +Camembert's +Camemberts +Cameron +Cameron's +Cameroon +Cameroon's +Cameroons +Camilla +Camilla's +Camille +Camille's +Camoens +Camoens's +Campanella +Campanella's +Campbell +Campbell's +Campinas +Campinas's +Campos +Campos's +Camry +Camry's +Camus +Camus's +Canaan +Canaan's +Canada +Canada's +Canadian +Canadian's +Canadians +Canaletto +Canaletto's +Canaries +Canaries's +Canaveral +Canaveral's +Canberra +Canberra's +Cancer +Cancer's +Cancers +Cancun +Cancun's +Candace +Candace's +Candice +Candice's +Candide +Candide's +Candy +Candy's +Cannes +Cannes's +Cannon +Cannon's +Canon +Canon's +Canopus +Canopus's +Cantabrigian +Cantabrigian's +Canterbury +Canterbury's +Canton +Canton's +Cantonese +Cantonese's +Cantor +Cantor's +Cantrell +Cantrell's +Cantu +Cantu's +Canute +Canute's +Capablanca +Capablanca's +Capek +Capek's +Capella +Capella's +Capet +Capet's +Capetian +Capetian's +Capetown +Capetown's +Caph +Caph's +Capistrano +Capistrano's +Capitol +Capitol's +Capitoline +Capitoline's +Capitols +Capone +Capone's +Capote +Capote's +Capra +Capra's +Capri +Capri's +Capricorn +Capricorn's +Capricorns +Capuchin +Capuchin's +Capulet +Capulet's +Cara +Cara's +Caracalla +Caracalla's +Caracas +Caracas's +Caravaggio +Caravaggio's +Carboloy +Carboloy's +Carboniferous +Carboniferous's +Carborundum +Carborundum's +Cardenas +Cardenas's +Cardiff +Cardiff's +Cardin +Cardin's +Cardozo +Cardozo's +Carey +Carey's +Carib +Carib's +Caribbean +Caribbean's +Caribbeans +Carina +Carina's +Carissa +Carissa's +Carl +Carl's +Carla +Carla's +Carlene +Carlene's +Carlin +Carlin's +Carlo +Carlo's +Carlos +Carlos's +Carlsbad +Carlsbad's +Carlson +Carlson's +Carlton +Carlton's +Carly +Carly's +Carlyle +Carlyle's +Carmela +Carmela's +Carmella +Carmella's +Carmelo +Carmelo's +Carmen +Carmen's +Carmichael +Carmichael's +Carmine +Carmine's +Carnap +Carnap's +Carnation +Carnation's +Carnegie +Carnegie's +Carney +Carney's +Carnot +Carnot's +Carol +Carol's +Carole +Carole's +Carolina +Carolina's +Caroline +Caroline's +Carolingian +Carolingian's +Carolinian +Carolinian's +Carolyn +Carolyn's +Carpathian +Carpathian's +Carpathians +Carpathians's +Carpenter +Carpenter's +Carr +Carr's +Carranza +Carranza's +Carrie +Carrie's +Carrier +Carrier's +Carrillo +Carrillo's +Carroll +Carroll's +Carson +Carson's +Carter +Carter's +Cartesian +Cartesian's +Carthage +Carthage's +Carthaginian +Carthaginian's +Cartier +Cartier's +Cartwright +Cartwright's +Caruso +Caruso's +Carver +Carver's +Cary +Cary's +Casablanca +Casablanca's +Casals +Casals's +Casandra +Casandra's +Casanova +Casanova's +Casanovas +Cascades +Cascades's +Case +Case's +Casey +Casey's +Cash +Cash's +Casio +Casio's +Caspar +Caspar's +Caspian +Caspian's +Cassandra +Cassandra's +Cassatt +Cassatt's +Cassie +Cassie's +Cassiopeia +Cassiopeia's +Cassius +Cassius's +Castaneda +Castaneda's +Castillo +Castillo's +Castlereagh +Castlereagh's +Castor +Castor's +Castries +Castries's +Castro +Castro's +Catalan +Catalan's +Catalina +Catalina's +Catalonia +Catalonia's +Catawba +Catawba's +Caterpillar +Caterpillar's +Cathay +Cathay's +Cather +Cather's +Catherine +Catherine's +Cathleen +Cathleen's +Catholic +Catholic's +Catholicism +Catholicism's +Catholicisms +Catholics +Cathryn +Cathryn's +Cathy +Cathy's +Catiline +Catiline's +Cato +Cato's +Catskill +Catskill's +Catskills +Catskills's +Catt +Catt's +Catullus +Catullus's +Caucasian +Caucasian's +Caucasians +Caucasoid +Caucasus +Caucasus's +Cauchy +Cauchy's +Cavendish +Cavendish's +Cavour +Cavour's +Caxton +Caxton's +Cayenne +Cayenne's +Cayman +Cayman's +Cayuga +Cayuga's +Cd +Cd's +Ceausescu +Ceausescu's +Cebu +Cebu's +Cebuano +Cebuano's +Cecelia +Cecelia's +Cecil +Cecil's +Cecile +Cecile's +Cecilia +Cecilia's +Cecily +Cecily's +Cedric +Cedric's +Celebes +Celebes's +Celeste +Celeste's +Celgene +Celgene's +Celia +Celia's +Celina +Celina's +Cellini +Cellini's +Celsius +Celsius's +Celt +Celt's +Celtic +Celtic's +Celtics +Celts +Cenozoic +Cenozoic's +Centaurus +Centaurus's +Centigrade +Cepheid +Cepheid's +Cepheus +Cepheus's +Cerberus +Cerberus's +Cerenkov +Cerenkov's +Ceres +Ceres's +Cerf +Cerf's +Cervantes +Cervantes's +Cesar +Cesar's +Cesarean +Cesarean's +Cessna +Cessna's +Cetus +Cetus's +Ceylon +Ceylon's +Cezanne +Cezanne's +Ch'in +Ch'in's +Chablis +Chablis's +Chad +Chad's +Chadwick +Chadwick's +Chagall +Chagall's +Chaitanya +Chaitanya's +Chaitin +Chaitin's +Chaldean +Chaldean's +Challenger +Challenger's +Chamberlain +Chamberlain's +Chambers +Chambers's +Champlain +Champlain's +Champollion +Champollion's +Chan +Chan's +Chance +Chance's +Chancellorsville +Chancellorsville's +Chandigarh +Chandigarh's +Chandler +Chandler's +Chandon +Chandon's +Chandra +Chandra's +Chandragupta +Chandragupta's +Chandrasekhar +Chandrasekhar's +Chanel +Chanel's +Chaney +Chaney's +Chang +Chang's +Changchun +Changchun's +Changsha +Changsha's +Chantilly +Chantilly's +Chanukah +Chanukah's +Chanukahs +Chaplin +Chaplin's +Chapman +Chapman's +Chappaquiddick +Chappaquiddick's +Chapultepec +Chapultepec's +Charbray +Charbray's +Chardonnay +Chardonnay's +Charity +Charity's +Charlemagne +Charlemagne's +Charlene +Charlene's +Charles +Charles's +Charleston +Charleston's +Charlestons +Charley +Charley's +Charlie +Charlie's +Charlotte +Charlotte's +Charlottetown +Charlottetown's +Charmaine +Charmaine's +Charmin +Charmin's +Charolais +Charolais's +Charon +Charon's +Chartism +Chartism's +Chartres +Chartres's +Charybdis +Charybdis's +Chase +Chase's +Chasity +Chasity's +Chateaubriand +Chateaubriand's +Chattahoochee +Chattahoochee's +Chattanooga +Chattanooga's +Chatterley +Chatterley's +Chatterton +Chatterton's +Chaucer +Chaucer's +Chauncey +Chauncey's +Chautauqua +Chautauqua's +Chavez +Chavez's +Chayefsky +Chayefsky's +Che +Che's +Chechen +Chechen's +Chechnya +Chechnya's +Cheddar +Cheddar's +Cheer +Cheer's +Cheerios +Cheerios's +Cheetos +Cheetos's +Cheever +Cheever's +Chekhov +Chekhov's +Chelsea +Chelsea's +Chelyabinsk +Chelyabinsk's +Chen +Chen's +Cheney +Cheney's +Chengdu +Chengdu's +Chennai +Chennai's +Cheops +Cheops's +Cheri +Cheri's +Cherie +Cherie's +Chernenko +Chernenko's +Chernobyl +Chernobyl's +Chernomyrdin +Chernomyrdin's +Cherokee +Cherokee's +Cherokees +Cherry +Cherry's +Cheryl +Cheryl's +Chesapeake +Chesapeake's +Cheshire +Cheshire's +Chester +Chester's +Chesterfield +Chesterfield's +Chesterton +Chesterton's +Chevalier +Chevalier's +Cheviot +Cheviot's +Chevrolet +Chevrolet's +Chevron +Chevron's +Chevy +Chevy's +Cheyenne +Cheyenne's +Cheyennes +Chi +Chi's +Chianti +Chianti's +Chiantis +Chiba +Chiba's +Chibcha +Chibcha's +Chicago +Chicago's +Chicagoan +Chicagoan's +Chicana +Chicana's +Chicano +Chicano's +Chickasaw +Chickasaw's +Chiclets +Chiclets's +Chihuahua +Chihuahua's +Chihuahuas +Chile +Chile's +Chilean +Chilean's +Chileans +Chimborazo +Chimborazo's +Chimera +Chimera's +Chimu +Chimu's +China +China's +Chinatown +Chinatown's +Chinese +Chinese's +Chinook +Chinook's +Chinooks +Chipewyan +Chipewyan's +Chippendale +Chippendale's +Chippewa +Chippewa's +Chiquita +Chiquita's +Chirico +Chirico's +Chisholm +Chisholm's +Chisinau +Chisinau's +Chittagong +Chittagong's +Chivas +Chivas's +Chloe +Chloe's +Choctaw +Choctaw's +Chomsky +Chomsky's +Chongqing +Chongqing's +Chopin +Chopin's +Chopra +Chopra's +Chou +Chou's +Chretien +Chretien's +Chris +Chris's +Christ +Christ's +Christa +Christa's +Christchurch +Christchurch's +Christendom +Christendom's +Christendoms +Christensen +Christensen's +Christi +Christi's +Christian +Christian's +Christianities +Christianity +Christianity's +Christians +Christie +Christie's +Christina +Christina's +Christine +Christine's +Christmas +Christmas's +Christmases +Christoper +Christoper's +Christopher +Christopher's +Christs +Christy +Christy's +Chrysler +Chrysler's +Chrysostom +Chrysostom's +Chrystal +Chrystal's +Chuck +Chuck's +Chukchi +Chukchi's +Chumash +Chumash's +Chung +Chung's +Chungking +Chungking's +Church +Church's +Churchill +Churchill's +Churriguera +Churriguera's +Chuvash +Chuvash's +Ci +Ci's +Cicero +Cicero's +Cid +Cid's +Cimabue +Cimabue's +Cincinnati +Cincinnati's +Cinderella +Cinderella's +Cinderellas +Cindy +Cindy's +CinemaScope +CinemaScope's +Cinerama +Cinerama's +Cipro +Cipro's +Circe +Circe's +Cisco +Cisco's +Citibank +Citibank's +Citigroup +Citigroup's +Citroen +Citroen's +Cl +Cl's +Claiborne +Claiborne's +Clair +Clair's +Claire +Claire's +Clairol +Clairol's +Clancy +Clancy's +Clapeyron +Clapeyron's +Clapton +Clapton's +Clara +Clara's +Clare +Clare's +Clarence +Clarence's +Clarendon +Clarendon's +Clarice +Clarice's +Clarissa +Clarissa's +Clark +Clark's +Clarke +Clarke's +Claude +Claude's +Claudette +Claudette's +Claudia +Claudia's +Claudine +Claudine's +Claudio +Claudio's +Claudius +Claudius's +Claus +Claus's +Clausewitz +Clausewitz's +Clausius +Clausius's +Clay +Clay's +Clayton +Clayton's +Clearasil +Clearasil's +Clem +Clem's +Clemenceau +Clemenceau's +Clemens +Clemens's +Clement +Clement's +Clementine +Clementine's +Clements +Clements's +Clemons +Clemons's +Clemson +Clemson's +Cleo +Cleo's +Cleopatra +Cleopatra's +Cleveland +Cleveland's +Cliburn +Cliburn's +Cliff +Cliff's +Clifford +Clifford's +Clifton +Clifton's +Cline +Cline's +Clint +Clint's +Clinton +Clinton's +Clio +Clio's +Clive +Clive's +Clojure +Clojure's +Clorets +Clorets's +Clorox +Clorox's +Closure +Closure's +Clotho +Clotho's +Clouseau +Clouseau's +Clovis +Clovis's +Clyde +Clyde's +Clydesdale +Clydesdale's +Clytemnestra +Clytemnestra's +Cobain +Cobain's +Cobb +Cobb's +Cochabamba +Cochabamba's +Cochin +Cochin's +Cochise +Cochise's +Cochran +Cochran's +Cockney +Cockney's +Cocteau +Cocteau's +Cody +Cody's +Coffey +Coffey's +Cognac +Cognac's +Cohan +Cohan's +Cohen +Cohen's +Coimbatore +Coimbatore's +Cointreau +Cointreau's +Coke +Coke's +Cokes +Colbert +Colbert's +Colby +Colby's +Cole +Cole's +Coleen +Coleen's +Coleman +Coleman's +Coleridge +Coleridge's +Colette +Colette's +Colfax +Colfax's +Colgate +Colgate's +Colin +Colin's +Colleen +Colleen's +Collier +Collier's +Collin +Collin's +Collins +Collins's +Cologne +Cologne's +Colombia +Colombia's +Colombian +Colombian's +Colombians +Colombo +Colombo's +Colon +Colon's +Colonial +Colorado +Colorado's +Colosseum +Colosseum's +Colt +Colt's +Coltrane +Coltrane's +Columbia +Columbia's +Columbine +Columbine's +Columbus +Columbus's +Comanche +Comanche's +Comanches +Combs +Combs's +Comintern +Comintern's +Commons +Commons's +Commonwealth +Communion +Communion's +Communions +Communism +Communist +Communist's +Communists +Como +Como's +Comoros +Comoros's +Compaq +Compaq's +Compton +Compton's +CompuServe +CompuServe's +Comte +Comte's +Conakry +Conakry's +Conan +Conan's +Concepción +Concepción's +Concetta +Concetta's +Concord +Concord's +Concorde +Concorde's +Concords +Condillac +Condillac's +Condorcet +Condorcet's +Conestoga +Conestoga's +Confederacy +Confederacy's +Confederate +Confederate's +Confederates +Confucian +Confucian's +Confucianism +Confucianism's +Confucianisms +Confucians +Confucius +Confucius's +Congo +Congo's +Congolese +Congolese's +Congregationalist +Congregationalist's +Congregationalists +Congress +Congress's +Congresses +Congreve +Congreve's +Conley +Conley's +Connecticut +Connecticut's +Connemara +Connemara's +Conner +Conner's +Connery +Connery's +Connie +Connie's +Connolly +Connolly's +Connors +Connors's +Conrad +Conrad's +Conrail +Conrail's +Constable +Constable's +Constance +Constance's +Constantine +Constantine's +Constantinople +Constantinople's +Constitution +Consuelo +Consuelo's +Continent +Continent's +Continental +Continental's +Contreras +Contreras's +Conway +Conway's +Cook +Cook's +Cooke +Cooke's +Cooley +Cooley's +Coolidge +Coolidge's +Cooper +Cooper's +Cooperstown +Cooperstown's +Coors +Coors's +Copacabana +Copacabana's +Copeland +Copeland's +Copenhagen +Copenhagen's +Copernican +Copernican's +Copernicus +Copernicus's +Copland +Copland's +Copley +Copley's +Copperfield +Copperfield's +Coppertone +Coppertone's +Coppola +Coppola's +Coptic +Coptic's +Cora +Cora's +Cordelia +Cordelia's +Cordilleras +Cordilleras's +Cordoba +Cordoba's +Cordova +Cordova's +Corey +Corey's +Corfu +Corfu's +Corina +Corina's +Corine +Corine's +Corinne +Corinne's +Corinth +Corinth's +Corinthian +Corinthian's +Corinthians +Corinthians's +Coriolanus +Coriolanus's +Coriolis +Coriolis's +Corleone +Corleone's +Cormack +Cormack's +Corneille +Corneille's +Cornelia +Cornelia's +Cornelius +Cornelius's +Cornell +Cornell's +Corning +Corning's +Cornish +Cornish's +Cornwall +Cornwall's +Cornwallis +Cornwallis's +Coronado +Coronado's +Corot +Corot's +Correggio +Correggio's +Corrine +Corrine's +Corsica +Corsica's +Corsican +Corsican's +Cortes +Cortes's +Corteses +Cortez +Cortez's +Cortland +Cortland's +Corvallis +Corvallis's +Corvette +Corvette's +Corvus +Corvus's +Cory +Cory's +Cosby +Cosby's +CosmosDB +CosmosDB's +Cossack +Cossack's +Costco +Costco's +Costello +Costello's +Costner +Costner's +Cote +Cote's +Cotonou +Cotonou's +Cotopaxi +Cotopaxi's +Cotswold +Cotswold's +Cotton +Cotton's +Coulomb +Coulomb's +Coulter +Coulter's +Couperin +Couperin's +Courbet +Courbet's +Courtney +Courtney's +Cousteau +Cousteau's +Coventries +Coventry +Coventry's +Coward +Coward's +Cowley +Cowley's +Cowper +Cowper's +Cox +Cox's +Coy +Coy's +Cozumel +Cozumel's +Cr +Cr's +Crabbe +Crabbe's +Craft +Craft's +Craig +Craig's +Cranach +Cranach's +Crane +Crane's +Cranmer +Cranmer's +Crater +Crater's +Crawford +Crawford's +Cray +Cray's +Crayola +Crayola's +Creation +Creation's +Creator +Creator's +Crecy +Crecy's +Cree +Cree's +Creek +Creek's +Creighton +Creighton's +Creole +Creole's +Creoles +Creon +Creon's +Cressida +Cressida's +Crest +Crest's +Cretaceous +Cretaceous's +Cretan +Cretan's +Crete +Crete's +Crichton +Crichton's +Crick +Crick's +Crimea +Crimea's +Crimean +Crimean's +Criollo +Criollo's +Crisco +Crisco's +Cristina +Cristina's +Croat +Croat's +Croatia +Croatia's +Croatian +Croatian's +Croatians +Croats +Croce +Croce's +Crockett +Crockett's +Croesus +Croesus's +Cromwell +Cromwell's +Cromwellian +Cromwellian's +Cronin +Cronin's +Cronkite +Cronkite's +Cronus +Cronus's +Crookes +Crookes's +Crosby +Crosby's +Cross +Cross's +Crowley +Crowley's +Cruikshank +Cruikshank's +Cruise +Cruise's +Crusades +Crusades's +Crusoe +Crusoe's +Crux +Crux's +Cruz +Cruz's +Cryptozoic +Cryptozoic's +Crystal +Crystal's +Cs +Csonka +Csonka's +Ctesiphon +Ctesiphon's +Cthulhu +Cthulhu's +Cu +Cu's +Cuba +Cuba's +Cuban +Cuban's +Cubans +Cuchulain +Cuchulain's +Cuisinart +Cuisinart's +Culbertson +Culbertson's +Cullen +Cullen's +Cumberland +Cumberland's +Cummings +Cummings's +Cunard +Cunard's +Cunningham +Cunningham's +Cupid +Cupid's +Curacao +Curacao's +Curie +Curie's +Curitiba +Curitiba's +Currier +Currier's +Curry +Curry's +Curt +Curt's +Curtis +Curtis's +Custer +Custer's +Cuvier +Cuvier's +Cuzco +Cuzco's +Cybele +Cybele's +Cyclades +Cyclades's +Cyclops +Cyclops's +Cygnus +Cygnus's +Cymbeline +Cymbeline's +Cynthia +Cynthia's +Cyprian +Cyprian's +Cypriot +Cypriot's +Cypriots +Cyprus +Cyprus's +Cyrano +Cyrano's +Cyril +Cyril's +Cyrillic +Cyrillic's +Cyrus +Cyrus's +Czech +Czech's +Czechia +Czechia's +Czechoslovakia +Czechoslovakia's +Czechoslovakian +Czechoslovakian's +Czechoslovakians +Czechs +Czerny +Czerny's +D +D's +Dacca +Dacca's +Dachau +Dachau's +Dacron +Dacron's +Dacrons +Dada +Dada's +Dadaism +Dadaism's +Daedalus +Daedalus's +Daguerre +Daguerre's +Dagwood +Dagwood's +Dahomey +Dahomey's +Daimler +Daimler's +Daisy +Daisy's +Dakar +Dakar's +Dakota +Dakota's +Dakotan +Dakotan's +Dakotas +Dalai +Dale +Dale's +Daley +Daley's +Dali +Dali's +Dalian +Dalian's +Dallas +Dallas's +Dalmatian +Dalmatian's +Dalmatians +Dalton +Dalton's +Damascus +Damascus's +Damian +Damian's +Damien +Damien's +Damion +Damion's +Damocles +Damocles's +Damon +Damon's +Dana +Dana's +Dane +Dane's +Danelaw +Danelaw's +Danes +Dangerfield +Dangerfield's +Danial +Danial's +Daniel +Daniel's +Danielle +Danielle's +Daniels +Daniels's +Danish +Danish's +Dannie +Dannie's +Danny +Danny's +Danone +Danone's +Dante +Dante's +Danton +Danton's +Danube +Danube's +Danubian +Danubian's +Daphne +Daphne's +Darby +Darby's +Darcy +Darcy's +Dardanelles +Dardanelles's +Dare +Dare's +Daren +Daren's +Darfur +Darfur's +Darin +Darin's +Dario +Dario's +Darius +Darius's +Darjeeling +Darjeeling's +Darla +Darla's +Darlene +Darlene's +Darling +Darling's +Darnell +Darnell's +Darrel +Darrel's +Darrell +Darrell's +Darren +Darren's +Darrin +Darrin's +Darrow +Darrow's +Darryl +Darryl's +Darth +Darth's +Dartmoor +Dartmoor's +Dartmouth +Dartmouth's +Darvon +Darvon's +Darwin +Darwin's +Darwinian +Darwinian's +Darwinism +Darwinism's +Daryl +Daryl's +Daugherty +Daugherty's +Daumier +Daumier's +Davao +Davao's +Dave +Dave's +Davenport +Davenport's +David +David's +Davids +Davidson +Davidson's +Davies +Davies's +Davis +Davis's +Davy +Davy's +Dawes +Dawes's +Dawn +Dawn's +Dawson +Dawson's +Day +Day's +Dayton +Dayton's +DeGeneres +DeGeneres's +Deadhead +Deadhead's +Dean +Dean's +Deana +Deana's +Deandre +Deandre's +Deann +Deann's +Deanna +Deanna's +Deanne +Deanne's +Debbie +Debbie's +Debby +Debby's +Debian +Debian's +Debora +Debora's +Deborah +Deborah's +Debouillet +Debouillet's +Debra +Debra's +Debs +Debs's +Debussy +Debussy's +Dec +Dec's +Decalogue +Decalogue's +Decatur +Decatur's +Decca +Decca's +Deccan +Deccan's +December +December's +Decembers +Decker +Decker's +Dedekind +Dedekind's +Dee +Dee's +Deena +Deena's +Deere +Deere's +Defoe +Defoe's +Degas +Degas's +Deidre +Deidre's +Deimos +Deimos's +Deirdre +Deirdre's +Deity +Dejesus +Dejesus's +Delacroix +Delacroix's +Delacruz +Delacruz's +Delaney +Delaney's +Delano +Delano's +Delaware +Delaware's +Delawarean +Delawarean's +Delawareans +Delawares +Delbert +Delbert's +Deleon +Deleon's +Delgado +Delgado's +Delhi +Delhi's +Delia +Delia's +Delibes +Delibes's +Delicious +Delicious's +Delilah +Delilah's +Delius +Delius's +Dell +Dell's +Della +Della's +Delmar +Delmar's +Delmarva +Delmarva's +Delmer +Delmer's +Delmonico +Delmonico's +Delores +Delores's +Deloris +Deloris's +Delphi +Delphi's +Delphic +Delphic's +Delphinus +Delphinus's +Delta +Delta's +Demavend +Demavend's +Demerol +Demerol's +Demeter +Demeter's +Demetrius +Demetrius's +Deming +Deming's +Democrat +Democrat's +Democratic +Democrats +Democritus +Democritus's +Demosthenes +Demosthenes's +Dempsey +Dempsey's +Dena +Dena's +Deneb +Deneb's +Denebola +Denebola's +Deng +Deng's +Denis +Denis's +Denise +Denise's +Denmark +Denmark's +Dennis +Dennis's +Denny +Denny's +Denver +Denver's +Deon +Deon's +Depp +Depp's +Derby +Derby's +Derek +Derek's +Derick +Derick's +Derrick +Derrick's +Derrida +Derrida's +Descartes +Descartes's +Desdemona +Desdemona's +Desiree +Desiree's +Desmond +Desmond's +Detroit +Detroit's +Deuteronomy +Deuteronomy's +Devanagari +Devanagari's +Devi +Devi's +Devin +Devin's +Devon +Devon's +Devonian +Devonian's +Dewar +Dewar's +Dewayne +Dewayne's +Dewey +Dewey's +Dewitt +Dewitt's +Dexedrine +Dexedrine's +Dexter +Dexter's +Dhaka +Dhaka's +Dhaulagiri +Dhaulagiri's +Di +Di's +DiCaprio +DiCaprio's +DiMaggio +DiMaggio's +Diaghilev +Diaghilev's +Dial +Dial's +Diana +Diana's +Diane +Diane's +Diann +Diann's +Dianna +Dianna's +Dianne +Dianne's +Diaspora +Diaspora's +Diaz +Diaz's +Dick +Dick's +Dickens +Dickens's +Dickerson +Dickerson's +Dickinson +Dickinson's +Dickson +Dickson's +Dictaphone +Dictaphone's +Diderot +Diderot's +Dido +Dido's +Didrikson +Didrikson's +Diefenbaker +Diefenbaker's +Diego +Diego's +Diem +Diem's +Diesel +Diesel's +Dietrich +Dietrich's +Dijkstra +Dijkstra's +Dijon +Dijon's +Dilbert +Dilbert's +Dillard +Dillard's +Dillinger +Dillinger's +Dillon +Dillon's +Dina +Dina's +Dinah +Dinah's +Dino +Dino's +Diocletian +Diocletian's +Diogenes +Diogenes's +Dion +Dion's +Dionne +Dionne's +Dionysian +Dionysian's +Dionysus +Dionysus's +Diophantine +Diophantine's +Dior +Dior's +Dipper +Dipper's +Dirac +Dirac's +Dirichlet +Dirichlet's +Dirk +Dirk's +Dis +Dis's +Disney +Disney's +Disneyland +Disneyland's +Disraeli +Disraeli's +Diwali +Diwali's +Dix +Dix's +Dixie +Dixie's +Dixiecrat +Dixiecrat's +Dixieland +Dixieland's +Dixielands +Dixon +Dixon's +Djakarta +Djakarta's +Django +Django's +Djibouti +Djibouti's +Dmitri +Dmitri's +Dnepropetrovsk +Dnepropetrovsk's +Dnieper +Dnieper's +Dniester +Dniester's +Dobbin +Dobbin's +Doberman +Doberman's +Dobro +Dobro's +Doctor +Doctorow +Doctorow's +Dodge +Dodge's +Dodgson +Dodgson's +Dodoma +Dodoma's +Dodson +Dodson's +Doe +Doe's +Doha +Doha's +Dolby +Dolby's +Dole +Dole's +Dollie +Dollie's +Dolly +Dolly's +Dolores +Dolores's +Domesday +Domesday's +Domingo +Domingo's +Dominguez +Dominguez's +Dominic +Dominic's +Dominica +Dominica's +Dominican +Dominican's +Dominicans +Dominick +Dominick's +Dominique +Dominique's +Domitian +Domitian's +Don +Don's +Dona +Dona's +Donahue +Donahue's +Donald +Donald's +Donaldson +Donaldson's +Donatello +Donatello's +Donetsk +Donetsk's +Donizetti +Donizetti's +Donn +Donn's +Donna +Donna's +Donne +Donne's +Donnell +Donnell's +Donner +Donner's +Donnie +Donnie's +Donny +Donny's +Donovan +Donovan's +Dooley +Dooley's +Doolittle +Doolittle's +Doonesbury +Doonesbury's +Doppler +Doppler's +Dora +Dora's +Dorcas +Dorcas's +Doreen +Doreen's +Dorian +Dorian's +Doric +Doric's +Doris +Doris's +Doritos +Doritos's +Dorothea +Dorothea's +Dorothy +Dorothy's +Dorset +Dorset's +Dorsey +Dorsey's +Dorthy +Dorthy's +Dortmund +Dortmund's +Dostoevsky +Dostoevsky's +Dot +Dot's +Dotson +Dotson's +Douala +Douala's +Douay +Douay's +Doubleday +Doubleday's +Doug +Doug's +Douglas +Douglas's +Douglass +Douglass's +Douro +Douro's +Dover +Dover's +Dow +Dow's +Downs +Downs's +Downy +Downy's +Doyle +Doyle's +Draco +Draco's +Draconian +Draconian's +Dracula +Dracula's +Drake +Drake's +Dramamine +Dramamine's +Drambuie +Drambuie's +Drano +Drano's +Dravidian +Dravidian's +Dreiser +Dreiser's +Dresden +Dresden's +Drew +Drew's +Dreyfus +Dreyfus's +Dristan +Dristan's +Dropbox +Dropbox's +Drudge +Drudge's +Druid +Druid's +Drupal +Drupal's +Dryden +Dryden's +Dschubba +Dschubba's +DuPont +DuPont's +Duane +Duane's +Dubai +Dubai's +Dubcek +Dubcek's +Dubhe +Dubhe's +Dublin +Dublin's +Dubrovnik +Dubrovnik's +Duchamp +Duchamp's +Dudley +Dudley's +Duffy +Duffy's +Duisburg +Duisburg's +Duke +Duke's +Dulles +Dulles's +Duluth +Duluth's +Dumas +Dumas's +Dumbledore +Dumbledore's +Dumbo +Dumbo's +Dumpster +Dumpster's +Dunant +Dunant's +Dunbar +Dunbar's +Duncan +Duncan's +Dunedin +Dunedin's +Dunkirk +Dunkirk's +Dunlap +Dunlap's +Dunn +Dunn's +Dunne +Dunne's +Duracell +Duracell's +Duran +Duran's +Durant +Durant's +Durante +Durante's +Durban +Durban's +Durex +Durex's +Durham +Durham's +Durhams +Durkheim +Durkheim's +Duroc +Duroc's +Durocher +Durocher's +Duse +Duse's +Dushanbe +Dushanbe's +Dustbuster +Dustbuster's +Dustin +Dustin's +Dusty +Dusty's +Dutch +Dutch's +Dutchman +Dutchman's +Dutchmen +Dutchmen's +Duvalier +Duvalier's +Dvina +Dvina's +Dvorák +Dvorák's +Dwayne +Dwayne's +Dwight +Dwight's +Dyer +Dyer's +Dylan +Dylan's +DynamoDB +DynamoDB's +Dyson +Dyson's +Dzerzhinsky +Dzerzhinsky's +Dzungaria +Dzungaria's +Dürer +Dürer's +Düsseldorf +Düsseldorf's +E +E's +ECMAScript +ECMAScript's +Eakins +Eakins's +Earhart +Earhart's +Earl +Earl's +Earle +Earle's +Earlene +Earlene's +Earline +Earline's +Earnest +Earnest's +Earnestine +Earnestine's +Earnhardt +Earnhardt's +Earp +Earp's +Earth +Earth's +East +East's +Easter +Easter's +Eastern +Easterner +Easters +Eastman +Eastman's +Easts +Eastwood +Eastwood's +Eaton +Eaton's +Eben +Eben's +Ebeneezer +Ebeneezer's +Ebert +Ebert's +Ebola +Ebola's +Ebonics +Ebonics's +Ebony +Ebony's +Ebro +Ebro's +Ecclesiastes +Ecclesiastes's +Eco +Eco's +Ecuador +Ecuador's +Ecuadoran +Ecuadoran's +Ecuadorans +Ecuadorian +Ecuadorian's +Ecuadorians +Ed +Ed's +Edam +Edam's +Edams +Edda +Edda's +Eddie +Eddie's +Eddington +Eddington's +Eddy +Eddy's +Eden +Eden's +Edens +Edgar +Edgar's +Edgardo +Edgardo's +Edinburgh +Edinburgh's +Edison +Edison's +Edith +Edith's +Edmond +Edmond's +Edmonton +Edmonton's +Edmund +Edmund's +Edna +Edna's +Edsel +Edsel's +Eduardo +Eduardo's +Edward +Edward's +Edwardian +Edwardian's +Edwardo +Edwardo's +Edwards +Edwards's +Edwin +Edwin's +Edwina +Edwina's +Eeyore +Eeyore's +Effie +Effie's +Efrain +Efrain's +Efren +Efren's +Eggo +Eggo's +Egypt +Egypt's +Egyptian +Egyptian's +Egyptians +Egyptology +Egyptology's +Ehrenberg +Ehrenberg's +Ehrlich +Ehrlich's +Eichmann +Eichmann's +Eiffel +Eiffel's +Eileen +Eileen's +Einstein +Einstein's +Einsteins +Eire +Eire's +Eisenhower +Eisenhower's +Eisenstein +Eisenstein's +Eisner +Eisner's +Elaine +Elaine's +Elam +Elam's +Elanor +Elanor's +Elasticsearch +Elasticsearch's +Elastoplast +Elastoplast's +Elba +Elba's +Elbe +Elbe's +Elbert +Elbert's +Elbrus +Elbrus's +Eldon +Eldon's +Eleanor +Eleanor's +Eleazar +Eleazar's +Electra +Electra's +Elena +Elena's +Elgar +Elgar's +Eli +Eli's +Elias +Elias's +Elijah +Elijah's +Elinor +Elinor's +Eliot +Eliot's +Elisa +Elisa's +Elisabeth +Elisabeth's +Elise +Elise's +Eliseo +Eliseo's +Elisha +Elisha's +Eliza +Eliza's +Elizabeth +Elizabeth's +Elizabethan +Elizabethan's +Elizabethans +Ella +Ella's +Ellen +Ellen's +Ellesmere +Ellesmere's +Ellie +Ellie's +Ellington +Ellington's +Elliot +Elliot's +Elliott +Elliott's +Ellis +Ellis's +Ellison +Ellison's +Elma +Elma's +Elmer +Elmer's +Elmo +Elmo's +Elnath +Elnath's +Elnora +Elnora's +Elohim +Elohim's +Eloise +Eloise's +Eloy +Eloy's +Elroy +Elroy's +Elsa +Elsa's +Elsie +Elsie's +Elsinore +Elsinore's +Eltanin +Eltanin's +Elton +Elton's +Elul +Elul's +Elva +Elva's +Elvia +Elvia's +Elvin +Elvin's +Elvira +Elvira's +Elvis +Elvis's +Elway +Elway's +Elwood +Elwood's +Elysian +Elysian's +Elysium +Elysium's +Elysiums +Elysée +Elysée's +Emacs +Emacs's +Emanuel +Emanuel's +Emerson +Emerson's +Emery +Emery's +Emil +Emil's +Emile +Emile's +Emilia +Emilia's +Emilio +Emilio's +Emily +Emily's +Eminem +Eminem's +Emma +Emma's +Emmanuel +Emmanuel's +Emmett +Emmett's +Emmy +Emmy's +Emory +Emory's +Encarta +Encarta's +Endymion +Endymion's +Engels +Engels's +England +England's +English +English's +Englisher +Englishes +Englishman +Englishman's +Englishmen +Englishmen's +Englishwoman +Englishwoman's +Englishwomen +Englishwomen's +Enid +Enid's +Enif +Enif's +Eniwetok +Eniwetok's +Enkidu +Enkidu's +Enoch +Enoch's +Enos +Enos's +Enrico +Enrico's +Enrique +Enrique's +Enron +Enron's +Enterprise +Enterprise's +Eocene +Eocene's +Epcot +Epcot's +Ephesian +Ephesian's +Ephesus +Ephesus's +Ephraim +Ephraim's +Epictetus +Epictetus's +Epicurean +Epicurean's +Epicurus +Epicurus's +Epimethius +Epimethius's +Epiphanies +Epiphany +Epiphany's +Episcopal +Episcopalian +Episcopalian's +Episcopalians +Epsom +Epsom's +Epson +Epson's +Epstein +Epstein's +Equuleus +Equuleus's +Erasmus +Erasmus's +Erato +Erato's +Eratosthenes +Eratosthenes's +Erebus +Erebus's +Erector +Erector's +Erewhon +Erewhon's +Erhard +Erhard's +Eric +Eric's +Erica +Erica's +Erich +Erich's +Erick +Erick's +Ericka +Ericka's +Erickson +Erickson's +Ericson +Ericson's +Ericsson +Ericsson's +Eridanus +Eridanus's +Erie +Erie's +Erik +Erik's +Erika +Erika's +Erin +Erin's +Eris +Eris's +Eritrea +Eritrea's +Erlang +Erlang's +Erlenmeyer +Erlenmeyer's +Erma +Erma's +Erna +Erna's +Ernest +Ernest's +Ernestine +Ernestine's +Ernesto +Ernesto's +Ernie +Ernie's +Ernst +Ernst's +Eros +Eros's +Eroses +Errol +Errol's +Erse +Erse's +ErvIn +ErvIn's +Erwin +Erwin's +Es +Esau +Esau's +Escher +Escher's +Escherichia +Escherichia's +Eskimo +Eskimo's +Eskimos +Esmeralda +Esmeralda's +Esperanto +Esperanto's +Esperanza +Esperanza's +Espinoza +Espinoza's +Essen +Essen's +Essene +Essene's +Essequibo +Essequibo's +Essex +Essex's +Essie +Essie's +Establishment +Esteban +Esteban's +Estela +Estela's +Estella +Estella's +Estelle +Estelle's +Ester +Ester's +Esterházy +Esterházy's +Estes +Estes's +Esther +Esther's +Estonia +Estonia's +Estonian +Estonian's +Estonians +Estrada +Estrada's +Ethan +Ethan's +Ethel +Ethel's +Ethelred +Ethelred's +Ethernet +Ethernet's +Ethiopia +Ethiopia's +Ethiopian +Ethiopian's +Ethiopians +Etna +Etna's +Eton +Eton's +Etruria +Etruria's +Etruscan +Etruscan's +Etta +Etta's +Eucharist +Eucharist's +Eucharistic +Eucharists +Euclid +Euclid's +Euclidean +Euclidean's +Eugene +Eugene's +Eugenia +Eugenia's +Eugenie +Eugenie's +Eugenio +Eugenio's +Eula +Eula's +Euler +Euler's +Eumenides +Eumenides's +Eunice +Eunice's +Euphrates +Euphrates's +Eurasia +Eurasia's +Eurasian +Eurasian's +Eurasians +Euripides +Euripides's +Eurodollar +Eurodollar's +Eurodollars +Europa +Europa's +Europe +Europe's +European +European's +Europeans +Eurydice +Eurydice's +Eustachian +Eustachian's +Euterpe +Euterpe's +Eva +Eva's +Evan +Evan's +Evangelina +Evangelina's +Evangeline +Evangeline's +Evans +Evans's +Evansville +Evansville's +Eve +Eve's +Evelyn +Evelyn's +Evenki +Evenki's +EverReady +EverReady's +Everest +Everest's +Everett +Everett's +Everette +Everette's +Everglades +Everglades's +Evert +Evert's +Evian +Evian's +Evita +Evita's +Ewing +Ewing's +Excalibur +Excalibur's +Excedrin +Excedrin's +Excellencies +Excellency +Excellency's +Exercycle +Exercycle's +Exocet +Exocet's +Exodus +Exodus's +Exxon +Exxon's +Eyck +Eyck's +Eyre +Eyre's +Eysenck +Eysenck's +Ezekiel +Ezekiel's +Ezra +Ezra's +F +F's +FDR +FDR's +FNMA +FNMA's +FSF +FSF's +Fabergé +Fabergé's +Fabian +Fabian's +Facebook +Facebook's +Faeroe +Faeroe's +Fafnir +Fafnir's +Fagin +Fagin's +Fahd +Fahd's +Fahrenheit +Fahrenheit's +Fairbanks +Fairbanks's +Faisal +Faisal's +Faisalabad +Faisalabad's +Faith +Faith's +Falasha +Falasha's +Falkland +Falkland's +Falklands +Falklands's +Fallopian +Fallopian's +Falstaff +Falstaff's +Falwell +Falwell's +Fannie +Fannie's +Fanny +Fanny's +Faraday +Faraday's +Fargo +Fargo's +Farley +Farley's +Farmer +Farmer's +Farragut +Farragut's +Farrakhan +Farrakhan's +Farrell +Farrell's +Farrow +Farrow's +Farsi +Farsi's +Fascism +Fassbinder +Fassbinder's +Fatah +Fatah's +Fates +Fates's +Father +Father's +Fathers +Fatima +Fatima's +Fatimid +Fatimid's +Faulkner +Faulkner's +Faulknerian +Faulknerian's +Fauntleroy +Fauntleroy's +Faust +Faust's +Faustian +Faustian's +Faustino +Faustino's +Faustus +Faustus's +Fawkes +Fawkes's +Fay +Fay's +Faye +Faye's +Fe +Fe's +Feb +Feb's +Februaries +February +February's +FedEx +FedEx's +Federalist +Federalist's +Federico +Federico's +Feds +Feds's +Felecia +Felecia's +Felice +Felice's +Felicia +Felicia's +Felicity +Felicity's +Felipe +Felipe's +Felix +Felix's +Fellini +Fellini's +Fenian +Fenian's +Ferber +Ferber's +Ferdinand +Ferdinand's +Fergus +Fergus's +Ferguson +Ferguson's +Ferlinghetti +Ferlinghetti's +Fermat +Fermat's +Fermi +Fermi's +Fern +Fern's +Fernandez +Fernandez's +Fernando +Fernando's +Ferrari +Ferrari's +Ferraro +Ferraro's +Ferrell +Ferrell's +Ferris +Ferris's +Feynman +Feynman's +Fez +Fez's +Fiat +Fiat's +Fiberglas +Fiberglas's +Fibonacci +Fibonacci's +Fichte +Fichte's +Fidel +Fidel's +Fido +Fido's +Fielding +Fielding's +Fields +Fields's +Figaro +Figaro's +Figueroa +Figueroa's +Fiji +Fiji's +Fijian +Fijian's +Fijians +Filipino +Filipino's +Filipinos +Fillmore +Fillmore's +Filofax +Filofax's +Finch +Finch's +Finland +Finland's +Finley +Finley's +Finn +Finn's +Finnbogadottir +Finnbogadottir's +Finnegan +Finnegan's +Finnish +Finnish's +Finns +Fiona +Fiona's +Firebase +Firebase's +Firefox +Firefox's +Firestone +Firestone's +Fischer +Fischer's +Fisher +Fisher's +Fisk +Fisk's +Fitch +Fitch's +Fitzgerald +Fitzgerald's +Fitzpatrick +Fitzpatrick's +Fitzroy +Fitzroy's +Fizeau +Fizeau's +Flanagan +Flanagan's +Flanders +Flanders's +Flatt +Flatt's +Flaubert +Flaubert's +Fleischer +Fleischer's +Fleming +Fleming's +Flemish +Flemish's +Fletcher +Fletcher's +Flint +Flint's +Flintstones +Flintstones's +Flo +Flo's +Flora +Flora's +Florence +Florence's +Florentine +Florentine's +Flores +Flores's +Florida +Florida's +Floridan +Floridan's +Florine +Florine's +Florsheim +Florsheim's +Flory +Flory's +Flossie +Flossie's +Flowers +Flowers's +Floyd +Floyd's +Flynn +Flynn's +Foch +Foch's +Fokker +Fokker's +Foley +Foley's +Folgers +Folgers's +Folsom +Folsom's +Fomalhaut +Fomalhaut's +Fonda +Fonda's +Foosball +Foosball's +Forbes +Forbes's +Ford +Ford's +Foreman +Foreman's +Forest +Forest's +Forester +Forester's +Formica +Formica's +Formicas +Formosa +Formosa's +Formosan +Formosan's +Forrest +Forrest's +Forster +Forster's +Fortaleza +Fortaleza's +Fosse +Fosse's +Foster +Foster's +Fotomat +Fotomat's +Foucault +Foucault's +Fourier +Fourier's +Fourneyron +Fourneyron's +Fowler +Fowler's +Fox +Fox's +Fr +Fr's +Fragonard +Fragonard's +Fran +Fran's +France +France's +Frances +Frances's +Francesca +Francesca's +Francine +Francine's +Francis +Francis's +Francisca +Francisca's +Franciscan +Franciscan's +Francisco +Francisco's +Franck +Franck's +Franco +Franco's +Francois +Francois's +Francoise +Francoise's +Franglais +Franglais's +Frank +Frank's +Frankel +Frankel's +Frankenstein +Frankenstein's +Frankfort +Frankfort's +Frankfurt +Frankfurt's +Frankfurter +Frankfurter's +Frankie +Frankie's +Franklin +Franklin's +Franks +Franks's +Franny +Franny's +Franz +Franz's +Fraser +Fraser's +Frazier +Frazier's +Fred +Fred's +Freda +Freda's +Freddie +Freddie's +Freddy +Freddy's +Frederic +Frederic's +Frederick +Frederick's +Fredericton +Fredericton's +Fredric +Fredric's +Fredrick +Fredrick's +Freeman +Freeman's +Freemason +Freemason's +Freemasonries +Freemasonry +Freemasonry's +Freemasons +Freetown +Freetown's +Freida +Freida's +Fremont +Fremont's +French +French's +Frenches +Frenchman +Frenchman's +Frenchmen +Frenchmen's +Frenchwoman +Frenchwoman's +Frenchwomen +Frenchwomen's +Freon +Freon's +Fresnel +Fresnel's +Fresno +Fresno's +Freud +Freud's +Freudian +Freudian's +Frey +Frey's +Freya +Freya's +Friday +Friday's +Fridays +Frieda +Frieda's +Friedan +Friedan's +Friedman +Friedman's +Frigga +Frigga's +Frigidaire +Frigidaire's +Frisbee +Frisbee's +Frisco +Frisco's +Frisian +Frisian's +Frito +Frito's +Fritz +Fritz's +Frobisher +Frobisher's +Froissart +Froissart's +Fromm +Fromm's +Fronde +Fronde's +Frontenac +Frontenac's +Frost +Frost's +Frostbelt +Frostbelt's +Fry +Fry's +Frye +Frye's +Fuchs +Fuchs's +Fuentes +Fuentes's +Fugger +Fugger's +Fuji +Fuji's +Fujitsu +Fujitsu's +Fujiwara +Fujiwara's +Fukuoka +Fukuoka's +Fukuyama +Fukuyama's +Fulani +Fulani's +Fulbright +Fulbright's +Fuller +Fuller's +Fulton +Fulton's +Funafuti +Funafuti's +Fundy +Fundy's +Furtwängler +Furtwängler's +Fushun +Fushun's +Fuzhou +Fuzhou's +Fuzzbuster +Fuzzbuster's +G +G's +GE +GE's +GNU +GNU's +GTE +GTE's +Gable +Gable's +Gabon +Gabon's +Gaborone +Gaborone's +Gabriel +Gabriel's +Gabriela +Gabriela's +Gabrielle +Gabrielle's +Gacrux +Gacrux's +Gadsden +Gadsden's +Gaea +Gaea's +Gael +Gael's +Gaelic +Gaelic's +Gagarin +Gagarin's +Gage +Gage's +Gaia +Gaia's +Gail +Gail's +Gaiman +Gaiman's +Gaines +Gaines's +Gainsborough +Gainsborough's +Galahad +Galahad's +Galahads +Galapagos +Galapagos's +Galatea +Galatea's +Galatia +Galatia's +Galatians +Galatians's +Galbraith +Galbraith's +Gale +Gale's +Galen +Galen's +Galibi +Galibi's +Galilean +Galilean's +Galilee +Galilee's +Galileo +Galileo's +Gall +Gall's +Gallagher +Gallagher's +Gallegos +Gallegos's +Gallic +Gallic's +Gallo +Gallo's +Galloway +Galloway's +Gallup +Gallup's +Galois +Galois's +Galsworthy +Galsworthy's +Galvani +Galvani's +Galveston +Galveston's +Gamay +Gamay's +Gambia +Gambia's +Gamble +Gamble's +Gamow +Gamow's +Gandhi +Gandhi's +Gandhian +Gandhian's +Ganesha +Ganesha's +Ganges +Ganges's +Gangtok +Gangtok's +Gantry +Gantry's +Ganymede +Ganymede's +Gap +Gap's +Garbo +Garbo's +Garcia +Garcia's +Gardner +Gardner's +Gareth +Gareth's +Garfield +Garfield's +Garfunkel +Garfunkel's +Gargantua +Gargantua's +Garibaldi +Garibaldi's +Garland +Garland's +Garner +Garner's +Garrett +Garrett's +Garrick +Garrick's +Garrison +Garrison's +Garry +Garry's +Garth +Garth's +Garvey +Garvey's +Gary +Gary's +Garza +Garza's +Gascony +Gascony's +Gasser +Gasser's +Gates +Gates's +Gatling +Gatling's +Gatorade +Gatorade's +Gatsby +Gatsby's +Gatun +Gatun's +Gauguin +Gauguin's +Gaul +Gaul's +Gauls +Gauss +Gauss's +Gaussian +Gaussian's +Gautama +Gautama's +Gautier +Gautier's +Gavin +Gavin's +Gawain +Gawain's +Gay +Gay's +Gayle +Gayle's +Gaza +Gaza's +Gaziantep +Gaziantep's +Gd +Gd's +Gdansk +Gdansk's +Ge +Ge's +Geffen +Geffen's +Gehenna +Gehenna's +Gehrig +Gehrig's +Geiger +Geiger's +Gelbvieh +Gelbvieh's +Geller +Geller's +Gemini +Gemini's +Geminis +Gena +Gena's +Genaro +Genaro's +Gene +Gene's +Genesis +Genesis's +Genet +Genet's +Geneva +Geneva's +Genevieve +Genevieve's +Genghis +Genghis's +Genoa +Genoa's +Genoas +Gentile +Gentile's +Gentiles +Gentoo +Gentoo's +Gentry +Gentry's +Geo +Geo's +Geoffrey +Geoffrey's +George +George's +Georges +Georgetown +Georgetown's +Georgette +Georgette's +Georgia +Georgia's +Georgian +Georgian's +Georgians +Georgina +Georgina's +Gerald +Gerald's +Geraldine +Geraldine's +Gerard +Gerard's +Gerardo +Gerardo's +Gerber +Gerber's +Gere +Gere's +Geritol +Geritol's +German +German's +Germanic +Germanic's +Germans +Germany +Germany's +Geronimo +Geronimo's +Gerry +Gerry's +Gershwin +Gershwin's +Gertrude +Gertrude's +Gestapo +Gestapo's +Gestapos +Gethsemane +Gethsemane's +Getty +Getty's +Gettysburg +Gettysburg's +Gewürztraminer +Gewürztraminer's +Ghana +Ghana's +Ghanaian +Ghanian +Ghanian's +Ghanians +Ghats +Ghats's +Ghazvanid +Ghazvanid's +Ghent +Ghent's +Ghibelline +Ghibelline's +Giacometti +Giacometti's +Giannini +Giannini's +Giauque +Giauque's +Gibbon +Gibbon's +Gibbs +Gibbs's +Gibraltar +Gibraltar's +Gibraltars +Gibson +Gibson's +Gide +Gide's +Gideon +Gideon's +Gielgud +Gielgud's +Gienah +Gienah's +Gil +Gil's +Gila +Gila's +Gilbert +Gilbert's +Gilberto +Gilberto's +Gilchrist +Gilchrist's +Gilda +Gilda's +Gilead +Gilead's +Giles +Giles's +Gilgamesh +Gilgamesh's +Gill +Gill's +Gillespie +Gillespie's +Gillette +Gillette's +Gilliam +Gilliam's +Gillian +Gillian's +Gilligan +Gilligan's +Gilmore +Gilmore's +Gina +Gina's +Ginger +Ginger's +Gingrich +Gingrich's +Ginny +Ginny's +Gino +Gino's +Ginsberg +Ginsberg's +Ginsburg +Ginsburg's +Ginsu +Ginsu's +Giorgione +Giorgione's +Giotto +Giotto's +Giovanni +Giovanni's +Gipsies +Gipsy +Gipsy's +Giraudoux +Giraudoux's +Giselle +Giselle's +Gish +Gish's +GitHub +GitHub's +Giuliani +Giuliani's +Giuseppe +Giuseppe's +Giza +Giza's +Gladstone +Gladstone's +Gladstones +Gladys +Gladys's +Glaser +Glaser's +Glasgow +Glasgow's +Glass +Glass's +Glastonbury +Glastonbury's +Glaswegian +Glaswegian's +Glaxo +Glaxo's +Gleason +Gleason's +Glen +Glen's +Glenda +Glenda's +Glendale +Glenlivet +Glenlivet's +Glenn +Glenn's +Glenna +Glenna's +Gloria +Gloria's +Gloucester +Gloucester's +Glover +Glover's +Gnostic +Gnostic's +Gnosticism +Gnosticism's +Goa +Goa's +Gobi +Gobi's +God +God's +Goddard +Goddard's +Godiva +Godiva's +Godot +Godot's +Godthaab +Godthaab's +Godunov +Godunov's +Godzilla +Godzilla's +Goebbels +Goebbels's +Goering +Goering's +Goethals +Goethals's +Goethe +Goethe's +Goff +Goff's +Gog +Gog's +Gogol +Gogol's +Goiania +Goiania's +Golan +Golan's +Golconda +Golconda's +Golda +Golda's +Goldberg +Goldberg's +Golden +Golden's +Goldie +Goldie's +Goldilocks +Goldilocks's +Golding +Golding's +Goldman +Goldman's +Goldsmith +Goldsmith's +Goldwater +Goldwater's +Goldwyn +Goldwyn's +Golgi +Golgi's +Golgotha +Golgotha's +Goliath +Goliath's +Gomez +Gomez's +Gomorrah +Gomorrah's +Gompers +Gompers's +Gomulka +Gomulka's +Gondwanaland +Gondwanaland's +Gonzales +Gonzales's +Gonzalez +Gonzalez's +Gonzalo +Gonzalo's +Good +Good's +Goodall +Goodall's +Goodman +Goodman's +Goodrich +Goodrich's +Goodwill +Goodwill's +Goodwin +Goodwin's +Goodyear +Goodyear's +Google +Google's +Goolagong +Goolagong's +Gopher +Gorbachev +Gorbachev's +Gordian +Gordian's +Gordimer +Gordimer's +Gordon +Gordon's +Gore +Gore's +Goren +Goren's +Gorey +Gorey's +Gorgas +Gorgas's +Gorgonzola +Gorgonzola's +Gorky +Gorky's +Gospel +Gospel's +Gospels +Goth +Goth's +Gotham +Gotham's +Gothic +Gothic's +Gothics +Goths +Gouda +Gouda's +Goudas +Gould +Gould's +Gounod +Gounod's +Goya +Goya's +Grable +Grable's +Gracchus +Gracchus's +Grace +Grace's +Graceland +Graceland's +Gracie +Gracie's +Graciela +Graciela's +Grady +Grady's +Graffias +Graffias's +Grafton +Grafton's +Graham +Graham's +Grahame +Grahame's +Grail +Grail's +Grammy +Grammy's +Grampians +Grampians's +Granada +Granada's +Grant +Grant's +Grass +Grass's +Graves +Graves's +Gray +Gray's +Grecian +Grecian's +Greece +Greece's +Greek +Greek's +Greeks +Greeley +Greeley's +Green +Green's +Greene +Greene's +Greenland +Greenland's +Greenpeace +Greenpeace's +Greensboro +Greensboro's +Greensleeves +Greensleeves's +Greenspan +Greenspan's +Greenwich +Greenwich's +Greer +Greer's +Greg +Greg's +Gregg +Gregg's +Gregorian +Gregorian's +Gregorio +Gregorio's +Gregory +Gregory's +Grenada +Grenada's +Grenadines +Grenadines's +Grendel +Grendel's +Grenoble +Grenoble's +Gresham +Gresham's +Greta +Greta's +Gretchen +Gretchen's +Gretel +Gretel's +Gretzky +Gretzky's +Grey +Grey's +Grieg +Grieg's +Griffin +Griffin's +Griffith +Griffith's +Grimes +Grimes's +Grimm +Grimm's +Grinch +Grinch's +Gris +Gris's +Gromyko +Gromyko's +Gropius +Gropius's +Gross +Gross's +Grosz +Grosz's +Grotius +Grotius's +Grover +Grover's +Grumman +Grumman's +Grundy +Grundy's +Grus +Grus's +Gruyeres +Gruyère +Gruyère's +Grünewald +Grünewald's +Guadalajara +Guadalajara's +Guadalcanal +Guadalcanal's +Guadalquivir +Guadalquivir's +Guadalupe +Guadalupe's +Guadeloupe +Guadeloupe's +Guallatiri +Guallatiri's +Guam +Guam's +Guangzhou +Guangzhou's +Guantanamo +Guantanamo's +Guarani +Guarani's +Guarnieri +Guarnieri's +Guatemala +Guatemala's +Guatemalan +Guatemalan's +Guatemalans +Guayaquil +Guayaquil's +Gucci +Gucci's +Guelph +Guelph's +Guernsey +Guernsey's +Guernseys +Guerra +Guerra's +Guerrero +Guerrero's +Guevara +Guevara's +Guggenheim +Guggenheim's +Guiana +Guiana's +Guillermo +Guillermo's +Guinea +Guinea's +Guinean +Guinean's +Guineans +Guinevere +Guinevere's +Guinness +Guinness's +Guiyang +Guiyang's +Guizot +Guizot's +Gujarat +Gujarat's +Gujarati +Gujarati's +Gujranwala +Gujranwala's +Gullah +Gullah's +Gulliver +Gulliver's +Gumbel +Gumbel's +Gunther +Gunther's +Guofeng +Guofeng's +Gupta +Gupta's +Gurkha +Gurkha's +Gus +Gus's +Gustav +Gustav's +Gustavo +Gustavo's +Gustavus +Gustavus's +Gutenberg +Gutenberg's +Guthrie +Guthrie's +Gutierrez +Gutierrez's +Guy +Guy's +Guyana +Guyana's +Guyanese +Guyanese's +Guzman +Guzman's +Gwalior +Gwalior's +Gwen +Gwen's +Gwendoline +Gwendoline's +Gwendolyn +Gwendolyn's +Gwyn +Gwyn's +Gypsies +Gypsy +Gypsy's +Gödel +Gödel's +Göteborg +Göteborg's +H +H's +HBO +HBO's +HBase +HBase's +HSBC +HSBC's +Haas +Haas's +Habakkuk +Habakkuk's +Haber +Haber's +Hadar +Hadar's +Hades +Hades's +Hadoop +Hadoop's +Hadrian +Hadrian's +Hafiz +Hafiz's +Hagar +Hagar's +Haggai +Haggai's +Hagiographa +Hagiographa's +Hague +Hague's +Hahn +Hahn's +Haifa +Haifa's +Haiphong +Haiphong's +Haiti +Haiti's +Haitian +Haitian's +Haitians +Hakka +Hakka's +Hakluyt +Hakluyt's +Hal +Hal's +Haldane +Haldane's +Hale +Hale's +Haleakala +Haleakala's +Haley +Haley's +Halifax +Halifax's +Hall +Hall's +Halley +Halley's +Halliburton +Halliburton's +Hallie +Hallie's +Hallmark +Hallmark's +Hallowe'en +Halloween +Halloween's +Halloweens +Hallstatt +Hallstatt's +Halon +Halon's +Hals +Hals's +Halsey +Halsey's +Ham +Ham's +Haman +Haman's +Hamburg +Hamburg's +Hamburgs +Hamhung +Hamhung's +Hamilcar +Hamilcar's +Hamill +Hamill's +Hamilton +Hamilton's +Hamiltonian +Hamiltonian's +Hamitic +Hamitic's +Hamlet +Hamlet's +Hamlin +Hamlin's +Hammarskjold +Hammarskjold's +Hammerstein +Hammerstein's +Hammett +Hammett's +Hammond +Hammond's +Hammurabi +Hammurabi's +Hampshire +Hampshire's +Hampton +Hampton's +Hamsun +Hamsun's +Han +Han's +Hancock +Hancock's +Handel +Handel's +Handy +Handy's +Haney +Haney's +Hangul +Hangul's +Hangzhou +Hangzhou's +Hank +Hank's +Hanna +Hanna's +Hannah +Hannah's +Hannibal +Hannibal's +Hanoi +Hanoi's +Hanover +Hanover's +Hanoverian +Hanoverian's +Hans +Hans's +Hansel +Hansel's +Hansen +Hansen's +Hanson +Hanson's +Hanukkah +Hanukkah's +Hanukkahs +Hapsburg +Hapsburg's +Harare +Harare's +Harbin +Harbin's +Hardin +Hardin's +Harding +Harding's +Hardy +Hardy's +Hargreaves +Hargreaves's +Harlan +Harlan's +Harlem +Harlem's +Harlequin +Harlequin's +Harley +Harley's +Harlow +Harlow's +Harmon +Harmon's +Harold +Harold's +Harper +Harper's +Harrell +Harrell's +Harriet +Harriet's +Harriett +Harriett's +Harrington +Harrington's +Harris +Harris's +Harrisburg +Harrisburg's +Harrison +Harrison's +Harrods +Harrods's +Harry +Harry's +Hart +Hart's +Harte +Harte's +Hartford +Hartford's +Hartline +Hartline's +Hartman +Hartman's +Harvard +Harvard's +Harvey +Harvey's +Hasbro +Hasbro's +Hasidim +Hasidim's +Hastings +Hastings's +Hatfield +Hatfield's +Hathaway +Hathaway's +Hatsheput +Hatsheput's +Hatteras +Hatteras's +Hattie +Hattie's +Hauptmann +Hauptmann's +Hausa +Hausa's +Hausdorff +Hausdorff's +Havana +Havana's +Havanas +Havarti +Havarti's +Havel +Havel's +Havoline +Havoline's +Hawaii +Hawaii's +Hawaiian +Hawaiian's +Hawaiians +Hawking +Hawking's +Hawkins +Hawkins's +Hawthorne +Hawthorne's +Hay +Hay's +Hayden +Hayden's +Haydn +Haydn's +Hayes +Hayes's +Haynes +Haynes's +Hays +Hays's +Haywood +Haywood's +Hayworth +Hayworth's +Hazel +Hazel's +Hazlitt +Hazlitt's +He +He's +Head +Head's +Hearst +Hearst's +Heath +Heath's +Heather +Heather's +Heaviside +Heaviside's +Hebe +Hebe's +Hebert +Hebert's +Hebraic +Hebraic's +Hebrew +Hebrew's +Hebrews +Hebrews's +Hebrides +Hebrides's +Hecate +Hecate's +Hector +Hector's +Hecuba +Hecuba's +Heep +Heep's +Hefner +Hefner's +Hegel +Hegel's +Hegelian +Hegelian's +Hegira +Hegira's +Heidegger +Heidegger's +Heidelberg +Heidelberg's +Heidi +Heidi's +Heifetz +Heifetz's +Heimlich +Heimlich's +Heine +Heine's +Heineken +Heineken's +Heinlein +Heinlein's +Heinrich +Heinrich's +Heinz +Heinz's +Heisenberg +Heisenberg's +Heisman +Heisman's +Helen +Helen's +Helena +Helena's +Helene +Helene's +Helga +Helga's +Helicon +Helicon's +Heliopolis +Heliopolis's +Helios +Helios's +Hell +Hell's +Hellenic +Hellenic's +Hellenism +Hellenism's +Hellenisms +Hellenistic +Hellenistic's +Hellenization +Hellenization's +Hellenize +Hellenize's +Heller +Heller's +Hellespont +Hellespont's +Hellman +Hellman's +Hells +Helmholtz +Helmholtz's +Helsinki +Helsinki's +Helvetius +Helvetius's +Hemingway +Hemingway's +Hench +Hench's +Henderson +Henderson's +Hendricks +Hendricks's +Hendrix +Hendrix's +Henley +Henley's +Hennessy +Hennessy's +Henri +Henri's +Henrietta +Henrietta's +Henry +Henry's +Hensley +Hensley's +Henson +Henson's +Hepburn +Hepburn's +Hephaestus +Hephaestus's +Hepplewhite +Hepplewhite's +Hera +Hera's +Heraclitus +Heraclitus's +Herbart +Herbart's +Herbert +Herbert's +Herculaneum +Herculaneum's +Hercules +Hercules's +Herder +Herder's +Hereford +Hereford's +Herero +Herero's +Heriberto +Heriberto's +Herman +Herman's +Hermaphroditus +Hermaphroditus's +Hermes +Hermes's +Herminia +Herminia's +Hermitage +Hermitage's +Hermite +Hermite's +Hermosillo +Hermosillo's +Hernandez +Hernandez's +Herod +Herod's +Herodotus +Herodotus's +Heroku +Heroku's +Herrera +Herrera's +Herrick +Herrick's +Herring +Herring's +Herschel +Herschel's +Hersey +Hersey's +Hershel +Hershel's +Hershey +Hershey's +Hertz +Hertz's +Hertzsprung +Hertzsprung's +Herzegovina +Herzegovina's +Herzl +Herzl's +Heshvan +Heshvan's +Hesiod +Hesiod's +Hesperus +Hesperus's +Hess +Hess's +Hesse +Hesse's +Hessian +Hessian's +Hester +Hester's +Heston +Heston's +Hettie +Hettie's +Hewitt +Hewitt's +Hewlett +Hewlett's +Heyerdahl +Heyerdahl's +Heywood +Heywood's +Hezbollah +Hezbollah's +Hezekiah +Hezekiah's +Hg +Hg's +Hialeah +Hialeah's +Hiawatha +Hiawatha's +Hibernia +Hibernia's +Hickman +Hickman's +Hickok +Hickok's +Hicks +Hicks's +Hieronymus +Hieronymus's +Higgins +Higgins's +Highlander +Highlander's +Highlanders +Highness +Highness's +Hilario +Hilario's +Hilary +Hilary's +Hilbert +Hilbert's +Hilda +Hilda's +Hildebrand +Hildebrand's +Hilfiger +Hilfiger's +Hill +Hill's +Hillary +Hillary's +Hillel +Hillel's +Hilton +Hilton's +Himalaya +Himalaya's +Himalayas +Himalayas's +Himmler +Himmler's +Hinayana +Hinayana's +Hindemith +Hindemith's +Hindenburg +Hindenburg's +Hindi +Hindi's +Hindu +Hindu's +Hinduism +Hinduism's +Hinduisms +Hindus +Hindustan +Hindustan's +Hindustani +Hindustani's +Hines +Hines's +Hinton +Hinton's +Hipparchus +Hipparchus's +Hippocrates +Hippocrates's +Hippocratic +Hippocratic's +Hiram +Hiram's +Hirobumi +Hirobumi's +Hirohito +Hirohito's +Hiroshima +Hiroshima's +Hispanic +Hispanic's +Hispanics +Hispaniola +Hispaniola's +Hiss +Hiss's +Hitachi +Hitachi's +Hitchcock +Hitchcock's +Hitler +Hitler's +Hitlers +Hittite +Hittite's +Hmong +Hmong's +Hobart +Hobart's +Hobbes +Hobbes's +Hobbs +Hobbs's +Hockney +Hockney's +Hodge +Hodge's +Hodges +Hodges's +Hodgkin +Hodgkin's +Hoff +Hoff's +Hoffa +Hoffa's +Hoffman +Hoffman's +Hofstadter +Hofstadter's +Hogan +Hogan's +Hogarth +Hogarth's +Hogwarts +Hogwarts's +Hohenlohe +Hohenlohe's +Hohenstaufen +Hohenstaufen's +Hohenzollern +Hohenzollern's +Hohhot +Hohhot's +Hohokam +Hohokam's +Hokkaido +Hokkaido's +Hokusai +Hokusai's +Holbein +Holbein's +Holcomb +Holcomb's +Holden +Holden's +Holder +Holder's +Holiday +Holiday's +Holland +Holland's +Hollands +Hollerith +Hollerith's +Holley +Holley's +Hollie +Hollie's +Hollis +Hollis's +Holloway +Holloway's +Holly +Holly's +Hollywood +Hollywood's +Holman +Holman's +Holmes +Holmes's +Holocaust +Holocaust's +Holocene +Holocene's +Holst +Holst's +Holstein +Holstein's +Holsteins +Holt +Holt's +Homer +Homer's +Homeric +Homeric's +Honda +Honda's +Honduran +Honduran's +Hondurans +Honduras +Honduras's +Honecker +Honecker's +Honeywell +Honeywell's +Hong +Honiara +Honiara's +Honolulu +Honolulu's +Honshu +Honshu's +Hood +Hood's +Hooke +Hooke's +Hooker +Hooker's +Hooper +Hooper's +Hoosier +Hoosier's +Hooters +Hooters's +Hoover +Hoover's +Hoovers +Hope +Hope's +Hopewell +Hopewell's +Hopi +Hopi's +Hopkins +Hopkins's +Hopper +Hopper's +Horace +Horace's +Horacio +Horacio's +Horatio +Horatio's +Hormel +Hormel's +Hormuz +Hormuz's +Horn +Horn's +Hornblower +Hornblower's +Horne +Horne's +Horowitz +Horowitz's +Horthy +Horthy's +Horton +Horton's +Horus +Horus's +Hosea +Hosea's +Hotpoint +Hotpoint's +Hottentot +Hottentot's +Houdini +Houdini's +House +House's +Housman +Housman's +Houston +Houston's +Houyhnhnm +Houyhnhnm's +Hovhaness +Hovhaness's +Howard +Howard's +Howe +Howe's +Howell +Howell's +Howells +Howells's +Hoyle +Hoyle's +Hrothgar +Hrothgar's +Huang +Huang's +Hubbard +Hubbard's +Hubble +Hubble's +Huber +Huber's +Hubert +Hubert's +Huck +Huck's +Hudson +Hudson's +Huerta +Huerta's +Huey +Huey's +Huff +Huff's +Huffman +Huffman's +Huggins +Huggins's +Hugh +Hugh's +Hughes +Hughes's +Hugo +Hugo's +Huguenot +Huguenot's +Huguenots +Hui +Hui's +Huitzilopotchli +Huitzilopotchli's +Hull +Hull's +Humberto +Humberto's +Humboldt +Humboldt's +Hume +Hume's +Hummer +Hummer's +Humphrey +Humphrey's +Humvee +Humvee's +Hun +Hun's +Hungarian +Hungarian's +Hungarians +Hungary +Hungary's +Huns +Hunspell +Hunspell's +Hunt +Hunt's +Hunter +Hunter's +Huntington +Huntington's +Huntley +Huntley's +Huntsville +Huntsville's +Hurd +Hurd's +Hurley +Hurley's +Huron +Huron's +Hurst +Hurst's +Hus +Hus's +Hussein +Hussein's +Husserl +Husserl's +Hussite +Hussite's +Huston +Huston's +Hutchinson +Hutchinson's +Hutton +Hutton's +Hutu +Hutu's +Huxley +Huxley's +Huygens +Huygens's +Hyades +Hyades's +Hyde +Hyde's +Hyderabad +Hyderabad's +Hydra +Hydra's +Hymen +Hymen's +Hyperion +Hyperion's +Hyundai +Hyundai's +Hz +Hz's +Héloise +Héloise's +I +I'd +I'll +I'm +I's +I've +IBM +IBM's +IKEA +IKEA's +ING +ING's +ISO +ISO's +Iaccoca +Iaccoca's +Iago +Iago's +Ian +Ian's +Iapetus +Iapetus's +Ibadan +Ibadan's +Iberia +Iberia's +Iberian +Iberian's +Ibiza +Ibiza's +Iblis +Iblis's +Ibo +Ibo's +Ibsen +Ibsen's +Icahn +Icahn's +Icarus +Icarus's +Iceland +Iceland's +Icelander +Icelander's +Icelanders +Icelandic +Icelandic's +Idaho +Idaho's +Idahoan +Idahoan's +Idahoans +Idahoes +Idahos +Ieyasu +Ieyasu's +Ignacio +Ignacio's +Ignatius +Ignatius's +Igor +Igor's +Iguassu +Iguassu's +Ijssel +Ijssel's +Ijsselmeer +Ijsselmeer's +Ike +Ike's +Ikhnaton +Ikhnaton's +Ila +Ila's +Ilene +Ilene's +Iliad +Iliad's +Illinois +Illinois's +Illuminati +Illuminati's +Ilyushin +Ilyushin's +Imelda +Imelda's +Imhotep +Imhotep's +Imodium +Imodium's +Imogene +Imogene's +Imus +Imus's +Ina +Ina's +Inca +Inca's +Incas +Inchon +Inchon's +Independence +Independence's +India +India's +Indian +Indian's +Indiana +Indiana's +Indianan +Indianan's +Indianans +Indianapolis +Indianapolis's +Indians +Indies +Indies's +Indira +Indira's +Indochina +Indochina's +Indochinese +Indochinese's +Indonesia +Indonesia's +Indonesian +Indonesian's +Indonesians +Indore +Indore's +Indra +Indra's +Indus +Indus's +Indy +Indy's +Ines +Ines's +Inez +Inez's +Inge +Inge's +Inglewood +Ingram +Ingram's +Ingres +Ingres's +Ingrid +Ingrid's +Innocent +Innocent's +Inonu +Inonu's +Inquisition +Inquisition's +Instagram +Instagram's +Instamatic +Instamatic's +Intel +Intel's +Intelsat +Intelsat's +Internationale +Internationale's +Internet +Internet's +Interpol +Interpol's +Inuit +Inuit's +Inuits +Inuktitut +Inuktitut's +Invar +Invar's +Ionesco +Ionesco's +Ionian +Ionian's +Ionic +Ionic's +Ionics +Iowa +Iowa's +Iowan +Iowan's +Iowans +Iowas +Iphigenia +Iphigenia's +Iqaluit +Iqaluit's +Iqbal +Iqbal's +Iquitos +Iquitos's +Ira +Ira's +Iran +Iran's +Iranian +Iranian's +Iranians +Iraq +Iraq's +Iraqi +Iraqi's +Iraqis +Ireland +Ireland's +Irene +Irene's +Iris +Iris's +Irish +Irish's +Irisher +Irishman +Irishman's +Irishmen +Irishmen's +Irishwoman +Irishwoman's +Irishwomen +Irishwomen's +Irkutsk +Irkutsk's +Irma +Irma's +Iroquoian +Iroquoian's +Iroquois +Iroquois's +Irrawaddy +Irrawaddy's +Irtish +Irtish's +Irvin +Irvin's +Irving +Irving's +Irwin +Irwin's +Isaac +Isaac's +Isabel +Isabel's +Isabella +Isabella's +Isabelle +Isabelle's +Isaiah +Isaiah's +Iscariot +Iscariot's +Isfahan +Isfahan's +Isherwood +Isherwood's +Ishim +Ishim's +Ishmael +Ishmael's +Ishtar +Ishtar's +Isiah +Isiah's +Isidro +Isidro's +Isis +Isis's +Islam +Islam's +Islamabad +Islamabad's +Islamic +Islamic's +Islamism +Islamism's +Islamist +Islamist's +Islams +Ismael +Ismael's +Ismail +Ismail's +Isolde +Isolde's +Ispell +Ispell's +Israel +Israel's +Israeli +Israeli's +Israelis +Israelite +Israelite's +Israels +Issac +Issac's +Issachar +Issachar's +Istanbul +Istanbul's +Isuzu +Isuzu's +Itaipu +Itaipu's +Italian +Italian's +Italians +Italy +Italy's +Itasca +Itasca's +Ithaca +Ithaca's +Ithacan +Ithacan's +Ito +Ito's +Iva +Iva's +Ivan +Ivan's +Ivanhoe +Ivanhoe's +Ives +Ives's +Ivory +Ivory's +Ivy +Ivy's +Iyar +Iyar's +Izaak +Izaak's +Izanagi +Izanagi's +Izanami +Izanami's +Izhevsk +Izhevsk's +Izmir +Izmir's +Izod +Izod's +Izvestia +Izvestia's +J +J's +JFK +JFK's +Jack +Jack's +Jackie +Jackie's +Jacklyn +Jacklyn's +Jackson +Jackson's +Jacksonian +Jacksonian's +Jacksonville +Jacksonville's +Jacky +Jacky's +Jaclyn +Jaclyn's +Jacob +Jacob's +Jacobean +Jacobean's +Jacobi +Jacobi's +Jacobin +Jacobin's +Jacobite +Jacobite's +Jacobs +Jacobs's +Jacobson +Jacobson's +Jacquard +Jacquard's +Jacqueline +Jacqueline's +Jacquelyn +Jacquelyn's +Jacques +Jacques's +Jacuzzi +Jacuzzi's +Jagger +Jagger's +Jagiellon +Jagiellon's +Jaguar +Jaguar's +Jahangir +Jahangir's +Jaime +Jaime's +Jain +Jain's +Jainism +Jainism's +Jaipur +Jaipur's +Jakarta +Jakarta's +Jake +Jake's +Jamaal +Jamaal's +Jamaica +Jamaica's +Jamaican +Jamaican's +Jamaicans +Jamal +Jamal's +Jamar +Jamar's +Jame +Jame's +Jamel +Jamel's +James +James's +Jamestown +Jamestown's +Jami +Jami's +Jamie +Jamie's +Jan +Jan's +Jana +Jana's +Janacek +Janacek's +Jane +Jane's +Janell +Janell's +Janelle +Janelle's +Janet +Janet's +Janette +Janette's +Janice +Janice's +Janie +Janie's +Janine +Janine's +Janis +Janis's +Janissary +Janissary's +Janjaweed +Janjaweed's +Janna +Janna's +Jannie +Jannie's +Jansen +Jansen's +Jansenist +Jansenist's +Januaries +January +January's +Janus +Janus's +Japan +Japan's +Japanese +Japanese's +Japaneses +Japura +Japura's +Jared +Jared's +Jarlsberg +Jarlsberg's +Jarred +Jarred's +Jarrett +Jarrett's +Jarrod +Jarrod's +Jarvis +Jarvis's +Jasmine +Jasmine's +Jason +Jason's +Jasper +Jasper's +Jataka +Jataka's +Java +Java's +JavaScript +JavaScript's +Javanese +Javanese's +Javas +Javier +Javier's +Jaxartes +Jaxartes's +Jay +Jay's +Jayapura +Jayapura's +Jayawardene +Jayawardene's +Jaycee +Jaycee's +Jaycees +Jaycees's +Jayne +Jayne's +Jayson +Jayson's +Jean +Jean's +Jeanette +Jeanette's +Jeanie +Jeanie's +Jeanine +Jeanine's +Jeanne +Jeanne's +Jeannette +Jeannette's +Jeannie +Jeannie's +Jeannine +Jeannine's +Jed +Jed's +Jedi +Jedi's +Jeep +Jeep's +Jeeves +Jeeves's +Jeff +Jeff's +Jefferey +Jefferey's +Jefferson +Jefferson's +Jeffersonian +Jeffersonian's +Jeffery +Jeffery's +Jeffrey +Jeffrey's +Jeffry +Jeffry's +Jehoshaphat +Jehoshaphat's +Jehovah +Jehovah's +Jekyll +Jekyll's +Jenifer +Jenifer's +Jenkins +Jenkins's +Jenna +Jenna's +Jenner +Jenner's +Jennie +Jennie's +Jennifer +Jennifer's +Jennings +Jennings's +Jenny +Jenny's +Jensen +Jensen's +Jephthah +Jephthah's +Jerald +Jerald's +Jeremiah +Jeremiah's +Jeremiahs +Jeremy +Jeremy's +Jeri +Jeri's +Jericho +Jericho's +Jermaine +Jermaine's +Jeroboam +Jeroboam's +Jerold +Jerold's +Jerome +Jerome's +Jerri +Jerri's +Jerrod +Jerrod's +Jerrold +Jerrold's +Jerry +Jerry's +Jersey +Jersey's +Jerseys +Jerusalem +Jerusalem's +Jess +Jess's +Jesse +Jesse's +Jessica +Jessica's +Jessie +Jessie's +Jesuit +Jesuit's +Jesuits +Jesus +Jesus's +Jetway +Jetway's +Jew +Jew's +Jewel +Jewel's +Jewell +Jewell's +Jewish +Jewish's +Jewishness +Jewry +Jewry's +Jews +Jezebel +Jezebel's +Jezebels +Jidda +Jidda's +Jilin +Jilin's +Jill +Jill's +Jillian +Jillian's +Jim +Jim's +Jimenez +Jimenez's +Jimmie +Jimmie's +Jimmy +Jimmy's +Jinan +Jinan's +Jinnah +Jinnah's +Jinny +Jinny's +Jivaro +Jivaro's +Jo +Jo's +Joan +Joan's +Joann +Joann's +Joanna +Joanna's +Joanne +Joanne's +Joaquin +Joaquin's +Job +Job's +Jobs +Jobs's +Jocasta +Jocasta's +Jocelyn +Jocelyn's +Jock +Jock's +Jockey +Jockey's +Jodi +Jodi's +Jodie +Jodie's +Jody +Jody's +Joe +Joe's +Joel +Joel's +Joey +Joey's +Jogjakarta +Jogjakarta's +Johann +Johann's +Johanna +Johanna's +Johannes +Johannes's +Johannesburg +Johannesburg's +John +John's +Johnathan +Johnathan's +Johnathon +Johnathon's +Johnie +Johnie's +Johnnie +Johnnie's +Johnny +Johnny's +Johns +Johns's +Johnson +Johnson's +Johnston +Johnston's +Jolene +Jolene's +Joliet +Joliet's +Jolson +Jolson's +Jon +Jon's +Jonah +Jonah's +Jonahs +Jonas +Jonas's +Jonathan +Jonathan's +Jonathon +Jonathon's +Jones +Jones's +Joni +Joni's +Jonson +Jonson's +Joplin +Joplin's +Jordan +Jordan's +Jordanian +Jordanian's +Jordanians +Jorge +Jorge's +Jose +Jose's +Josef +Josef's +Josefa +Josefa's +Josefina +Josefina's +Joseph +Joseph's +Josephine +Josephine's +Josephs +Josephson +Josephson's +Josephus +Josephus's +Joshua +Joshua's +Josiah +Josiah's +Josie +Josie's +Josue +Josue's +Joule +Joule's +Jove +Jove's +Jovian +Jovian's +Joy +Joy's +Joyce +Joyce's +Joycean +Joycean's +Joyner +Joyner's +Juan +Juan's +Juana +Juana's +Juanita +Juanita's +Juarez +Juarez's +Jubal +Jubal's +Judaeo +Judah +Judah's +Judaic +Judaism +Judaism's +Judaisms +Judas +Judas's +Judases +Judd +Judd's +Jude +Jude's +Judea +Judea's +Judith +Judith's +Judson +Judson's +Judy +Judy's +Juggernaut +Juggernaut's +Jules +Jules's +Julia +Julia's +Julian +Julian's +Juliana +Juliana's +Julianne +Julianne's +Julie +Julie's +Julies +Juliet +Juliet's +Juliette +Juliette's +Julio +Julio's +Julius +Julius's +Julliard +Julliard's +July +July's +June +June's +Juneau +Juneau's +Junes +Jung +Jung's +Jungfrau +Jungfrau's +Jungian +Jungian's +Junior +Junior's +Juniors +Juno +Juno's +Jupiter +Jupiter's +Jurassic +Jurassic's +Jurua +Jurua's +Justice +Justice's +Justin +Justin's +Justine +Justine's +Justinian +Justinian's +Jutland +Jutland's +Juvenal +Juvenal's +K +K's +KFC +KFC's +Kaaba +Kaaba's +Kabul +Kabul's +Kafka +Kafka's +Kafkaesque +Kafkaesque's +Kagoshima +Kagoshima's +Kahlua +Kahlua's +Kaifeng +Kaifeng's +Kaiser +Kaiser's +Kaitlin +Kaitlin's +Kalahari +Kalahari's +Kalamazoo +Kalamazoo's +Kalashnikov +Kalashnikov's +Kalb +Kalb's +Kalevala +Kalevala's +Kalgoorlie +Kalgoorlie's +Kali +Kali's +Kalmyk +Kalmyk's +Kama +Kama's +Kamchatka +Kamchatka's +Kamehameha +Kamehameha's +Kampala +Kampala's +Kampuchea +Kampuchea's +Kanchenjunga +Kanchenjunga's +Kandahar +Kandahar's +Kandinsky +Kandinsky's +Kane +Kane's +Kannada +Kannada's +Kano +Kano's +Kanpur +Kanpur's +Kansan +Kansan's +Kansans +Kansas +Kansas's +Kant +Kant's +Kantian +Kantian's +Kaohsiung +Kaohsiung's +Kaposi +Kaposi's +Kara +Kara's +Karachi +Karachi's +Karaganda +Karaganda's +Karakorum +Karakorum's +Karamazov +Karamazov's +Kareem +Kareem's +Karen +Karen's +Karenina +Karenina's +Kari +Kari's +Karin +Karin's +Karina +Karina's +Karl +Karl's +Karla +Karla's +Karloff +Karloff's +Karo +Karo's +Karol +Karol's +Karroo +Karroo's +Karyn +Karyn's +Kasai +Kasai's +Kasey +Kasey's +Kashmir +Kashmir's +Kasparov +Kasparov's +Kate +Kate's +Katelyn +Katelyn's +Katharine +Katharine's +Katherine +Katherine's +Katheryn +Katheryn's +Kathiawar +Kathiawar's +Kathie +Kathie's +Kathleen +Kathleen's +Kathrine +Kathrine's +Kathryn +Kathryn's +Kathy +Kathy's +Katie +Katie's +Katina +Katina's +Katmai +Katmai's +Katmandu +Katmandu's +Katowice +Katowice's +Katrina +Katrina's +Katy +Katy's +Kauai +Kauai's +Kaufman +Kaufman's +Kaunas +Kaunas's +Kaunda +Kaunda's +Kawabata +Kawabata's +Kawasaki +Kawasaki's +Kay +Kay's +Kaye +Kaye's +Kayla +Kayla's +Kazakh +Kazakh's +Kazakhstan +Kazakhstan's +Kazan +Kazan's +Kazantzakis +Kazantzakis's +Keaton +Keaton's +Keats +Keats's +Keck +Keck's +Keenan +Keenan's +Keewatin +Keewatin's +Keillor +Keillor's +Keisha +Keisha's +Keith +Keith's +Keller +Keller's +Kelley +Kelley's +Kelli +Kelli's +Kellie +Kellie's +Kellogg +Kellogg's +Kelly +Kelly's +Kelsey +Kelsey's +Kelvin +Kelvin's +Kemerovo +Kemerovo's +Kemp +Kemp's +Kempis +Kempis's +Kendall +Kendall's +Kendra +Kendra's +Kendrick +Kendrick's +Kenmore +Kenmore's +Kennan +Kennan's +Kennedy +Kennedy's +Kenneth +Kenneth's +Kennith +Kennith's +Kenny +Kenny's +Kent +Kent's +Kenton +Kenton's +Kentuckian +Kentuckian's +Kentuckians +Kentucky +Kentucky's +Kenya +Kenya's +Kenyan +Kenyan's +Kenyans +Kenyatta +Kenyatta's +Kenyon +Kenyon's +Keogh +Keogh's +Keokuk +Keokuk's +Kepler +Kepler's +Kerensky +Kerensky's +Keri +Keri's +Kermit +Kermit's +Kern +Kern's +Kerouac +Kerouac's +Kerr +Kerr's +Kerri +Kerri's +Kerry +Kerry's +Kettering +Kettering's +Keven +Keven's +Kevin +Kevin's +Kevlar +Kevlar's +Kevorkian +Kevorkian's +Kewpie +Kewpie's +Key +Key's +Keynes +Keynes's +Keynesian +Keynesian's +Khabarovsk +Khabarovsk's +Khachaturian +Khachaturian's +Khalid +Khalid's +Khan +Khan's +Kharkov +Kharkov's +Khartoum +Khartoum's +Khayyam +Khayyam's +Khazar +Khazar's +Khmer +Khmer's +Khoikhoi +Khoikhoi's +Khoisan +Khoisan's +Khomeini +Khomeini's +Khorana +Khorana's +Khrushchev +Khrushchev's +Khufu +Khufu's +Khulna +Khulna's +Khwarizmi +Khwarizmi's +Khyber +Khyber's +Kickapoo +Kickapoo's +Kidd +Kidd's +Kiel +Kiel's +Kierkegaard +Kierkegaard's +Kieth +Kieth's +Kiev +Kiev's +Kigali +Kigali's +Kikuyu +Kikuyu's +Kilauea +Kilauea's +Kilimanjaro +Kilimanjaro's +Kilroy +Kilroy's +Kim +Kim's +Kimberley +Kimberley's +Kimberly +Kimberly's +King +King's +Kingston +Kingston's +Kingstown +Kingstown's +Kinko's +Kinney +Kinney's +Kinsey +Kinsey's +Kinshasa +Kinshasa's +Kiowa +Kiowa's +Kip +Kip's +Kipling +Kipling's +Kirby +Kirby's +Kirchhoff +Kirchhoff's +Kirchner +Kirchner's +Kirghistan +Kirghistan's +Kirghiz +Kirghiz's +Kiribati +Kiribati's +Kirinyaga +Kirinyaga's +Kirk +Kirk's +Kirkland +Kirkland's +Kirkpatrick +Kirkpatrick's +Kirov +Kirov's +Kirsten +Kirsten's +Kisangani +Kisangani's +Kishinev +Kishinev's +Kislev +Kislev's +Kissinger +Kissinger's +Kit +Kit's +Kitakyushu +Kitakyushu's +Kitchener +Kitchener's +Kitty +Kitty's +Kiwanis +Kiwanis's +Klan +Klan's +Klansman +Klansman's +Klaus +Klaus's +Klee +Klee's +Kleenex +Kleenex's +Kleenexes +Klein +Klein's +Klimt +Klimt's +Kline +Kline's +Klingon +Klingon's +Klondike +Klondike's +Klondikes +Kmart +Kmart's +Knapp +Knapp's +Knesset +Knesset's +Kngwarreye +Kngwarreye's +Knickerbocker +Knickerbocker's +Knievel +Knievel's +Knight +Knight's +Knopf +Knopf's +Knossos +Knossos's +Knowles +Knowles's +Knox +Knox's +Knoxville +Knoxville's +Knudsen +Knudsen's +Knuth +Knuth's +Kobe +Kobe's +Koch +Koch's +Kochab +Kochab's +Kodachrome +Kodachrome's +Kodak +Kodak's +Kodaly +Kodaly's +Kodiak +Kodiak's +Koestler +Koestler's +Kohinoor +Kohinoor's +Kohl +Kohl's +Koizumi +Koizumi's +Kojak +Kojak's +Kolyma +Kolyma's +Kommunizma +Kommunizma's +Kong +Kong's +Kongo +Kongo's +Konrad +Konrad's +Koontz +Koontz's +Koppel +Koppel's +Koran +Koran's +Korans +Korea +Korea's +Korean +Korean's +Koreans +Kornberg +Kornberg's +Kory +Kory's +Korzybski +Korzybski's +Kosciusko +Kosciusko's +Kossuth +Kossuth's +Kosygin +Kosygin's +Kotlin +Kotlin's +Koufax +Koufax's +Kowloon +Kowloon's +Kr +Kr's +Kraft +Kraft's +Krakatoa +Krakatoa's +Krakow +Krakow's +Kramer +Kramer's +Krasnodar +Krasnodar's +Krasnoyarsk +Krasnoyarsk's +Krebs +Krebs's +Kremlin +Kremlin's +Kremlinologist +Kresge +Kresge's +Kringle +Kringle's +Kris +Kris's +Krishna +Krishna's +Krishnamurti +Krishnamurti's +Krista +Krista's +Kristen +Kristen's +Kristi +Kristi's +Kristie +Kristie's +Kristin +Kristin's +Kristina +Kristina's +Kristine +Kristine's +Kristopher +Kristopher's +Kristy +Kristy's +Kroc +Kroc's +Kroger +Kroger's +Kronecker +Kronecker's +Kropotkin +Kropotkin's +Kruger +Kruger's +Krugerrand +Krugerrand's +Krupp +Krupp's +Krystal +Krystal's +Kshatriya +Kshatriya's +Kublai +Kublai's +Kubrick +Kubrick's +Kuhn +Kuhn's +Kuibyshev +Kuibyshev's +Kulthumm +Kulthumm's +Kunming +Kunming's +Kuomintang +Kuomintang's +Kurd +Kurd's +Kurdish +Kurdish's +Kurdistan +Kurdistan's +Kurile +Kurile's +Kurosawa +Kurosawa's +Kurt +Kurt's +Kurtis +Kurtis's +Kusch +Kusch's +Kutuzov +Kutuzov's +Kuwait +Kuwait's +Kuwaiti +Kuwaiti's +Kuwaitis +Kuznets +Kuznets's +Kuznetsk +Kuznetsk's +Kwakiutl +Kwakiutl's +Kwan +Kwan's +Kwangju +Kwangju's +Kwanzaa +Kwanzaa's +Kwanzaas +Kyle +Kyle's +Kyoto +Kyoto's +Kyrgyzstan +Kyrgyzstan's +Kyushu +Kyushu's +Köln +Köln's +L +L'Amour +L'Amour's +L'Oreal +L'Oreal's +L'Ouverture +L'Ouverture's +L's +LBJ +LBJ's +La +La's +Laban +Laban's +Labrador +Labrador's +Labradors +Lacey +Lacey's +Lachesis +Lachesis's +Lacy +Lacy's +Ladoga +Ladoga's +Ladonna +Ladonna's +Lafayette +Lafayette's +Lafitte +Lafitte's +Lagos +Lagos's +Lagrange +Lagrange's +Lagrangian +Lagrangian's +Lahore +Lahore's +Laius +Laius's +Lajos +Lajos's +Lakeisha +Lakeisha's +Lakewood +Lakisha +Lakisha's +Lakota +Lakota's +Lakshmi +Lakshmi's +Lamar +Lamar's +Lamarck +Lamarck's +Lamaze +Lamaze's +Lamb +Lamb's +Lambert +Lambert's +Lamborghini +Lamborghini's +Lambrusco +Lambrusco's +Lamont +Lamont's +Lana +Lana's +Lanai +Lanai's +Lancashire +Lancashire's +Lancaster +Lancaster's +Lance +Lance's +Lancelot +Lancelot's +Land +Land's +Landon +Landon's +Landry +Landry's +Landsat +Landsat's +Landsteiner +Landsteiner's +Lane +Lane's +Lang +Lang's +Langerhans +Langerhans's +Langland +Langland's +Langley +Langley's +Langmuir +Langmuir's +Lanka +Lanka's +Lanny +Lanny's +Lansing +Lansing's +Lanzhou +Lanzhou's +Lao +Lao's +Laocoon +Laocoon's +Laos +Laos's +Laotian +Laotian's +Laotians +Laplace +Laplace's +Lapland +Lapland's +Lapp +Lapp's +Lapps +Lara +Lara's +Laramie +Laramie's +Lardner +Lardner's +Laredo +Laredo's +Larousse +Larousse's +Larry +Larry's +Lars +Lars's +Larsen +Larsen's +Larson +Larson's +Las +Lascaux +Lascaux's +Lassa +Lassa's +Lassen +Lassen's +Lassie +Lassie's +Latasha +Latasha's +Lateran +Lateran's +Latin +Latin's +Latina +Latiner +Latino +Latino's +Latinos +Latins +Latisha +Latisha's +Latonya +Latonya's +Latoya +Latoya's +Latrobe +Latrobe's +Latvia +Latvia's +Latvian +Latvian's +Latvians +Laud +Laud's +Lauder +Lauder's +Laue +Laue's +Laundromat +Laundromat's +Laura +Laura's +Laurasia +Laurasia's +Laurel +Laurel's +Lauren +Lauren's +Laurence +Laurence's +Laurent +Laurent's +Lauri +Lauri's +Laurie +Laurie's +Laval +Laval's +Lavern +Lavern's +Laverne +Laverne's +Lavoisier +Lavoisier's +Lavonne +Lavonne's +Lawanda +Lawanda's +Lawrence +Lawrence's +Lawson +Lawson's +Layamon +Layamon's +Layla +Layla's +Lazaro +Lazaro's +Lazarus +Lazarus's +Le +Le's +Lea +Lea's +Leach +Leach's +Leadbelly +Leadbelly's +Leah +Leah's +Leakey +Leakey's +Lean +Lean's +Leander +Leander's +Leann +Leann's +Leanna +Leanna's +Leanne +Leanne's +Lear +Lear's +Learjet +Learjet's +Leary +Leary's +Leavenworth +Leavenworth's +Lebanese +Lebanese's +Lebanon +Lebanon's +Lebesgue +Lebesgue's +Leblanc +Leblanc's +Leda +Leda's +Lederberg +Lederberg's +Lee +Lee's +Leeds +Leeds's +Leeuwenhoek +Leeuwenhoek's +Leeward +Leeward's +Left +Legendre +Legendre's +Leger +Leger's +Leghorn +Leghorn's +Lego +Lego's +Legree +Legree's +Lehman +Lehman's +Leibniz +Leibniz's +Leicester +Leicester's +Leiden +Leiden's +Leif +Leif's +Leigh +Leigh's +Leila +Leila's +Leipzig +Leipzig's +Lela +Lela's +Leland +Leland's +Lelia +Lelia's +Lemaitre +Lemaitre's +Lemuel +Lemuel's +Lemuria +Lemuria's +Len +Len's +Lena +Lena's +Lenard +Lenard's +Lenin +Lenin's +Leningrad +Leningrad's +Leninism +Leninism's +Leninist +Leninist's +Lennon +Lennon's +Lenny +Lenny's +Leno +Leno's +Lenoir +Lenoir's +Lenora +Lenora's +Lenore +Lenore's +Lent +Lent's +Lenten +Lenten's +Lents +Leo +Leo's +Leola +Leola's +Leon +Leon's +Leona +Leona's +Leonard +Leonard's +Leonardo +Leonardo's +Leoncavallo +Leoncavallo's +Leonel +Leonel's +Leonid +Leonid's +Leonidas +Leonidas's +Leonor +Leonor's +Leopold +Leopold's +Leopoldo +Leopoldo's +Leos +Lepidus +Lepidus's +Lepke +Lepke's +Lepus +Lepus's +Lerner +Lerner's +Leroy +Leroy's +Les +Les's +Lesa +Lesa's +Lesley +Lesley's +Leslie +Leslie's +Lesotho +Lesotho's +Lesseps +Lesseps's +Lessie +Lessie's +Lester +Lester's +Lestrade +Lestrade's +Leta +Leta's +Letha +Letha's +Lethe +Lethe's +Leticia +Leticia's +Letitia +Letitia's +Letterman +Letterman's +Levant +Levant's +Levesque +Levesque's +Levi +Levi's +Leviathan +Leviathan's +Levine +Levine's +Leviticus +Leviticus's +Levitt +Levitt's +Levy +Levy's +Lew +Lew's +Lewinsky +Lewinsky's +Lewis +Lewis's +Lexington +Lexington's +Lexus +Lexus's +Lhasa +Lhasa's +Lhotse +Lhotse's +Li +Li's +Libby +Libby's +Liberace +Liberace's +Liberia +Liberia's +Liberian +Liberian's +Liberians +Libra +Libra's +Libras +LibreOffice +LibreOffice's +Libreville +Libreville's +Librium +Librium's +Libya +Libya's +Libyan +Libyan's +Libyans +Lichtenstein +Lichtenstein's +Lidia +Lidia's +Lie +Lie's +Lieberman +Lieberman's +Liebfraumilch +Liebfraumilch's +Liechtenstein +Liechtenstein's +Liege +Liege's +Lila +Lila's +Lilia +Lilia's +Lilian +Lilian's +Liliana +Liliana's +Lilith +Lilith's +Liliuokalani +Liliuokalani's +Lille +Lille's +Lillian +Lillian's +Lillie +Lillie's +Lilliput +Lilliput's +Lilliputian +Lilliputian's +Lilliputians +Lilly +Lilly's +Lilongwe +Lilongwe's +Lily +Lily's +Lima +Lima's +Limbaugh +Limbaugh's +Limburger +Limburger's +Limoges +Limoges's +Limousin +Limousin's +Limpopo +Limpopo's +Lin +Lin's +Lina +Lina's +Lincoln +Lincoln's +Lincolns +Lind +Lind's +Linda +Linda's +Lindbergh +Lindbergh's +Lindsay +Lindsay's +Lindsey +Lindsey's +Lindy +Lindy's +Linnaeus +Linnaeus's +Linotype +Linotype's +Linton +Linton's +Linus +Linus's +Linux +Linux's +Linwood +Linwood's +Lionel +Lionel's +Lipizzaner +Lipizzaner's +Lippi +Lippi's +Lippmann +Lippmann's +Lipscomb +Lipscomb's +Lipton +Lipton's +Lisa +Lisa's +Lisbon +Lisbon's +Lissajous +Lissajous's +Lister +Lister's +Listerine +Listerine's +Liston +Liston's +Liszt +Liszt's +Lithuania +Lithuania's +Lithuanian +Lithuanian's +Lithuanians +Little +Little's +Litton +Litton's +Liverpool +Liverpool's +Liverpudlian +Liverpudlian's +Livia +Livia's +Livingston +Livingston's +Livingstone +Livingstone's +Livonia +Livonia's +Livy +Livy's +Liz +Liz's +Liza +Liza's +Lizzie +Lizzie's +Lizzy +Lizzy's +Ljubljana +Ljubljana's +Llewellyn +Llewellyn's +Lloyd +Lloyd's +Loafer +Loafer's +Loafers +Lobachevsky +Lobachevsky's +Lochinvar +Lochinvar's +Locke +Locke's +Lockean +Lockean's +Lockheed +Lockheed's +Lockwood +Lockwood's +Lodge +Lodge's +Lodz +Lodz's +Loewe +Loewe's +Loewi +Loewi's +Loews +Loews's +Logan +Logan's +Lohengrin +Lohengrin's +Loire +Loire's +Lois +Lois's +Loki +Loki's +Lola +Lola's +Lolita +Lolita's +Lollard +Lollard's +Lollobrigida +Lollobrigida's +Lombard +Lombard's +Lombardi +Lombardi's +Lombardy +Lombardy's +Lome +Lome's +Lon +Lon's +London +London's +Londoner +Londoner's +Long +Long's +Longfellow +Longfellow's +Longstreet +Longstreet's +Lonnie +Lonnie's +Lopez +Lopez's +Lora +Lora's +Loraine +Loraine's +Lord +Lord's +Lords +Lorelei +Lorelei's +Loren +Loren's +Lorena +Lorena's +Lorene +Lorene's +Lorentz +Lorentz's +Lorenz +Lorenz's +Lorenzo +Lorenzo's +Loretta +Loretta's +Lori +Lori's +Lorie +Lorie's +Lorna +Lorna's +Lorraine +Lorraine's +Lorre +Lorre's +Lorrie +Lorrie's +Los +Lot +Lot's +Lothario +Lothario's +Lott +Lott's +Lottie +Lottie's +Lou +Lou's +Louella +Louella's +Louie +Louie's +Louis +Louis's +Louisa +Louisa's +Louise +Louise's +Louisiana +Louisiana's +Louisianan +Louisianan's +Louisianans +Louisianian +Louisianian's +Louisianians +Louisville +Louisville's +Lourdes +Lourdes's +Louvre +Louvre's +Love +Love's +Lovecraft +Lovecraft's +Lovelace +Lovelace's +Lowe +Lowe's +Lowell +Lowell's +Lowenbrau +Lowenbrau's +Lowery +Lowery's +Loyang +Loyang's +Loyd +Loyd's +Loyola +Loyola's +Luanda +Luanda's +Luann +Luann's +Lubavitcher +Lubavitcher's +Lubbock +Lubbock's +Lubumbashi +Lubumbashi's +Lucas +Lucas's +Luce +Luce's +Lucia +Lucia's +Lucian +Lucian's +Luciano +Luciano's +Lucien +Lucien's +Lucifer +Lucifer's +Lucile +Lucile's +Lucille +Lucille's +Lucinda +Lucinda's +Lucio +Lucio's +Lucite +Lucite's +Lucius +Lucius's +Lucknow +Lucknow's +Lucretia +Lucretia's +Lucretius +Lucretius's +Lucy +Lucy's +Luddite +Luddite's +Ludhiana +Ludhiana's +Ludwig +Ludwig's +Luella +Luella's +Lufthansa +Lufthansa's +Luftwaffe +Luftwaffe's +Luger +Luger's +Lugosi +Lugosi's +Luigi +Luigi's +Luis +Luis's +Luisa +Luisa's +Luke +Luke's +Lula +Lula's +Lully +Lully's +Lulu +Lulu's +Lumière +Lumière's +Luna +Luna's +Lupe +Lupe's +Lupercalia +Lupercalia's +Lupus +Lupus's +Luria +Luria's +Lusaka +Lusaka's +Lusitania +Lusitania's +Luther +Luther's +Lutheran +Lutheran's +Lutheranism +Lutheranism's +Lutherans +Luvs +Luvs's +Luxembourg +Luxembourg's +Luxembourger +Luxembourger's +Luxembourgers +Luz +Luz's +Luzon +Luzon's +Lvov +Lvov's +LyX +LyX's +Lycra +Lycra's +Lycurgus +Lycurgus's +Lydia +Lydia's +Lyell +Lyell's +Lyle +Lyle's +Lyly +Lyly's +Lyman +Lyman's +Lyme +Lyme's +Lynch +Lynch's +Lynda +Lynda's +Lyndon +Lyndon's +Lynette +Lynette's +Lynn +Lynn's +Lynne +Lynne's +Lynnette +Lynnette's +Lyon +Lyon's +Lyons +Lyons's +Lyra +Lyra's +Lysenko +Lysenko's +Lysistrata +Lysistrata's +Lysol +Lysol's +M +M's +MCI +MCI's +MGM +MGM's +MHz +MIT +MIT's +Maalox +Maalox's +Mabel +Mabel's +Mable +Mable's +MacArthur +MacArthur's +MacBride +MacBride's +MacDonald +MacDonald's +MacLeish +MacLeish's +Macao +Macao's +Macaulay +Macaulay's +Macbeth +Macbeth's +Maccabeus +Maccabeus's +Mace +Mace's +Macedon +Macedon's +Macedonia +Macedonia's +Macedonian +Macedonian's +Macedonians +Mach +Mach's +Machiavelli +Machiavelli's +Machiavellian +Machiavellian's +Macias +Macias's +Macintosh +Macintosh's +Mack +Mack's +Mackenzie +Mackenzie's +Mackinac +Mackinac's +Mackinaw +Mackinaw's +Macmillan +Macmillan's +Macon +Macon's +Macumba +Macumba's +Macy +Macy's +Madagascan +Madagascan's +Madagascans +Madagascar +Madagascar's +Madden +Madden's +Maddox +Maddox's +Madeira +Madeira's +Madeiras +Madeleine +Madeleine's +Madeline +Madeline's +Madelyn +Madelyn's +Madge +Madge's +Madison +Madison's +Madonna +Madonna's +Madonnas +Madras +Madras's +Madrid +Madrid's +Madurai +Madurai's +Mae +Mae's +Maeterlinck +Maeterlinck's +Mafia +Mafia's +Mafias +Mafioso +Mafioso's +Magdalena +Magdalena's +Magdalene +Magdalene's +Magellan +Magellan's +Magellanic +Magellanic's +Maggie +Maggie's +Maghreb +Maghreb's +Magi +Maginot +Maginot's +Magnitogorsk +Magnitogorsk's +Magog +Magog's +Magoo +Magoo's +Magritte +Magritte's +Magsaysay +Magsaysay's +Magyar +Magyar's +Magyars +Mahabharata +Mahabharata's +Maharashtra +Maharashtra's +Mahavira +Mahavira's +Mahayana +Mahayana's +Mahayanist +Mahayanist's +Mahdi +Mahdi's +Mahfouz +Mahfouz's +Mahican +Mahican's +Mahicans +Mahler +Mahler's +Mai +Mai's +Maidenform +Maidenform's +Maigret +Maigret's +Mailer +Mailer's +Maillol +Maillol's +Maiman +Maiman's +Maimonides +Maimonides's +Maine +Maine's +Maisie +Maisie's +Maitreya +Maitreya's +Major +Major's +Majorca +Majorca's +Majuro +Majuro's +Makarios +Makarios's +Malabar +Malabar's +Malabo +Malabo's +Malacca +Malacca's +Malachi +Malachi's +Malagasy +Malagasy's +Malamud +Malamud's +Malaprop +Malaprop's +Malawi +Malawi's +Malay +Malay's +Malayalam +Malayalam's +Malayan +Malayan's +Malays +Malaysia +Malaysia's +Malaysian +Malaysian's +Malaysians +Malcolm +Malcolm's +Maldive +Maldive's +Maldives +Maldives's +Maldivian +Maldivian's +Maldivians +Maldonado +Maldonado's +Male +Male's +Mali +Mali's +Malian +Malian's +Malians +Malibu +Malibu's +Malinda +Malinda's +Malinowski +Malinowski's +Mallarmé +Mallarmé's +Mallomars +Mallomars's +Mallory +Mallory's +Malone +Malone's +Malory +Malory's +Malplaquet +Malplaquet's +Malraux +Malraux's +Malta +Malta's +Maltese +Maltese's +Malthus +Malthus's +Malthusian +Malthusian's +Mameluke +Mameluke's +Mamet +Mamet's +Mamie +Mamie's +Mammon +Mammon's +Mamore +Mamore's +Managua +Managua's +Manama +Manama's +Manasseh +Manasseh's +Manaus +Manaus's +Manchester +Manchester's +Manchu +Manchu's +Manchuria +Manchuria's +Manchurian +Manchurian's +Mancini +Mancini's +Mandalay +Mandalay's +Mandarin +Mandarin's +Mandela +Mandela's +Mandelbrot +Mandelbrot's +Mandingo +Mandingo's +Mandrell +Mandrell's +Mandy +Mandy's +Manet +Manet's +Manfred +Manfred's +Manhattan +Manhattan's +Manhattans +Mani +Mani's +Manichean +Manichean's +Manila +Manila's +Manilas +Manilla +Manilla's +Manitoba +Manitoba's +Manitoulin +Manitoulin's +Manley +Manley's +Mann +Mann's +Mannheim +Mannheim's +Manning +Manning's +Mansfield +Mansfield's +Manson +Manson's +Mantegna +Mantegna's +Mantle +Mantle's +Manuel +Manuel's +Manuela +Manuela's +Manx +Manx's +Mao +Mao's +Maoism +Maoism's +Maoisms +Maoist +Maoist's +Maoists +Maori +Maori's +Maoris +Mapplethorpe +Mapplethorpe's +Maputo +Maputo's +Mar +Mar's +Mara +Mara's +Maracaibo +Maracaibo's +Marat +Marat's +Maratha +Maratha's +Marathi +Marathi's +Marathon +Marathon's +Marc +Marc's +Marceau +Marceau's +Marcel +Marcel's +Marcelino +Marcelino's +Marcella +Marcella's +Marcelo +Marcelo's +March +March's +Marches +Marci +Marci's +Marcia +Marcia's +Marciano +Marciano's +Marcie +Marcie's +Marco +Marco's +Marconi +Marconi's +Marcos +Marcos's +Marcus +Marcus's +Marcy +Marcy's +Marduk +Marduk's +Margaret +Margaret's +Margarita +Margarita's +Margarito +Margarito's +Marge +Marge's +Margery +Margery's +Margie +Margie's +Margo +Margo's +Margret +Margret's +Margrethe +Margrethe's +Marguerite +Marguerite's +Mari +Mari's +Maria +Maria's +MariaDB +MariaDB's +Marian +Marian's +Mariana +Mariana's +Marianas +Marianas's +Marianne +Marianne's +Mariano +Mariano's +Maribel +Maribel's +Maricela +Maricela's +Marie +Marie's +Marietta +Marietta's +Marilyn +Marilyn's +Marin +Marin's +Marina +Marina's +Marine +Marine's +Marines +Mario +Mario's +Marion +Marion's +Maris +Maris's +Marisa +Marisa's +Marisol +Marisol's +Marissa +Marissa's +Maritain +Maritain's +Maritza +Maritza's +Marius +Marius's +Marjorie +Marjorie's +Marjory +Marjory's +Mark +Mark's +Markab +Markab's +Markham +Markham's +Markov +Markov's +Marks +Marks's +Marla +Marla's +Marlboro +Marlboro's +Marlborough +Marlborough's +Marlene +Marlene's +Marley +Marley's +Marlin +Marlin's +Marlon +Marlon's +Marlowe +Marlowe's +Marmara +Marmara's +Marne +Marne's +Maronite +Maronite's +Marple +Marple's +Marquesas +Marquesas's +Marquette +Marquette's +Marquez +Marquez's +Marquis +Marquis's +Marquita +Marquita's +Marrakesh +Marrakesh's +Marriott +Marriott's +Mars +Mars's +Marsala +Marsala's +Marseillaise +Marseillaise's +Marseilles +Marseilles's +Marsh +Marsh's +Marsha +Marsha's +Marshall +Marshall's +Marta +Marta's +Martel +Martel's +Martha +Martha's +Martial +Martial's +Martian +Martian's +Martians +Martin +Martin's +Martina +Martina's +Martinez +Martinez's +Martinique +Martinique's +Marty +Marty's +Marva +Marva's +Marvell +Marvell's +Marvin +Marvin's +Marx +Marx's +Marxism +Marxism's +Marxisms +Marxist +Marxist's +Marxists +Mary +Mary's +Maryann +Maryann's +Maryanne +Maryanne's +Maryellen +Maryellen's +Maryland +Maryland's +Marylander +Marylander's +Marylou +Marylou's +Masada +Masada's +Masai +Masai's +Masaryk +Masaryk's +Mascagni +Mascagni's +Masefield +Masefield's +Maserati +Maserati's +Maseru +Maseru's +Mashhad +Mashhad's +Mason +Mason's +Masonic +Masonic's +Masonite +Masonite's +Masons +Mass +Mass's +Massachusetts +Massachusetts's +Massasoit +Massasoit's +Massenet +Massenet's +Masses +Massey +Massey's +MasterCard +MasterCard's +Masters +Masters's +Mather +Mather's +Mathew +Mathew's +Mathews +Mathews's +Mathewson +Mathewson's +Mathias +Mathias's +Mathis +Mathis's +Matilda +Matilda's +Matisse +Matisse's +Matlab +Matlab's +Mattel +Mattel's +Matterhorn +Matterhorn's +Matthew +Matthew's +Matthews +Matthews's +Matthias +Matthias's +Mattie +Mattie's +Maud +Maud's +Maude +Maude's +Maugham +Maugham's +Maui +Maui's +Maupassant +Maupassant's +Maura +Maura's +Maureen +Maureen's +Mauriac +Mauriac's +Maurice +Maurice's +Mauricio +Mauricio's +Maurine +Maurine's +Mauritania +Mauritania's +Mauritius +Mauritius's +Mauro +Mauro's +Maurois +Maurois's +Mauryan +Mauryan's +Mauser +Mauser's +Mavis +Mavis's +Max +Max's +Maximilian +Maximilian's +Maxine +Maxine's +Maxwell +Maxwell's +May +May's +Maya +Maya's +Mayan +Mayan's +Mayans +Mayas +Mayer +Mayer's +Mayfair +Mayfair's +Mayflower +Mayflower's +Maynard +Maynard's +Mayo +Mayo's +Mayra +Mayra's +Mays +Mays's +Maytag +Maytag's +Mazama +Mazama's +Mazarin +Mazarin's +Mazatlan +Mazatlan's +Mazda +Mazda's +Mazola +Mazola's +Mazzini +Mazzini's +Mbabane +Mbabane's +Mbini +Mbini's +McAdam +McAdam's +McBride +McBride's +McCain +McCain's +McCall +McCall's +McCarthy +McCarthy's +McCarthyism +McCarthyism's +McCartney +McCartney's +McCarty +McCarty's +McClain +McClain's +McClellan +McClellan's +McClure +McClure's +McConnell +McConnell's +McCormick +McCormick's +McCoy +McCoy's +McCray +McCray's +McCullough +McCullough's +McDaniel +McDaniel's +McDonald +McDonald's +McDonnell +McDonnell's +McDowell +McDowell's +McEnroe +McEnroe's +McFadden +McFadden's +McFarland +McFarland's +McGee +McGee's +McGovern +McGovern's +McGowan +McGowan's +McGuffey +McGuffey's +McGuire +McGuire's +McIntosh +McIntosh's +McIntyre +McIntyre's +McKay +McKay's +McKee +McKee's +McKenzie +McKenzie's +McKinley +McKinley's +McKinney +McKinney's +McKnight +McKnight's +McLaughlin +McLaughlin's +McLean +McLean's +McLeod +McLeod's +McLuhan +McLuhan's +McMahon +McMahon's +McMillan +McMillan's +McNamara +McNamara's +McNaughton +McNaughton's +McNeil +McNeil's +McPherson +McPherson's +McQueen +McQueen's +McVeigh +McVeigh's +Md +Md's +Mead +Mead's +Meade +Meade's +Meadows +Meadows's +Meagan +Meagan's +Meany +Meany's +Mecca +Mecca's +Meccas +Medan +Medan's +Medea +Medea's +Medellin +Medellin's +Media +Media's +Medicaid +Medicaid's +Medicaids +Medicare +Medicare's +Medicares +Medici +Medici's +Medina +Medina's +Mediterranean +Mediterranean's +Mediterraneans +Medusa +Medusa's +Meg +Meg's +Megan +Megan's +Meghan +Meghan's +Meier +Meier's +Meighen +Meighen's +Meiji +Meiji's +Meir +Meir's +Mejia +Mejia's +Mekong +Mekong's +Mel +Mel's +Melanesia +Melanesia's +Melanesian +Melanesian's +Melanie +Melanie's +Melba +Melba's +Melbourne +Melbourne's +Melchior +Melchior's +Melchizedek +Melchizedek's +Melendez +Melendez's +Melinda +Melinda's +Melisa +Melisa's +Melisande +Melisande's +Melissa +Melissa's +Mellon +Mellon's +Melody +Melody's +Melpomene +Melpomene's +Melton +Melton's +Melva +Melva's +Melville +Melville's +Melvin +Melvin's +Memcached +Memcached's +Memling +Memling's +Memphis +Memphis's +Menander +Menander's +Mencius +Mencius's +Mencken +Mencken's +Mendel +Mendel's +Mendeleev +Mendeleev's +Mendelian +Mendelian's +Mendelssohn +Mendelssohn's +Mendez +Mendez's +Mendocino +Mendocino's +Mendoza +Mendoza's +Menelaus +Menelaus's +Menelik +Menelik's +Menes +Menes's +Menkalinan +Menkalinan's +Menkar +Menkar's +Menkent +Menkent's +Mennen +Mennen's +Mennonite +Mennonite's +Mennonites +Menominee +Menominee's +Menotti +Menotti's +Mensa +Mensa's +Mentholatum +Mentholatum's +Menuhin +Menuhin's +Menzies +Menzies's +Mephistopheles +Mephistopheles's +Merak +Merak's +Mercado +Mercado's +Mercator +Mercator's +Mercedes +Mercedes's +Mercer +Mercer's +Mercia +Mercia's +Merck +Merck's +Mercuries +Mercurochrome +Mercurochrome's +Mercury +Mercury's +Meredith +Meredith's +Merino +Merino's +Merle +Merle's +Merlin +Merlin's +Merlot +Merlot's +Merovingian +Merovingian's +Merriam +Merriam's +Merrick +Merrick's +Merrill +Merrill's +Merrimack +Merrimack's +Merritt +Merritt's +Merthiolate +Merthiolate's +Merton +Merton's +Mervin +Mervin's +Mesa +Mesa's +Mesabi +Mesabi's +Mesmer +Mesmer's +Mesolithic +Mesolithic's +Mesopotamia +Mesopotamia's +Mesozoic +Mesozoic's +Messerschmidt +Messerschmidt's +Messiaen +Messiaen's +Messiah +Messiah's +Messiahs +Messianic +Metallica +Metallica's +Metamucil +Metamucil's +Methodism +Methodism's +Methodisms +Methodist +Methodist's +Methodists +Methuselah +Methuselah's +Metternich +Metternich's +Meuse +Meuse's +Mexicali +Mexicali's +Mexican +Mexican's +Mexicans +Mexico +Mexico's +Meyer +Meyer's +Meyerbeer +Meyerbeer's +Meyers +Meyers's +Mfume +Mfume's +Mg +Mg's +MiG +MiG's +Mia +Mia's +Miami +Miami's +Miamis +Miaplacidus +Miaplacidus's +Micah +Micah's +Micawber +Micawber's +Michael +Michael's +Micheal +Micheal's +Michel +Michel's +Michelangelo +Michelangelo's +Michele +Michele's +Michelin +Michelin's +Michelle +Michelle's +Michelob +Michelob's +Michelson +Michelson's +Michigan +Michigan's +Michigander +Michigander's +Michiganders +Mick +Mick's +Mickey +Mickey's +Mickie +Mickie's +Micky +Micky's +Micmac +Micmac's +Micronesia +Micronesia's +Micronesian +Micronesian's +Microsoft +Microsoft's +Midas +Midas's +Middleton +Middleton's +Midland +Midland's +Midway +Midway's +Midwest +Midwest's +Midwestern +Midwestern's +Miguel +Miguel's +Mike +Mike's +Mikhail +Mikhail's +Mikoyan +Mikoyan's +Milagros +Milagros's +Milan +Milan's +Mildred +Mildred's +Miles +Miles's +Milford +Milford's +Milken +Milken's +Mill +Mill's +Millard +Millard's +Millay +Millay's +Miller +Miller's +Millet +Millet's +Millicent +Millicent's +Millie +Millie's +Millikan +Millikan's +Mills +Mills's +Milne +Milne's +Milo +Milo's +Milosevic +Milosevic's +Milquetoast +Milquetoast's +Miltiades +Miltiades's +Milton +Milton's +Miltonic +Miltonic's +Miltown +Miltown's +Milwaukee +Milwaukee's +Mimi +Mimi's +Mimosa +Mimosa's +Minamoto +Minamoto's +Mindanao +Mindanao's +Mindoro +Mindoro's +Mindy +Mindy's +Minerva +Minerva's +Ming +Ming's +Mingus +Mingus's +Minneapolis +Minneapolis's +Minnelli +Minnelli's +Minnesota +Minnesota's +Minnesotan +Minnesotan's +Minnesotans +Minnie +Minnie's +Minoan +Minoan's +Minoans +Minolta +Minolta's +Minos +Minos's +Minot +Minot's +Minotaur +Minotaur's +Minsk +Minsk's +Minsky +Minsky's +Mintaka +Mintaka's +Minuit +Minuit's +Miocene +Miocene's +Mir +Mir's +Mira +Mira's +Mirabeau +Mirabeau's +Mirach +Mirach's +Miranda +Miranda's +Mirfak +Mirfak's +Miriam +Miriam's +Miro +Miro's +Mirzam +Mirzam's +Miskito +Miskito's +Miss +Mississauga +Mississauga's +Mississippi +Mississippi's +Mississippian +Mississippian's +Mississippians +Missouri +Missouri's +Missourian +Missourian's +Missourians +Missy +Missy's +Mistassini +Mistassini's +Mister +Misty +Misty's +Mitch +Mitch's +Mitchel +Mitchel's +Mitchell +Mitchell's +Mitford +Mitford's +Mithra +Mithra's +Mithridates +Mithridates's +Mitsubishi +Mitsubishi's +Mitterrand +Mitterrand's +Mitty +Mitty's +Mitzi +Mitzi's +Mixtec +Mixtec's +Mizar +Mizar's +Mn +Mn's +Mnemosyne +Mnemosyne's +Mo +Mo's +Mobil +Mobil's +Mobile +Mobile's +Mobutu +Mobutu's +Modesto +Modesto's +Modigliani +Modigliani's +Moe +Moe's +Moet +Moet's +Mogadishu +Mogadishu's +Mohacs +Mohacs's +Mohamed +Mohamed's +Mohammad +Mohammad's +Mohammed +Mohammed's +Mohammedan +Mohammedan's +Mohammedanism +Mohammedanism's +Mohammedanisms +Mohammedans +Mohawk +Mohawk's +Mohawks +Mohican +Mohican's +Mohicans +Moho +Moho's +Mohorovicic +Mohorovicic's +Moira +Moira's +Moises +Moises's +Moiseyev +Moiseyev's +Mojave +Mojave's +Moldavia +Moldavia's +Moldova +Moldova's +Moliere +Moliere's +Molina +Molina's +Moll +Moll's +Mollie +Mollie's +Molly +Molly's +Molnar +Molnar's +Moloch +Moloch's +Molokai +Molokai's +Molotov +Molotov's +Moluccas +Moluccas's +Mombasa +Mombasa's +Mona +Mona's +Monaco +Monaco's +Mondale +Mondale's +Monday +Monday's +Mondays +Mondrian +Mondrian's +Monera +Monera's +Monet +Monet's +MongoDB +MongoDB's +Mongol +Mongol's +Mongolia +Mongolia's +Mongolian +Mongolian's +Mongolians +Mongoloid +Mongols +Monica +Monica's +Monique +Monique's +Monk +Monk's +Monmouth +Monmouth's +Monongahela +Monongahela's +Monroe +Monroe's +Monrovia +Monrovia's +Mons +Monsanto +Monsanto's +Montague +Montague's +Montaigne +Montaigne's +Montana +Montana's +Montanan +Montanan's +Montanans +Montcalm +Montcalm's +Monte +Monte's +Montenegrin +Montenegrin's +Montenegro +Montenegro's +Monterrey +Monterrey's +Montesquieu +Montesquieu's +Montessori +Montessori's +Monteverdi +Monteverdi's +Montevideo +Montevideo's +Montezuma +Montezuma's +Montgolfier +Montgolfier's +Montgomery +Montgomery's +Monticello +Monticello's +Montoya +Montoya's +Montpelier +Montpelier's +Montrachet +Montrachet's +Montreal +Montreal's +Montserrat +Montserrat's +Monty +Monty's +Moody +Moody's +Moog +Moog's +Moon +Moon's +Mooney +Mooney's +Moor +Moor's +Moore +Moore's +Moorish +Moorish's +Moors +Morales +Morales's +Moran +Moran's +Moravia +Moravia's +Moravian +Moravian's +Mordred +Mordred's +More +More's +Moreno +Moreno's +Morgan +Morgan's +Moriarty +Moriarty's +Morin +Morin's +Morison +Morison's +Morita +Morita's +Morley +Morley's +Mormon +Mormon's +Mormonism +Mormonism's +Mormonisms +Mormons +Moro +Moro's +Moroccan +Moroccan's +Moroccans +Morocco +Morocco's +Moroni +Moroni's +Morpheus +Morpheus's +Morphy +Morphy's +Morris +Morris's +Morrison +Morrison's +Morrow +Morrow's +Morse +Morse's +Mort +Mort's +Mortimer +Mortimer's +Morton +Morton's +Mosaic +Mosaic's +Moscow +Moscow's +Moseley +Moseley's +Moselle +Moselle's +Moses +Moses's +Moslem +Moslem's +Moslems +Mosley +Mosley's +Moss +Moss's +Mosul +Mosul's +Motorola +Motorola's +Motown +Motown's +Motrin +Motrin's +Mott +Mott's +Mount +Mount's +Mountbatten +Mountbatten's +Mountie +Mountie's +Mounties +Moussorgsky +Moussorgsky's +Mouthe +Mouthe's +Mouton +Mouton's +Mowgli +Mowgli's +Mozambican +Mozambican's +Mozambicans +Mozambique +Mozambique's +Mozart +Mozart's +Mozilla +Mozilla's +Ms +Muawiya +Muawiya's +Mubarak +Mubarak's +Mueller +Mueller's +Muenster +Muenster's +Mugabe +Mugabe's +Muhammad +Muhammad's +Muhammadan +Muhammadan's +Muhammadanism +Muhammadanism's +Muhammadanisms +Muhammadans +Muir +Muir's +Mujib +Mujib's +Mulder +Mulder's +Mullen +Mullen's +Muller +Muller's +Mulligan +Mulligan's +Mullikan +Mullikan's +Mullins +Mullins's +Mulroney +Mulroney's +Multan +Multan's +Mumbai +Mumbai's +Mumford +Mumford's +Munch +Munch's +Munich +Munich's +Munoz +Munoz's +Munro +Munro's +Muppet +Muppet's +Murasaki +Murasaki's +Murat +Murat's +Murchison +Murchison's +Murdoch +Murdoch's +Muriel +Muriel's +Murillo +Murillo's +Murine +Murine's +Murmansk +Murmansk's +Murphy +Murphy's +Murray +Murray's +Murrow +Murrow's +Murrumbidgee +Murrumbidgee's +Muscat +Muscat's +Muscovite +Muscovite's +Muscovy +Muscovy's +Muse +Muse's +Musharraf +Musharraf's +Musial +Musial's +Muskogee +Muskogee's +Muslim +Muslim's +Muslims +Mussolini +Mussolini's +Mussorgsky +Mussorgsky's +Mutsuhito +Mutsuhito's +Muzak +Muzak's +MySQL +MySQL's +MySpace +MySpace's +Myanmar +Myanmar's +Mycenae +Mycenae's +Mycenaean +Mycenaean's +Myers +Myers's +Mylar +Mylar's +Mylars +Myles +Myles's +Myra +Myra's +Myrdal +Myrdal's +Myrna +Myrna's +Myron +Myron's +Myrtle +Myrtle's +Mysore +Mysore's +Myst +Myst's +Münchhausen +Münchhausen's +N +N's +NASCAR +NASCAR's +NORAD +NORAD's +NSA +NSA's +NVIDIA +NVIDIA's +Na +Na's +Nabisco +Nabisco's +Nabokov +Nabokov's +Nader +Nader's +Nadia +Nadia's +Nadine +Nadine's +Nagasaki +Nagasaki's +Nagoya +Nagoya's +Nagpur +Nagpur's +Nagy +Nagy's +Nahuatl +Nahuatl's +Nahum +Nahum's +Naipaul +Naipaul's +Nair +Nair's +Nairobi +Nairobi's +Naismith +Naismith's +Nam +Nam's +Namath +Namath's +Namibia +Namibia's +Namibian +Namibian's +Namibians +Nan +Nan's +Nanak +Nanak's +Nanchang +Nanchang's +Nancy +Nancy's +Nanette +Nanette's +Nanjing +Nanjing's +Nanking +Nanking's +Nankings +Nannie +Nannie's +Nanook +Nanook's +Nansen +Nansen's +Nantes +Nantes's +Nantucket +Nantucket's +Naomi +Naomi's +Naphtali +Naphtali's +Napier +Napier's +Naples +Naples's +Napoleon +Napoleon's +Napoleonic +Napoleonic's +Napster +Napster's +Narcissus +Narcissus's +Narmada +Narmada's +Narnia +Narnia's +Narragansett +Narragansett's +Nash +Nash's +Nashua +Nashua's +Nashville +Nashville's +Nassau +Nassau's +Nasser +Nasser's +Nat +Nat's +Natalia +Natalia's +Natalie +Natalie's +Natasha +Natasha's +Natchez +Natchez's +Nate +Nate's +Nathan +Nathan's +Nathaniel +Nathaniel's +Nathans +Nathans's +Nation +Nation's +Nationwide +Nationwide's +Naugahyde +Naugahyde's +Nauru +Nauru's +Nautilus +Nautilus's +Navaho +Navaho's +Navahoes +Navahos +Navajo +Navajo's +Navajoes +Navajos +Navarre +Navarre's +Navarro +Navarro's +Navratilova +Navratilova's +Nazarene +Nazarene's +Nazareth +Nazareth's +Nazca +Nazca's +Nazi +Nazi's +Naziism +Naziism's +Naziisms +Nazis +Nazism +Nazism's +Nazisms +Nd +Nd's +Ndjamena +Ndjamena's +Ne +Ne's +Neal +Neal's +Neanderthal +Neanderthal's +Neanderthals +Neapolitan +Neapolitan's +Nebraska +Nebraska's +Nebraskan +Nebraskan's +Nebraskans +Nebuchadnezzar +Nebuchadnezzar's +Ned +Ned's +Nefertiti +Nefertiti's +Negev +Negev's +Negro +Negro's +Negroes +Negroid +Negroid's +Negroids +Negros +Negros's +Nehemiah +Nehemiah's +Nehru +Nehru's +Neil +Neil's +Nelda +Nelda's +Nell +Nell's +Nellie +Nellie's +Nelly +Nelly's +Nelsen +Nelsen's +Nelson +Nelson's +Nembutal +Nembutal's +Nemesis +Nemesis's +Neo +Neo's +Neogene +Neogene's +Neolithic +Nepal +Nepal's +Nepalese +Nepalese's +Nepali +Nepali's +Neptune +Neptune's +Nereid +Nereid's +Nerf +Nerf's +Nero +Nero's +Neruda +Neruda's +Nescafe +Nescafe's +Nesselrode +Nesselrode's +Nestle +Nestle's +Nestor +Nestor's +Nestorius +Nestorius's +Netflix +Netflix's +Netherlander +Netherlander's +Netherlanders +Netherlands +Netherlands's +Netscape +Netscape's +Nettie +Nettie's +Netzahualcoyotl +Netzahualcoyotl's +Neva +Neva's +Nevada +Nevada's +Nevadan +Nevadan's +Nevadans +Nevis +Nevis's +Nevsky +Nevsky's +Newark +Newark's +Newcastle +Newcastle's +Newfoundland +Newfoundland's +Newfoundlands +Newman +Newman's +Newport +Newport's +Newsweek +Newsweek's +Newton +Newton's +Newtonian +Newtonian's +Nexis +Nexis's +Ngaliema +Ngaliema's +Nguyen +Nguyen's +Ni +Ni's +Niagara +Niagara's +Niamey +Niamey's +Nibelung +Nibelung's +Nicaea +Nicaea's +Nicaragua +Nicaragua's +Nicaraguan +Nicaraguan's +Nicaraguans +Niccolo +Niccolo's +Nice +Nice's +Nicene +Nicene's +Nichiren +Nichiren's +Nicholas +Nicholas's +Nichole +Nichole's +Nichols +Nichols's +Nicholson +Nicholson's +Nick +Nick's +Nickelodeon +Nickelodeon's +Nicklaus +Nicklaus's +Nickolas +Nickolas's +Nicobar +Nicobar's +Nicodemus +Nicodemus's +Nicola +Nicola's +Nicolas +Nicolas's +Nicole +Nicole's +Nicosia +Nicosia's +Niebuhr +Niebuhr's +Nielsen +Nielsen's +Nietzsche +Nietzsche's +Nieves +Nieves's +Nigel +Nigel's +Niger +Niger's +Nigeria +Nigeria's +Nigerian +Nigerian's +Nigerians +Nightingale +Nightingale's +Nijinsky +Nijinsky's +Nike +Nike's +Nikita +Nikita's +Nikkei +Nikkei's +Nikki +Nikki's +Nikolai +Nikolai's +Nikolayev +Nikolayev's +Nikon +Nikon's +Nile +Nile's +Nimitz +Nimitz's +Nimrod +Nimrod's +Nina +Nina's +Nineveh +Nineveh's +Nintendo +Nintendo's +Niobe +Niobe's +Nippon +Nippon's +Nirenberg +Nirenberg's +Nirvana +Nirvana's +Nisan +Nisan's +Nisei +Nisei's +Nissan +Nissan's +Nita +Nita's +Nivea +Nivea's +Nixon +Nixon's +Nkrumah +Nkrumah's +NoDoz +NoDoz's +Noah +Noah's +Nobel +Nobel's +Nobelist +Nobelist's +Nobelists +Noble +Noble's +Noe +Noe's +Noel +Noel's +Noelle +Noelle's +Noels +Noemi +Noemi's +Noh +Noh's +Nokia +Nokia's +Nola +Nola's +Nolan +Nolan's +Nome +Nome's +Nona +Nona's +Nootka +Nootka's +Nora +Nora's +Norbert +Norbert's +Norberto +Norberto's +Nordic +Nordic's +Nordics +Noreen +Noreen's +Norfolk +Norfolk's +Noriega +Noriega's +Norma +Norma's +Norman +Norman's +Normand +Normand's +Normandy +Normandy's +Normans +Norplant +Norplant's +Norris +Norris's +Norse +Norse's +Norseman +Norseman's +Norsemen +Norsemen's +North +North's +Northampton +Northampton's +Northeast +Northeast's +Northeasts +Northerner +Northerner's +Northrop +Northrop's +Northrup +Northrup's +Norths +Northwest +Northwest's +Northwests +Norton +Norton's +Norway +Norway's +Norwegian +Norwegian's +Norwegians +Norwich +Norwich's +Nosferatu +Nosferatu's +Nostradamus +Nostradamus's +Nottingham +Nottingham's +Nouakchott +Nouakchott's +Noumea +Noumea's +Nova +Nova's +Novartis +Novartis's +November +November's +Novembers +Novgorod +Novgorod's +Novocain +Novocain's +Novocaine +Novokuznetsk +Novokuznetsk's +Novosibirsk +Novosibirsk's +Noxzema +Noxzema's +Noyce +Noyce's +Noyes +Noyes's +Np +Np's +Nubia +Nubia's +Nubian +Nubian's +Nukualofa +Nukualofa's +Numbers +Numbers's +Nunavut +Nunavut's +Nunez +Nunez's +Nunki +Nunki's +Nuremberg +Nuremberg's +Nureyev +Nureyev's +NutraSweet +NutraSweet's +NyQuil +NyQuil's +Nyasa +Nyasa's +Nyerere +Nyerere's +O +O'Brien +O'Brien's +O'Casey +O'Casey's +O'Connell +O'Connell's +O'Connor +O'Connor's +O'Donnell +O'Donnell's +O'Hara +O'Hara's +O'Higgins +O'Higgins's +O'Keeffe +O'Keeffe's +O'Neil +O'Neil's +O'Neill +O'Neill's +O'Rourke +O'Rourke's +O'Toole +O'Toole's +O's +OHSA +OHSA's +OK +OK's +OKed +OKing +OKs +Oahu +Oahu's +Oakland +Oakland's +Oakley +Oakley's +Oates +Oates's +Oaxaca +Oaxaca's +Ob +Ob's +Obadiah +Obadiah's +Obama +Obama's +Obamacare +Oberlin +Oberlin's +Oberon +Oberon's +Ocaml +Ocaml's +Occam +Occam's +Occident +Occidental +Occidental's +Occidentals +Oceania +Oceania's +Oceanus +Oceanus's +Ochoa +Ochoa's +Oct +Oct's +Octavia +Octavia's +Octavio +Octavio's +October +October's +Octobers +Odell +Odell's +Oder +Oder's +Odessa +Odessa's +Odets +Odets's +Odin +Odin's +Odis +Odis's +Odom +Odom's +Odysseus +Odysseus's +Odyssey +Odyssey's +Oedipal +Oedipal's +Oedipus +Oedipus's +Oersted +Oersted's +Ofelia +Ofelia's +Offenbach +Offenbach's +OfficeMax +OfficeMax's +Ogbomosho +Ogbomosho's +Ogden +Ogden's +Ogilvy +Ogilvy's +Oglethorpe +Oglethorpe's +Ohio +Ohio's +Ohioan +Ohioan's +Ohioans +Oise +Oise's +Ojibwa +Ojibwa's +Ojibwas +Okeechobee +Okeechobee's +Okefenokee +Okefenokee's +Okhotsk +Okhotsk's +Okinawa +Okinawa's +Oklahoma +Oklahoma's +Oklahoman +Oklahoman's +Oktoberfest +Oktoberfest's +Ola +Ola's +Olaf +Olaf's +Olajuwon +Olajuwon's +Olav +Olav's +Oldenburg +Oldenburg's +Oldfield +Oldfield's +Oldsmobile +Oldsmobile's +Olduvai +Olduvai's +Olen +Olen's +Olenek +Olenek's +Olga +Olga's +Oligocene +Oligocene's +Olin +Olin's +Olive +Olive's +Oliver +Oliver's +Olivetti +Olivetti's +Olivia +Olivia's +Olivier +Olivier's +Ollie +Ollie's +Olmec +Olmec's +Olmsted +Olmsted's +Olsen +Olsen's +Olson +Olson's +Olympia +Olympia's +Olympiad +Olympiad's +Olympiads +Olympian +Olympian's +Olympians +Olympias +Olympic +Olympic's +Olympics +Olympics's +Olympus +Olympus's +Omaha +Omaha's +Omahas +Oman +Oman's +Omar +Omar's +Omayyad +Omayyad's +Omdurman +Omdurman's +Omsk +Omsk's +Onassis +Onassis's +Oneal +Oneal's +Onega +Onega's +Onegin +Onegin's +Oneida +Oneida's +Onion +Onion's +Ono +Ono's +Onondaga +Onondaga's +Onsager +Onsager's +Ontario +Ontario's +Oort +Oort's +Opal +Opal's +Opel +Opel's +OpenOffice +OpenOffice's +Ophelia +Ophelia's +Ophiuchus +Ophiuchus's +Oppenheimer +Oppenheimer's +Oprah +Oprah's +Ora +Ora's +Oracle +Oracle's +Oran +Oran's +Orange +Orange's +Oranjestad +Oranjestad's +Orbison +Orbison's +Ordovician +Ordovician's +Oregon +Oregon's +Oregonian +Oregonian's +Oregonians +Oreo +Oreo's +Orestes +Orestes's +Orient +Orient's +Oriental +Oriental's +Orientals +Orin +Orin's +Orinoco +Orinoco's +Orion +Orion's +Oriya +Oriya's +Orizaba +Orizaba's +Orkney +Orkney's +Orlando +Orlando's +Orleans +Orleans's +Orlon +Orlon's +Orlons +Orly +Orly's +Orpheus +Orpheus's +Orphic +Orphic's +Orr +Orr's +Ortega +Ortega's +Ortiz +Ortiz's +Orval +Orval's +Orville +Orville's +Orwell +Orwell's +Orwellian +Orwellian's +Os +Os's +Osage +Osage's +Osaka +Osaka's +Osbert +Osbert's +Osborn +Osborn's +Osborne +Osborne's +Oscar +Oscar's +Oscars +Osceola +Osceola's +Osgood +Osgood's +Oshawa +Oshawa's +Oshkosh +Oshkosh's +Osiris +Osiris's +Oslo +Oslo's +Osman +Osman's +Ostrogoth +Ostrogoth's +Ostwald +Ostwald's +Osvaldo +Osvaldo's +Oswald +Oswald's +Othello +Othello's +Otis +Otis's +Ottawa +Ottawa's +Ottawas +Otto +Otto's +Ottoman +Ottoman's +Ouagadougou +Ouagadougou's +Ouija +Ouija's +Ovid +Ovid's +Owen +Owen's +Owens +Owens's +Oxford +Oxford's +Oxfords +Oxnard +Oxnard's +Oxonian +Oxonian's +Oxus +Oxus's +Oxycontin +Oxycontin's +Oz +Oz's +Ozark +Ozark's +Ozarks +Ozarks's +Ozymandias +Ozymandias's +Ozzie +Ozzie's +P +P's +PHP +PHP's +Pa +Pa's +Paar +Paar's +Pablo +Pablo's +Pablum +Pablum's +Pabst +Pabst's +Pace +Pace's +Pacheco +Pacheco's +Pacific +Pacific's +Pacino +Pacino's +Packard +Packard's +Paderewski +Paderewski's +Padilla +Padilla's +Paganini +Paganini's +Page +Page's +Paglia +Paglia's +Pahlavi +Pahlavi's +Paige +Paige's +Paine +Paine's +Pakistan +Pakistan's +Pakistani +Pakistani's +Pakistanis +Palau +Palau's +Palembang +Palembang's +Paleocene +Paleocene's +Paleogene +Paleogene's +Paleolithic +Paleolithic's +Paleozoic +Paleozoic's +Palermo +Palermo's +Palestine +Palestine's +Palestinian +Palestinian's +Palestinians +Palestrina +Palestrina's +Paley +Paley's +Palikir +Palikir's +Palisades +Palisades's +Palladio +Palladio's +Palmer +Palmer's +Palmerston +Palmerston's +Palmolive +Palmolive's +Palmyra +Palmyra's +Palomar +Palomar's +Pam +Pam's +Pamela +Pamela's +Pamirs +Pamirs's +Pampers +Pampers's +Pan +Pan's +Panama +Panama's +Panamanian +Panamanian's +Panamanians +Panamas +Panasonic +Panasonic's +Pandora +Pandora's +Pangaea +Pangaea's +Pankhurst +Pankhurst's +Panmunjom +Panmunjom's +Pansy +Pansy's +Pantagruel +Pantagruel's +Pantaloon +Pantaloon's +Pantheon +Pantheon's +Panza +Panza's +Paracelsus +Paracelsus's +Paraclete +Paraclete's +Paradise +Paraguay +Paraguay's +Paraguayan +Paraguayan's +Paraguayans +Paramaribo +Paramaribo's +Paramount +Paramount's +Paraná +Paraná's +Parcheesi +Parcheesi's +Pareto +Pareto's +Paris +Paris's +Parisian +Parisian's +Parisians +Park +Park's +Parker +Parker's +Parkinson +Parkinson's +Parkman +Parkman's +Parks +Parks's +Parliament +Parliament's +Parmesan +Parmesan's +Parmesans +Parnassus +Parnassus's +Parnell +Parnell's +Parr +Parr's +Parrish +Parrish's +Parsi +Parsi's +Parsifal +Parsifal's +Parsons +Parsons's +Parthenon +Parthenon's +Parthia +Parthia's +Pasadena +Pasadena's +Pascal +Pascal's +Pasquale +Pasquale's +Passion +Passion's +Passions +Passover +Passover's +Passovers +Pasternak +Pasternak's +Pasteur +Pasteur's +Pat +Pat's +Patagonia +Patagonia's +Patagonian +Patagonian's +Pate +Pate's +Patel +Patel's +Paterson +Paterson's +Patna +Patna's +Patrica +Patrica's +Patrice +Patrice's +Patricia +Patricia's +Patrick +Patrick's +Patsy +Patsy's +Patterson +Patterson's +Patti +Patti's +Patton +Patton's +Patty +Patty's +Paul +Paul's +Paula +Paula's +Paulette +Paulette's +Pauli +Pauli's +Pauline +Pauline's +Pauling +Pauling's +Pavarotti +Pavarotti's +Pavlov +Pavlov's +Pavlova +Pavlova's +Pavlovian +Pavlovian's +Pawnee +Pawnee's +PayPal +PayPal's +Payne +Payne's +Pb +Pb's +Pd +Pd's +Peabody +Peabody's +Peace +Peace's +Peale +Peale's +Pearl +Pearl's +Pearlie +Pearlie's +Pearson +Pearson's +Peary +Peary's +Pechora +Pechora's +Peck +Peck's +Peckinpah +Peckinpah's +Pecos +Pecos's +Pedro +Pedro's +Peel +Peel's +Peg +Peg's +Pegasus +Pegasus's +Pegasuses +Peggy +Peggy's +Pei +Pei's +Peiping +Peiping's +Pekinese +Pekinese's +Pekineses +Peking +Peking's +Pekingese +Pekingese's +Pekingeses +Pekings +Pele +Pele's +Pelee +Pelee's +Peloponnese +Peloponnese's +Pembroke +Pembroke's +Pena +Pena's +Penderecki +Penderecki's +Penelope +Penelope's +Penn +Penn's +Penney +Penney's +Pennington +Pennington's +Pennsylvania +Pennsylvania's +Pennsylvanian +Pennsylvanian's +Pennsylvanians +Penny +Penny's +Pennzoil +Pennzoil's +Pensacola +Pensacola's +Pentagon +Pentagon's +Pentateuch +Pentateuch's +Pentax +Pentax's +Pentecost +Pentecost's +Pentecostal +Pentecostal's +Pentecostals +Pentecosts +Pentium +Pentium's +Peoria +Peoria's +Pepin +Pepin's +Pepsi +Pepsi's +Pepys +Pepys's +Pequot +Pequot's +Percheron +Percheron's +Percival +Percival's +Percy +Percy's +Perelman +Perelman's +Perez +Perez's +Periclean +Periclean's +Pericles +Pericles's +Perkins +Perkins's +Perl +Perl's +Perm +Perm's +Permalloy +Permalloy's +Permian +Permian's +Pernod +Pernod's +Peron +Peron's +Perot +Perot's +Perrier +Perrier's +Perry +Perry's +Perseid +Perseid's +Persephone +Persephone's +Persepolis +Persepolis's +Perseus +Perseus's +Pershing +Pershing's +Persia +Persia's +Persian +Persian's +Persians +Perth +Perth's +Peru +Peru's +Peruvian +Peruvian's +Peruvians +Peshawar +Peshawar's +Pete +Pete's +Peter +Peter's +Peters +Peters's +Petersen +Petersen's +Peterson +Peterson's +Petra +Petra's +Petrarch +Petrarch's +Petty +Petty's +Peugeot +Peugeot's +Pfizer +Pfizer's +Phaedra +Phaedra's +Phaethon +Phaethon's +Phanerozoic +Phanerozoic's +Pharaoh +Pharaoh's +Pharaohs +Pharisee +Pharisee's +Pharisees +Phekda +Phekda's +Phelps +Phelps's +Phidias +Phidias's +Philadelphia +Philadelphia's +Philby +Philby's +Philip +Philip's +Philippe +Philippe's +Philippians +Philippians's +Philippine +Philippine's +Philippines +Philippines's +Philips +Philips's +Philistine +Philistine's +Phillip +Phillip's +Phillipa +Phillipa's +Phillips +Phillips's +Philly +Philly's +Phipps +Phipps's +Phobos +Phobos's +Phoebe +Phoebe's +Phoenicia +Phoenicia's +Phoenix +Phoenix's +Photostat +Photostat's +Photostats +Photostatted +Photostatting +Phrygia +Phrygia's +Phyllis +Phyllis's +Piaf +Piaf's +Piaget +Piaget's +Pianola +Pianola's +Picasso +Picasso's +Piccadilly +Piccadilly's +Pickering +Pickering's +Pickett +Pickett's +Pickford +Pickford's +Pickwick +Pickwick's +Pict +Pict's +Piedmont +Piedmont's +Pierce +Pierce's +Pierre +Pierre's +Pierrot +Pierrot's +Pigmies +Pigmy +Pigmy's +Pike +Pike's +Pilate +Pilate's +Pilates +Pilates's +Pilcomayo +Pilcomayo's +Pilgrim +Pilgrim's +Pillsbury +Pillsbury's +Pinatubo +Pinatubo's +Pincus +Pincus's +Pindar +Pindar's +Pinkerton +Pinkerton's +Pinocchio +Pinocchio's +Pinochet +Pinochet's +Pinter +Pinter's +Pippin +Pippin's +Piraeus +Piraeus's +Pirandello +Pirandello's +Pisa +Pisa's +Pisces +Pisces's +Pisistratus +Pisistratus's +Pissaro +Pissaro's +Pitcairn +Pitcairn's +Pitt +Pitt's +Pittman +Pittman's +Pitts +Pitts's +Pittsburgh +Pittsburgh's +Pius +Pius's +Pizarro +Pizarro's +Planck +Planck's +Plantagenet +Plantagenet's +Plasticine +Plasticine's +Plataea +Plataea's +Plath +Plath's +Plato +Plato's +Platonic +Platonism +Platonism's +Platonist +Platonist's +Platte +Platte's +Plautus +Plautus's +PlayStation +PlayStation's +Playboy +Playboy's +Playtex +Playtex's +Pleiades +Pleiades's +Pleistocene +Pleistocene's +Plexiglas +Plexiglas's +Plexiglases +Pliny +Pliny's +Pliocene +Pliocene's +Plutarch +Plutarch's +Pluto +Pluto's +Plymouth +Plymouth's +Po +Po's +Pocahontas +Pocahontas's +Pocono +Pocono's +Poconos +Poconos's +Podgorica +Podgorica's +Podhoretz +Podhoretz's +Podunk +Podunk's +Poe +Poe's +Pogo +Pogo's +Poincaré +Poincaré's +Poiret +Poiret's +Poirot +Poirot's +Poisson +Poisson's +Poitier +Poitier's +Pokémon +Pokémon's +Poland +Poland's +Polanski +Polanski's +Polaris +Polaris's +Polaroid +Polaroid's +Polaroids +Pole +Pole's +Poles +Polish +Polish's +Politburo +Politburo's +Polk +Polk's +Pollard +Pollard's +Pollock +Pollock's +Pollux +Pollux's +Polly +Polly's +Pollyanna +Pollyanna's +Polo +Polo's +Poltava +Poltava's +Polyhymnia +Polyhymnia's +Polynesia +Polynesia's +Polynesian +Polynesian's +Polynesians +Polyphemus +Polyphemus's +Pomerania +Pomerania's +Pomeranian +Pomeranian's +Pomona +Pomona's +Pompadour +Pompadour's +Pompeii +Pompeii's +Pompey +Pompey's +Ponce +Ponce's +Pontchartrain +Pontchartrain's +Pontiac +Pontiac's +Pontianak +Pontianak's +Pooh +Pooh's +Poole +Poole's +Poona +Poona's +Pope +Pope's +Popeye +Popeye's +Popocatepetl +Popocatepetl's +Popper +Popper's +Poppins +Poppins's +Popsicle +Popsicle's +Porfirio +Porfirio's +Porrima +Porrima's +Porsche +Porsche's +Porter +Porter's +Portia +Portia's +Portland +Portland's +Portsmouth +Portsmouth's +Portugal +Portugal's +Portuguese +Portuguese's +Poseidon +Poseidon's +Post +Post's +PostgreSQL +PostgreSQL's +Potemkin +Potemkin's +Potomac +Potomac's +Potsdam +Potsdam's +Pottawatomie +Pottawatomie's +Potter +Potter's +Potts +Potts's +Pound +Pound's +Poussin +Poussin's +Powell +Powell's +PowerPC +PowerPC's +PowerPoint +PowerPoint's +Powers +Powers's +Powhatan +Powhatan's +Poznan +Poznan's +Prada +Prada's +Prado +Prado's +Praetorian +Praetorian's +Prague +Prague's +Praia +Praia's +Prakrit +Prakrit's +Pratchett +Pratchett's +Pratt +Pratt's +Pravda +Pravda's +Praxiteles +Praxiteles's +Preakness +Preakness's +Precambrian +Precambrian's +Preminger +Preminger's +Premyslid +Premyslid's +Prensa +Prensa's +Prentice +Prentice's +Presbyterian +Presbyterian's +Presbyterianism +Presbyterianism's +Presbyterians +Prescott +Prescott's +President +President's +Presidents +Presley +Presley's +Preston +Preston's +Pretoria +Pretoria's +Priam +Priam's +Pribilof +Pribilof's +Price +Price's +Priceline +Priceline's +Priestley +Priestley's +Prince +Prince's +Princeton +Princeton's +Principe +Principe's +Priscilla +Priscilla's +Prius +Prius's +Procrustean +Procrustean's +Procrustes +Procrustes's +Procter +Procter's +Procyon +Procyon's +Prohibition +Prokofiev +Prokofiev's +Promethean +Promethean's +Prometheus +Prometheus's +Proserpine +Proserpine's +Protagoras +Protagoras's +Proterozoic +Proterozoic's +Protestant +Protestant's +Protestantism +Protestantism's +Protestantisms +Protestants +Proteus +Proteus's +Proudhon +Proudhon's +Proust +Proust's +Provencals +Provence +Provence's +Provençal +Provençal's +Proverbs +Providence +Providence's +Providences +Provo +Provo's +Prozac +Prozac's +Prudence +Prudence's +Prudential +Prudential's +Pruitt +Pruitt's +Prussia +Prussia's +Prussian +Prussian's +Prut +Prut's +Pryor +Pryor's +Psalms +Psalms's +Psalter +Psalter's +Psalters +Psyche +Psyche's +Pt +Pt's +Ptah +Ptah's +Ptolemaic +Ptolemaic's +Ptolemies +Ptolemy +Ptolemy's +Pu +Pu's +Puccini +Puccini's +Puck +Puck's +Puckett +Puckett's +Puebla +Puebla's +Pueblo +Pueblo's +Puerto +Puget +Puget's +Pugh +Pugh's +Pulaski +Pulaski's +Pulitzer +Pulitzer's +Pullman +Pullman's +Pullmans +Punch +Punch's +Punic +Punic's +Punjab +Punjab's +Punjabi +Punjabi's +Purana +Purana's +Purcell +Purcell's +Purdue +Purdue's +Purim +Purim's +Purims +Purina +Purina's +Puritan +Puritan's +Puritanism +Puritanism's +Puritanisms +Purus +Purus's +Pusan +Pusan's +Pusey +Pusey's +Pushkin +Pushkin's +Pushtu +Pushtu's +Putin +Putin's +Putnam +Putnam's +Puzo +Puzo's +PyTorch +PyTorch's +Pygmalion +Pygmalion's +Pygmies +Pygmy +Pygmy's +Pyle +Pyle's +Pym +Pym's +Pynchon +Pynchon's +Pyongyang +Pyongyang's +Pyotr +Pyotr's +Pyrenees +Pyrenees's +Pyrex +Pyrex's +Pyrexes +Pyrrhic +Pyrrhic's +Pythagoras +Pythagoras's +Pythagorean +Pythagorean's +Pythias +Pythias's +Python +Python's +Pétain +Pétain's +Pôrto +Pôrto's +Q +Qaddafi +Qaddafi's +Qantas +Qantas's +Qatar +Qatar's +Qingdao +Qingdao's +Qiqihar +Qiqihar's +Qom +Qom's +Quaalude +Quaalude's +Quaker +Quaker's +Quakers +Qualcomm +Qualcomm's +Quaoar +Quaoar's +Quasimodo +Quasimodo's +Quaternary +Quaternary's +Quayle +Quayle's +Quebec +Quebec's +Quechua +Quechua's +Queen +Queen's +Queens +Queens's +Queensland +Queensland's +Quentin +Quentin's +Quetzalcoatl +Quetzalcoatl's +Quezon +Quezon's +Quincy +Quincy's +Quinn +Quinn's +Quintilian +Quintilian's +Quinton +Quinton's +Quirinal +Quirinal's +Quisling +Quisling's +Quito +Quito's +Quixote +Quixote's +Quixotism +Quixotism's +Qumran +Qumran's +Quonset +Quonset's +Qur'an +Quran +Québecois +Québecois's +R +R's +RCA +RCA's +RDS +RDS's +Ra +Ra's +Rabat +Rabat's +Rabelais +Rabelais's +Rabelaisian +Rabelaisian's +Rabin +Rabin's +Rachael +Rachael's +Rachel +Rachel's +Rachelle +Rachelle's +Rachmaninoff +Rachmaninoff's +Racine +Racine's +Radcliffe +Radcliffe's +Rae +Rae's +Rafael +Rafael's +Raffles +Raffles's +Ragnarök +Ragnarök's +Rainier +Rainier's +Raleigh +Raleigh's +Ralph +Ralph's +Rama +Rama's +Ramada +Ramada's +Ramadan +Ramadan's +Ramadans +Ramakrishna +Ramakrishna's +Ramanujan +Ramanujan's +Ramayana +Ramayana's +Rambo +Rambo's +Ramirez +Ramirez's +Ramiro +Ramiro's +Ramon +Ramon's +Ramona +Ramona's +Ramos +Ramos's +Ramsay +Ramsay's +Ramses +Ramses's +Ramsey +Ramsey's +Rand +Rand's +Randal +Randal's +Randall +Randall's +Randell +Randell's +Randi +Randi's +Randolph +Randolph's +Randy +Randy's +Rangoon +Rangoon's +Rankin +Rankin's +Rankine +Rankine's +Raoul +Raoul's +Raphael +Raphael's +Rapunzel +Rapunzel's +Raquel +Raquel's +Rasalgethi +Rasalgethi's +Rasalhague +Rasalhague's +Rasmussen +Rasmussen's +Rasputin +Rasputin's +Rasta +Rastaban +Rastaban's +Rastafarian +Rastafarian's +Rastafarianism +Rather +Rather's +Ratliff +Ratliff's +Raul +Raul's +Ravel +Ravel's +Rawalpindi +Rawalpindi's +Ray +Ray's +RayBan +RayBan's +Rayburn +Rayburn's +Rayleigh +Rayleigh's +Raymond +Raymond's +Raymundo +Raymundo's +Reagan +Reagan's +Reaganomics +Reaganomics's +Realtor +Realtor's +Reasoner +Reasoner's +Reba +Reba's +Rebecca +Rebecca's +Rebekah +Rebekah's +Recife +Recife's +Red +Red's +Redford +Redford's +Redgrave +Redgrave's +Redis +Redis's +Redmond +Redmond's +Redshift +Redshift's +Reebok +Reebok's +Reed +Reed's +Reese +Reese's +Reeves +Reeves's +Refugio +Refugio's +Reggie +Reggie's +Regina +Regina's +Reginae +Reginae's +Reginald +Reginald's +Regor +Regor's +Regulus +Regulus's +Rehnquist +Rehnquist's +Reich +Reich's +Reichstag +Reichstag's +Reid +Reid's +Reilly +Reilly's +Reinaldo +Reinaldo's +Reinhardt +Reinhardt's +Reinhold +Reinhold's +Remarque +Remarque's +Rembrandt +Rembrandt's +Remington +Remington's +Remus +Remus's +Rena +Rena's +Renaissance +Renaissance's +Renaissances +Renault +Renault's +Rene +Rene's +Renee +Renee's +Reno +Reno's +Renoir +Renoir's +Representative +Republican +Republican's +Republicans +Resurrection +Reuben +Reuben's +Reunion +Reunion's +Reuters +Reuters's +Reuther +Reuther's +Reva +Reva's +Revelations +Revelations's +Revere +Revere's +Reverend +Reverend's +Revlon +Revlon's +Rex +Rex's +Reyes +Reyes's +Reykjavik +Reykjavik's +Reyna +Reyna's +Reynaldo +Reynaldo's +Reynolds +Reynolds's +Rhea +Rhea's +Rhee +Rhee's +Rheingau +Rheingau's +Rhenish +Rhenish's +Rhiannon +Rhiannon's +Rhine +Rhine's +Rhineland +Rhineland's +Rhoda +Rhoda's +Rhode +Rhodes +Rhodes's +Rhodesia +Rhodesia's +Rhonda +Rhonda's +Rhone +Rhone's +Ribbentrop +Ribbentrop's +Ricardo +Ricardo's +Rice +Rice's +Rich +Rich's +Richard +Richard's +Richards +Richards's +Richardson +Richardson's +Richelieu +Richelieu's +Richie +Richie's +Richmond +Richmond's +Richter +Richter's +Richthofen +Richthofen's +Rick +Rick's +Rickenbacker +Rickenbacker's +Rickey +Rickey's +Rickie +Rickie's +Rickover +Rickover's +Ricky +Ricky's +Rico +Rico's +Riddle +Riddle's +Ride +Ride's +Riefenstahl +Riefenstahl's +Riel +Riel's +Riemann +Riemann's +Riesling +Riesling's +Riga +Riga's +Rigel +Rigel's +Riggs +Riggs's +Rigoberto +Rigoberto's +Rigoletto +Rigoletto's +Riley +Riley's +Rilke +Rilke's +Rimbaud +Rimbaud's +Ringling +Ringling's +Ringo +Ringo's +Rio +Rio's +Rios +Rios's +Ripley +Ripley's +Risorgimento +Risorgimento's +Rita +Rita's +Ritalin +Ritalin's +Ritz +Ritz's +Rivas +Rivas's +Rivera +Rivera's +Rivers +Rivers's +Riverside +Riviera +Riviera's +Rivieras +Riyadh +Riyadh's +Rizal +Rizal's +Rn +Rn's +Roach +Roach's +Rob +Rob's +Robbie +Robbie's +Robbin +Robbin's +Robbins +Robbins's +Robby +Robby's +Roberson +Roberson's +Robert +Robert's +Roberta +Roberta's +Roberto +Roberto's +Roberts +Roberts's +Robertson +Robertson's +Robeson +Robeson's +Robespierre +Robespierre's +Robin +Robin's +Robinson +Robinson's +Robitussin +Robitussin's +Robles +Robles's +Robson +Robson's +Robt +Robt's +Robyn +Robyn's +Rocco +Rocco's +Rocha +Rocha's +Rochambeau +Rochambeau's +Roche +Roche's +Rochelle +Rochelle's +Rochester +Rochester's +Rock +Rock's +Rockefeller +Rockefeller's +Rockford +Rockford's +Rockies +Rockies's +Rockne +Rockne's +Rockwell +Rockwell's +Rocky +Rocky's +Rod +Rod's +Roddenberry +Roddenberry's +Roderick +Roderick's +Rodger +Rodger's +Rodgers +Rodgers's +Rodin +Rodin's +Rodney +Rodney's +Rodolfo +Rodolfo's +Rodrick +Rodrick's +Rodrigo +Rodrigo's +Rodriguez +Rodriguez's +Rodriquez +Rodriquez's +Roeg +Roeg's +Roentgen +Roentgen's +Rogelio +Rogelio's +Roger +Roger's +Rogers +Rogers's +Roget +Roget's +Rojas +Rojas's +Roku +Roku's +Rolaids +Rolaids's +Roland +Roland's +Rolando +Rolando's +Rolex +Rolex's +Rolland +Rolland's +Rollerblade +Rollerblade's +Rollins +Rollins's +Rolodex +Rolodex's +Rolvaag +Rolvaag's +Roman +Roman's +Romanesque +Romanesque's +Romania +Romania's +Romanian +Romanian's +Romanians +Romanies +Romano +Romano's +Romanov +Romanov's +Romans +Romans's +Romansh +Romansh's +Romanticism +Romany +Romany's +Rome +Rome's +Romeo +Romeo's +Romero +Romero's +Romes +Rommel +Rommel's +Romney +Romney's +Romulus +Romulus's +Ron +Ron's +Ronald +Ronald's +Ronda +Ronda's +Ronnie +Ronnie's +Ronny +Ronny's +Ronstadt +Ronstadt's +Rooney +Rooney's +Roosevelt +Roosevelt's +Root +Root's +Roquefort +Roquefort's +Roqueforts +Rorschach +Rorschach's +Rory +Rory's +Rosa +Rosa's +Rosales +Rosales's +Rosalie +Rosalie's +Rosalind +Rosalind's +Rosalinda +Rosalinda's +Rosalyn +Rosalyn's +Rosanna +Rosanna's +Rosanne +Rosanne's +Rosario +Rosario's +Roscoe +Roscoe's +Rose +Rose's +Roseann +Roseann's +Roseau +Roseau's +Rosecrans +Rosecrans's +Rosella +Rosella's +Rosemarie +Rosemarie's +Rosemary +Rosemary's +Rosenberg +Rosenberg's +Rosendo +Rosendo's +Rosenzweig +Rosenzweig's +Rosetta +Rosetta's +Rosicrucian +Rosicrucian's +Rosie +Rosie's +Roslyn +Roslyn's +Ross +Ross's +Rossetti +Rossetti's +Rossini +Rossini's +Rostand +Rostand's +Rostov +Rostov's +Rostropovich +Rostropovich's +Roswell +Roswell's +Rotarian +Rotarian's +Roth +Roth's +Rothko +Rothko's +Rothschild +Rothschild's +Rotterdam +Rotterdam's +Rottweiler +Rottweiler's +Rouault +Rouault's +Roumania +Roumania's +Rourke +Rourke's +Rousseau +Rousseau's +Rove +Rove's +Rover +Rover's +Rowe +Rowe's +Rowena +Rowena's +Rowland +Rowland's +Rowling +Rowling's +Roxanne +Roxanne's +Roxie +Roxie's +Roxy +Roxy's +Roy +Roy's +Royal +Royal's +Royce +Royce's +Rozelle +Rozelle's +Rubaiyat +Rubaiyat's +Rubbermaid +Rubbermaid's +Ruben +Ruben's +Rubens +Rubens's +Rubicon +Rubicon's +Rubik +Rubik's +Rubin +Rubin's +Rubinstein +Rubinstein's +Ruby +Ruby's +Ruchbah +Ruchbah's +Rudolf +Rudolf's +Rudolph +Rudolph's +Rudy +Rudy's +Rudyard +Rudyard's +Rufus +Rufus's +Ruhr +Ruhr's +Ruiz +Ruiz's +Rukeyser +Rukeyser's +Rumania +Rumania's +Rumpelstiltskin +Rumpelstiltskin's +Rumsfeld +Rumsfeld's +Runnymede +Runnymede's +Runyon +Runyon's +Rupert +Rupert's +Rush +Rush's +Rushdie +Rushdie's +Rushmore +Rushmore's +Ruskin +Ruskin's +Russel +Russel's +Russell +Russell's +Russia +Russia's +Russian +Russian's +Russians +Russo +Russo's +Rustbelt +Rustbelt's +Rusty +Rusty's +Rutan +Rutan's +Rutgers +Rutgers's +Ruth +Ruth's +Rutherford +Rutherford's +Ruthie +Ruthie's +Rutledge +Rutledge's +Rwanda +Rwanda's +Rwandan +Rwandan's +Rwandans +Rwandas +Ryan +Ryan's +Rydberg +Rydberg's +Ryder +Ryder's +Ryukyu +Ryukyu's +S +S's +SAP +SAP's +SARS +SARS's +SQLite +SQLite's +SUSE +SUSE's +SVN +SVN's +Saab +Saab's +Saar +Saar's +Saarinen +Saarinen's +Saatchi +Saatchi's +Sabbath +Sabbath's +Sabbaths +Sabik +Sabik's +Sabin +Sabin's +Sabina +Sabina's +Sabine +Sabine's +Sabre +Sabre's +Sabrina +Sabrina's +Sacajawea +Sacajawea's +Sacco +Sacco's +Sachs +Sachs's +Sacramento +Sacramento's +Sadat +Sadat's +Saddam +Saddam's +Sadducee +Sadducee's +Sade +Sade's +Sadie +Sadie's +Sadr +Sadr's +Safavid +Safavid's +Safeway +Safeway's +Sagan +Sagan's +Saginaw +Saginaw's +Sagittarius +Sagittarius's +Sagittariuses +Sahara +Sahara's +Sahel +Sahel's +Saigon +Saigon's +Saiph +Saiph's +Sakai +Sakai's +Sakha +Sakha's +Sakhalin +Sakhalin's +Sakharov +Sakharov's +Saki +Saki's +Saks +Saks's +Sal +Sal's +Saladin +Saladin's +Salado +Salado's +Salamis +Salamis's +Salas +Salas's +Salazar +Salazar's +Salem +Salem's +Salerno +Salerno's +Salesforce +Salesforce's +Salinas +Salinas's +Salinger +Salinger's +Salisbury +Salisbury's +Salish +Salish's +Salk +Salk's +Sallie +Sallie's +Sallust +Sallust's +Sally +Sally's +Salome +Salome's +Salton +Salton's +Salvador +Salvador's +Salvadoran +Salvadoran's +Salvadorans +Salvadorian +Salvadorian's +Salvadorians +Salvatore +Salvatore's +Salween +Salween's +Salyut +Salyut's +Samantha +Samantha's +Samar +Samar's +Samara +Samara's +Samaritan +Samaritan's +Samaritans +Samarkand +Samarkand's +Sammie +Sammie's +Sammy +Sammy's +Samoa +Samoa's +Samoan +Samoan's +Samoset +Samoset's +Samoyed +Samoyed's +Sampson +Sampson's +Samson +Samson's +Samsonite +Samsonite's +Samsung +Samsung's +Samuel +Samuel's +Samuelson +Samuelson's +San +San's +Sana +Sana's +Sanchez +Sanchez's +Sancho +Sancho's +Sand +Sand's +Sandburg +Sandburg's +Sanders +Sanders's +Sandinista +Sandinista's +Sandoval +Sandoval's +Sandra +Sandra's +Sandy +Sandy's +Sanford +Sanford's +Sanforized +Sanforized's +Sang +Sang's +Sanger +Sanger's +Sanhedrin +Sanhedrin's +Sanka +Sanka's +Sankara +Sankara's +Sanskrit +Sanskrit's +Santa +Santa's +Santana +Santana's +Santayana +Santayana's +Santeria +Santeria's +Santiago +Santiago's +Santos +Santos's +Sappho +Sappho's +Sapporo +Sapporo's +Sara +Sara's +Saracen +Saracen's +Saracens +Saragossa +Saragossa's +Sarah +Sarah's +Sarajevo +Sarajevo's +Saran +Saran's +Sarasota +Sarasota's +Saratov +Saratov's +Sarawak +Sarawak's +Sardinia +Sardinia's +Sargasso +Sargasso's +Sargent +Sargent's +Sargon +Sargon's +Sarnoff +Sarnoff's +Saroyan +Saroyan's +Sarto +Sarto's +Sartre +Sartre's +Sasha +Sasha's +Saskatchewan +Saskatchewan's +Saskatoon +Saskatoon's +Sasquatch +Sasquatch's +Sassanian +Sassanian's +Sassoon +Sassoon's +Satan +Satan's +Satanism +Satanism's +Satanist +Satanist's +Saturday +Saturday's +Saturdays +Saturn +Saturn's +Saturnalia +Saturnalia's +Saudi +Saudi's +Saudis +Saul +Saul's +Saunders +Saunders's +Saundra +Saundra's +Saussure +Saussure's +Sauterne +Sauterne's +Savage +Savage's +Savannah +Savannah's +Savior +Savior's +Savonarola +Savonarola's +Savoy +Savoy's +Savoyard +Savoyard's +Sawyer +Sawyer's +Saxon +Saxon's +Saxons +Saxony +Saxony's +Sayers +Sayers's +Sb +Sb's +Scala +Scala's +Scandinavia +Scandinavia's +Scandinavian +Scandinavian's +Scandinavians +Scaramouch +Scaramouch's +Scarborough +Scarborough's +Scarlatti +Scarlatti's +Scheat +Scheat's +Schedar +Schedar's +Scheherazade +Scheherazade's +Schelling +Schelling's +Schenectady +Schenectady's +Schiaparelli +Schiaparelli's +Schick +Schick's +Schiller +Schiller's +Schindler +Schindler's +Schlesinger +Schlesinger's +Schliemann +Schliemann's +Schlitz +Schlitz's +Schmidt +Schmidt's +Schnabel +Schnabel's +Schnauzer +Schnauzer's +Schneider +Schneider's +Schoenberg +Schoenberg's +Schopenhauer +Schopenhauer's +Schrieffer +Schrieffer's +Schroeder +Schroeder's +Schrödinger +Schrödinger's +Schubert +Schubert's +Schultz +Schultz's +Schulz +Schulz's +Schumann +Schumann's +Schumpeter +Schumpeter's +Schuyler +Schuyler's +Schuylkill +Schuylkill's +Schwartz +Schwartz's +Schwarzenegger +Schwarzenegger's +Schwarzkopf +Schwarzkopf's +Schweitzer +Schweitzer's +Schweppes +Schweppes's +Schwinger +Schwinger's +Schwinn +Schwinn's +Scientology +Scientology's +Scipio +Scipio's +Scopes +Scopes's +Scorpio +Scorpio's +Scorpios +Scorpius +Scorpius's +Scorsese +Scorsese's +Scot +Scot's +Scotch +Scotch's +Scotches +Scotchman +Scotchman's +Scotchmen +Scotchmen's +Scotia +Scotia's +Scotland +Scotland's +Scots +Scotsman +Scotsman's +Scotsmen +Scotsmen's +Scotswoman +Scotswoman's +Scotswomen +Scotswomen's +Scott +Scott's +Scottie +Scottie's +Scottish +Scottish's +Scottsdale +Scottsdale's +Scotty +Scotty's +Scout +Scrabble +Scrabble's +Scranton +Scranton's +Scriabin +Scriabin's +Scribner +Scribner's +Scripture +Scripture's +Scriptures +Scrooge +Scrooge's +Scruggs +Scruggs's +Scud +Scud's +Sculley +Sculley's +Scylla +Scylla's +Scythia +Scythia's +Scythian +Scythian's +Se +Se's +Seaborg +Seaborg's +Seagram +Seagram's +Sean +Sean's +Sears +Sears's +Seattle +Seattle's +Sebastian +Sebastian's +Seconal +Seconal's +Secretariat +Secretariat's +Secretary +Seder +Seder's +Seders +Sedna +Sedna's +Seebeck +Seebeck's +Seeger +Seeger's +Sega +Sega's +Segovia +Segovia's +Segre +Segre's +Segundo +Segundo's +Seiko +Seiko's +Seine +Seine's +Seinfeld +Seinfeld's +Sejong +Sejong's +Selassie +Selassie's +Selectric +Selectric's +Selena +Selena's +Seleucid +Seleucid's +Seleucus +Seleucus's +Selim +Selim's +Seljuk +Seljuk's +Selkirk +Selkirk's +Sellers +Sellers's +Selma +Selma's +Selznick +Selznick's +Semarang +Semarang's +Seminole +Seminole's +Seminoles +Semiramis +Semiramis's +Semite +Semite's +Semites +Semitic +Semitic's +Semitics +Semtex +Semtex's +Senate +Senate's +Senates +Senator +Sendai +Sendai's +Seneca +Seneca's +Senecas +Senegal +Senegal's +Senegalese +Senegalese's +Senghor +Senghor's +Senior +Senior's +Sennacherib +Sennacherib's +Sennett +Sennett's +Sensurround +Sensurround's +Seoul +Seoul's +Sephardi +Sephardi's +Sepoy +Sepoy's +September +September's +Septembers +Septuagint +Septuagint's +Septuagints +Sequoya +Sequoya's +Serb +Serb's +Serbia +Serbia's +Serbian +Serbian's +Serbians +Serbs +Serena +Serena's +Serengeti +Serengeti's +Sergei +Sergei's +Sergio +Sergio's +Serpens +Serpens's +Serra +Serra's +Serrano +Serrano's +Set +Set's +Seth +Seth's +Seton +Seton's +Seurat +Seurat's +Seuss +Seuss's +Sevastopol +Sevastopol's +Severn +Severn's +Severus +Severus's +Seville +Seville's +Seward +Seward's +Sextans +Sextans's +Sexton +Sexton's +Seychelles +Seychelles's +Seyfert +Seyfert's +Seymour +Seymour's +Shackleton +Shackleton's +Shaffer +Shaffer's +Shaka +Shaka's +Shakespeare +Shakespeare's +Shakespearean +Shakespearean's +Shana +Shana's +Shane +Shane's +Shanghai +Shanghai's +Shankara +Shankara's +Shanna +Shanna's +Shannon +Shannon's +Shantung +Shantung's +Shapiro +Shapiro's +SharePoint +SharePoint's +Shari +Shari'a +Shari'a's +Shari's +Sharif +Sharif's +Sharlene +Sharlene's +Sharon +Sharon's +Sharp +Sharp's +Sharpe +Sharpe's +Sharron +Sharron's +Shasta +Shasta's +Shaula +Shaula's +Shaun +Shaun's +Shauna +Shauna's +Shavian +Shavian's +Shavuot +Shavuot's +Shaw +Shaw's +Shawn +Shawn's +Shawna +Shawna's +Shawnee +Shawnee's +Shcharansky +Shcharansky's +Shea +Shea's +Sheba +Sheba's +Shebeli +Shebeli's +Sheena +Sheena's +Sheetrock +Sheetrock's +Sheffield +Sheffield's +Sheila +Sheila's +Shelby +Shelby's +Sheldon +Sheldon's +Shelia +Shelia's +Shell +Shell's +Shelley +Shelley's +Shelly +Shelly's +Shelton +Shelton's +Shenandoah +Shenandoah's +Shenyang +Shenyang's +Sheol +Sheol's +Shepard +Shepard's +Shepherd +Shepherd's +Sheppard +Sheppard's +Sheratan +Sheratan's +Sheraton +Sheraton's +Sheree +Sheree's +Sheri +Sheri's +Sheridan +Sheridan's +Sherlock +Sherlock's +Sherman +Sherman's +Sherpa +Sherpa's +Sherri +Sherri's +Sherrie +Sherrie's +Sherry +Sherry's +Sherwood +Sherwood's +Sheryl +Sheryl's +Shetland +Shetland's +Shetlands +Shetlands's +Shevardnadze +Shevardnadze's +Shevat +Shevat's +Shi'ite +Shi'ite's +Shields +Shields's +Shijiazhuang +Shijiazhuang's +Shikoku +Shikoku's +Shillong +Shillong's +Shiloh +Shiloh's +Shinto +Shinto's +Shintoism +Shintoism's +Shintoisms +Shintos +Shiraz +Shiraz's +Shirley +Shirley's +Shiva +Shiva's +Shockley +Shockley's +Short +Short's +Shorthorn +Shorthorn's +Shoshone +Shoshone's +Shostakovitch +Shostakovitch's +Shrek +Shrek's +Shreveport +Shreveport's +Shriner +Shriner's +Shropshire +Shropshire's +Shula +Shula's +Shylock +Shylock's +Shylockian +Shylockian's +Si +Si's +Siam +Siam's +Siamese +Siamese's +Sian +Sian's +Sibelius +Sibelius's +Siberia +Siberia's +Siberian +Siberian's +Sibyl +Sibyl's +Sicilian +Sicilian's +Sicilians +Sicily +Sicily's +Sid +Sid's +Siddhartha +Siddhartha's +Sidney +Sidney's +Siegfried +Siegfried's +Siemens +Siemens's +Sierpinski +Sierpinski's +Sigismund +Sigismund's +Sigmund +Sigmund's +Sigurd +Sigurd's +Sihanouk +Sihanouk's +Sikh +Sikh's +Sikhism +Sikhs +Sikkim +Sikkim's +Sikkimese +Sikkimese's +Sikorsky +Sikorsky's +Silas +Silas's +Silurian +Silurian's +Silva +Silva's +Silvia +Silvia's +Simenon +Simenon's +Simmental +Simmental's +Simmons +Simmons's +Simon +Simon's +Simone +Simone's +Simpson +Simpson's +Simpsons +Simpsons's +Sims +Sims's +Sinai +Sinai's +Sinatra +Sinatra's +Sinclair +Sinclair's +Sindbad +Sindbad's +Sindhi +Sindhi's +Singapore +Singapore's +Singer +Singer's +Singh +Singh's +Singleton +Singleton's +Sinhalese +Sinhalese's +Sinkiang +Sinkiang's +Sioux +Sioux's +Sirius +Sirius's +Sister +Sister's +Sisters +Sistine +Sistine's +Sisyphean +Sisyphean's +Sisyphus +Sisyphus's +Siva +Siva's +Sivan +Sivan's +Sjaelland +Sjaelland's +Skinner +Skinner's +Skippy +Skippy's +Skopje +Skopje's +Skye +Skye's +Skylab +Skylab's +Skype +Skype's +Slackware +Slackware's +Slashdot +Slashdot's +Slater +Slater's +Slav +Slav's +Slavic +Slavic's +Slavonic +Slavonic's +Slavs +Slinky +Slinky's +Sloan +Sloan's +Sloane +Sloane's +Slocum +Slocum's +Slovak +Slovak's +Slovakia +Slovakia's +Slovakian +Slovaks +Slovenia +Slovenia's +Slovenian +Slovenian's +Slovenians +Slurpee +Slurpee's +Small +Small's +Smetana +Smetana's +Smirnoff +Smirnoff's +Smith +Smith's +Smithson +Smithson's +Smithsonian +Smithsonian's +Smokey +Smokey's +Smolensk +Smolensk's +Smollett +Smollett's +Smuts +Smuts's +Sn +Sn's +Snake +Snake's +Snapple +Snapple's +Snead +Snead's +Snell +Snell's +Snickers +Snickers's +Snider +Snider's +Snoopy +Snoopy's +Snow +Snow's +Snowbelt +Snowbelt's +Snyder +Snyder's +Soave +Soave's +Socorro +Socorro's +Socrates +Socrates's +Socratic +Socratic's +Soddy +Soddy's +Sodom +Sodom's +Sofia +Sofia's +Soho +Soho's +Solis +Solis's +Solomon +Solomon's +Solon +Solon's +Solzhenitsyn +Solzhenitsyn's +Somali +Somali's +Somalia +Somalia's +Somalian +Somalian's +Somalians +Somalis +Somme +Somme's +Somoza +Somoza's +Son +Son's +Sondheim +Sondheim's +Sondra +Sondra's +Songhai +Songhai's +Songhua +Songhua's +Sonia +Sonia's +Sonja +Sonja's +Sonny +Sonny's +Sontag +Sontag's +Sony +Sony's +Sonya +Sonya's +Sophia +Sophia's +Sophie +Sophie's +Sophoclean +Sophoclean's +Sophocles +Sophocles's +Sopwith +Sopwith's +Sorbonne +Sorbonne's +Sosa +Sosa's +Soto +Soto's +Souphanouvong +Souphanouvong's +Sourceforge +Sourceforge's +Sousa +Sousa's +South +South's +Southampton +Southampton's +Southeast +Southeast's +Southeasts +Southerner +Southerner's +Southerners +Southey +Southey's +Souths +Southwest +Southwest's +Southwests +Soviet +Soviet's +Soweto +Soweto's +Soyinka +Soyinka's +Soyuz +Soyuz's +Spaatz +Spaatz's +Spackle +Spackle's +Spahn +Spahn's +Spain +Spain's +Spam +Spam's +Spaniard +Spaniard's +Spaniards +Spanish +Spanish's +Sparks +Sparks's +Sparta +Sparta's +Spartacus +Spartacus's +Spartan +Spartan's +Spartans +Spears +Spears's +Speer +Speer's +Spence +Spence's +Spencer +Spencer's +Spencerian +Spencerian's +Spengler +Spengler's +Spenglerian +Spenglerian's +Spenser +Spenser's +Spenserian +Spenserian's +Sperry +Sperry's +Sphinx +Sphinx's +Spica +Spica's +Spielberg +Spielberg's +Spillane +Spillane's +Spinoza +Spinoza's +Spinx +Spinx's +Spiro +Spiro's +Spirograph +Spirograph's +Spitsbergen +Spitsbergen's +Spitz +Spitz's +Spock +Spock's +Spokane +Spokane's +Springfield +Springfield's +Springsteen +Springsteen's +Sprint +Sprint's +Sprite +Sprite's +Sputnik +Sputnik's +Squanto +Squanto's +Squibb +Squibb's +Srinagar +Srinagar's +Srivijaya +Srivijaya's +Stacey +Stacey's +Staci +Staci's +Stacie +Stacie's +Stacy +Stacy's +Stael +Stael's +Stafford +Stafford's +StairMaster +StairMaster's +Stalin +Stalin's +Stalingrad +Stalingrad's +Stalinist +Stalinist's +Stallone +Stallone's +Stamford +Stamford's +Stan +Stan's +Standish +Standish's +Stanford +Stanford's +Stanislavsky +Stanislavsky's +Stanley +Stanley's +Stanton +Stanton's +Staples +Staples's +Starbucks +Starbucks's +Stark +Stark's +Starkey +Starkey's +Starr +Starr's +Staten +Staten's +Staubach +Staubach's +Steadicam +Steadicam's +Steele +Steele's +Stefan +Stefan's +Stefanie +Stefanie's +Stein +Stein's +Steinbeck +Steinbeck's +Steinem +Steinem's +Steiner +Steiner's +Steinmetz +Steinmetz's +Steinway +Steinway's +Stella +Stella's +Stendhal +Stendhal's +Stengel +Stengel's +Stephan +Stephan's +Stephanie +Stephanie's +Stephen +Stephen's +Stephens +Stephens's +Stephenson +Stephenson's +Sterling +Sterling's +Stern +Stern's +Sterne +Sterne's +Sterno +Sterno's +Stetson +Stetson's +Steuben +Steuben's +Steve +Steve's +Steven +Steven's +Stevens +Stevens's +Stevenson +Stevenson's +Stevie +Stevie's +Stewart +Stewart's +Stieglitz +Stieglitz's +Stilton +Stilton's +Stimson +Stimson's +Stine +Stine's +Stirling +Stirling's +Stockhausen +Stockhausen's +Stockholm +Stockholm's +Stockton +Stockton's +Stoic +Stoic's +Stoicism +Stoicism's +Stokes +Stokes's +Stolichnaya +Stolichnaya's +Stolypin +Stolypin's +Stone +Stone's +Stonehenge +Stonehenge's +Stoppard +Stoppard's +Stout +Stout's +Stowe +Stowe's +Strabo +Strabo's +Stradivarius +Stradivarius's +Strasbourg +Strasbourg's +Strauss +Strauss's +Stravinsky +Stravinsky's +Streisand +Streisand's +Strickland +Strickland's +Strindberg +Strindberg's +Stromboli +Stromboli's +Strong +Strong's +Stu +Stu's +Stuart +Stuart's +Stuarts +Studebaker +Studebaker's +Stuttgart +Stuttgart's +Stuyvesant +Stuyvesant's +Stygian +Stygian's +Styrofoam +Styrofoam's +Styrofoams +Styron +Styron's +Styx +Styx's +Suarez +Suarez's +Subaru +Subaru's +Sucre +Sucre's +Sucrets +Sucrets's +Sudan +Sudan's +Sudanese +Sudanese's +Sudetenland +Sudetenland's +Sudoku +Sudoku's +Sudra +Sudra's +Sue +Sue's +Suetonius +Suetonius's +Suez +Suez's +Suffolk +Suffolk's +Sufi +Sufi's +Sufism +Sufism's +Suharto +Suharto's +Sui +Sui's +Sukarno +Sukarno's +Sukkot +Sukkoth +Sukkoth's +Sukkoths +Sulawesi +Sulawesi's +Suleiman +Suleiman's +Sulla +Sulla's +Sullivan +Sullivan's +Sumatra +Sumatra's +Sumeria +Sumeria's +Sumerian +Sumerian's +Summer +Summer's +Summers +Summers's +Sumner +Sumner's +Sumter +Sumter's +Sunbeam +Sunbeam's +Sunbelt +Sunbelt's +Sundanese +Sundanese's +Sundas +Sundas's +Sunday +Sunday's +Sundays +Sung +Sung's +Sunkist +Sunkist's +Sunni +Sunni's +Sunnyvale +Sunnyvale's +Superbowl +Superbowl's +Superfund +Superfund's +Superglue +Superglue's +Superior +Superior's +Superman +Superman's +Surabaya +Surabaya's +Surat +Surat's +Surinam +Surinam's +Suriname +Suriname's +Surya +Surya's +Susan +Susan's +Susana +Susana's +Susanna +Susanna's +Susanne +Susanne's +Susie +Susie's +Susquehanna +Susquehanna's +Sussex +Sussex's +Sutherland +Sutherland's +Sutton +Sutton's +Suva +Suva's +Suwanee +Suwanee's +Suzanne +Suzanne's +Suzette +Suzette's +Suzhou +Suzhou's +Suzuki +Suzuki's +Suzy +Suzy's +Svalbard +Svalbard's +Sven +Sven's +Svengali +Svengali's +Swahili +Swahili's +Swahilis +Swammerdam +Swammerdam's +Swanee +Swanee's +Swansea +Swansea's +Swanson +Swanson's +Swazi +Swazi's +Swaziland +Swaziland's +Swede +Swede's +Sweden +Sweden's +Swedenborg +Swedenborg's +Swedes +Swedish +Swedish's +Sweeney +Sweeney's +Sweet +Sweet's +Swift +Swift's +Swinburne +Swinburne's +Swiss +Swiss's +Swissair +Swissair's +Swisses +Switzerland +Switzerland's +Sybil +Sybil's +Sydney +Sydney's +Sykes +Sykes's +Sylvester +Sylvester's +Sylvia +Sylvia's +Sylvie +Sylvie's +Synge +Synge's +Syracuse +Syracuse's +Syria +Syria's +Syriac +Syriac's +Syrian +Syrian's +Syrians +Szechuan +Szechuan's +Szilard +Szilard's +Szymborska +Szymborska's +Sèvres +Sèvres's +T +T'ang +T'ang's +T's +TWA +TWA's +Tabasco +Tabasco's +Tabatha +Tabatha's +Tabitha +Tabitha's +Tabriz +Tabriz's +Tacitus +Tacitus's +Tacoma +Tacoma's +Tad +Tad's +Tadzhik +Tadzhik's +Tadzhikistan +Tadzhikistan's +Taegu +Taegu's +Taejon +Taejon's +Taft +Taft's +Tagalog +Tagalog's +Tagore +Tagore's +Tagus +Tagus's +Tahiti +Tahiti's +Tahitian +Tahitian's +Tahitians +Tahoe +Tahoe's +Taichung +Taichung's +Taine +Taine's +Taipei +Taipei's +Taiping +Taiping's +Taiwan +Taiwan's +Taiwanese +Taiwanese's +Taiyuan +Taiyuan's +Tajikistan +Tajikistan's +Taklamakan +Taklamakan's +Talbot +Talbot's +Taliban +Taliban's +Taliesin +Taliesin's +Tallahassee +Tallahassee's +Tallchief +Tallchief's +Talley +Talley's +Talleyrand +Talleyrand's +Tallinn +Tallinn's +Talmud +Talmud's +Talmudic +Talmuds +Tamara +Tamara's +Tameka +Tameka's +Tamera +Tamera's +Tamerlane +Tamerlane's +Tami +Tami's +Tamika +Tamika's +Tamil +Tamil's +Tammany +Tammany's +Tammi +Tammi's +Tammie +Tammie's +Tammuz +Tammuz's +Tammy +Tammy's +Tampa +Tampa's +Tampax +Tampax's +Tamra +Tamra's +Tamworth +Tamworth's +Tancred +Tancred's +Taney +Taney's +Tanganyika +Tanganyika's +Tangiers +Tangiers's +Tangshan +Tangshan's +Tania +Tania's +Tanisha +Tanisha's +Tanner +Tanner's +Tannhäuser +Tannhäuser's +Tantalus +Tantalus's +Tanya +Tanya's +Tanzania +Tanzania's +Tanzanian +Tanzanian's +Tanzanians +Tao +Tao's +Taoism +Taoism's +Taoisms +Taoist +Taoist's +Taoists +Tara +Tara's +Tarantino +Tarantino's +Tarawa +Tarawa's +Tarazed +Tarazed's +Tarbell +Tarbell's +Target +Target's +Tarim +Tarim's +Tarkenton +Tarkenton's +Tarkington +Tarkington's +Tartar +Tartar's +Tartars +Tartary +Tartary's +Tartuffe +Tartuffe's +Tarzan +Tarzan's +Tasha +Tasha's +Tashkent +Tashkent's +Tasman +Tasman's +Tasmania +Tasmania's +Tasmanian +Tasmanian's +Tass +Tass's +Tatar +Tatar's +Tatars +Tate +Tate's +Tatum +Tatum's +Taurus +Taurus's +Tauruses +Tawney +Tawney's +Taylor +Taylor's +Tb +Tb's +Tbilisi +Tbilisi's +Tchaikovsky +Tchaikovsky's +Teasdale +Teasdale's +Technicolor +Technicolor's +Tecumseh +Tecumseh's +Ted +Ted's +Teddy +Teddy's +Teflon +Teflon's +Teflons +Tegucigalpa +Tegucigalpa's +Teheran +Teheran's +Tehran +TelePrompter +TelePrompter's +Telemachus +Telemachus's +Telemann +Telemann's +Teletype +Tell +Tell's +Teller +Teller's +Telugu +Telugu's +Tempe +Templar +Templar's +Tennessee +Tennessee's +Tennyson +Tennyson's +Tenochtitlan +Tenochtitlan's +TensorFlow +TensorFlow's +Teotihuacan +Teotihuacan's +Terence +Terence's +Teresa +Teresa's +Tereshkova +Tereshkova's +Teri +Teri's +Terkel +Terkel's +Terpsichore +Terpsichore's +Terr +Terr's +Terra +Terra's +Terran +Terran's +Terrance +Terrance's +Terrell +Terrell's +Terrence +Terrence's +Terri +Terri's +Terrie +Terrie's +Terry +Terry's +Tertiary +Tertiary's +Tesla +Tesla's +Tess +Tess's +Tessa +Tessa's +Tessie +Tessie's +Tet +Tet's +Tethys +Tethys's +Tetons +Tetons's +Teutonic +Teutonic's +Tevet +Tevet's +Texaco +Texaco's +Texan +Texan's +Texans +Texas +Texas's +Th +Th's +Thackeray +Thackeray's +Thad +Thad's +Thaddeus +Thaddeus's +Thai +Thai's +Thailand +Thailand's +Thais +Thales +Thales's +Thalia +Thalia's +Thames +Thames's +Thanh +Thanh's +Thanksgiving +Thanksgiving's +Thanksgivings +Thant +Thant's +Thar +Thar's +Tharp +Tharp's +Thatcher +Thatcher's +Thea +Thea's +Thebes +Thebes's +Theiler +Theiler's +Thelma +Thelma's +Themistocles +Themistocles's +Theocritus +Theocritus's +Theodora +Theodora's +Theodore +Theodore's +Theodoric +Theodoric's +Theodosius +Theodosius's +Theosophy +Theosophy's +Theravada +Theravada's +Theresa +Theresa's +Therese +Therese's +Thermopylae +Thermopylae's +Thermos +Theron +Theron's +Theseus +Theseus's +Thespian +Thespian's +Thespis +Thespis's +Thessalonian +Thessalonian's +Thessaloníki +Thessaloníki's +Thessaly +Thessaly's +Thieu +Thieu's +Thimbu +Thimbu's +Thomas +Thomas's +Thomism +Thomism's +Thomistic +Thomistic's +Thompson +Thompson's +Thomson +Thomson's +Thor +Thor's +Thorazine +Thorazine's +Thoreau +Thoreau's +Thornton +Thornton's +Thoroughbred +Thoroughbred's +Thorpe +Thorpe's +Thoth +Thoth's +Thrace +Thrace's +Thracian +Thracian's +Thucydides +Thucydides's +Thule +Thule's +Thunderbird +Thunderbird's +Thurber +Thurber's +Thurman +Thurman's +Thurmond +Thurmond's +Thursday +Thursday's +Thursdays +Thutmose +Thutmose's +Ti +Ti's +Tia +Tia's +Tianjin +Tianjin's +Tiber +Tiber's +Tiberius +Tiberius's +Tibet +Tibet's +Tibetan +Tibetan's +Tibetans +Ticketmaster +Ticketmaster's +Ticonderoga +Ticonderoga's +Tide +Tide's +Tienanmen +Tienanmen's +Tientsin +Tientsin's +Tiffany +Tiffany's +Tigris +Tigris's +Tijuana +Tijuana's +Tillich +Tillich's +Tillman +Tillman's +Tilsit +Tilsit's +Tim +Tim's +Timbuktu +Timbuktu's +Timex +Timex's +Timmy +Timmy's +Timon +Timon's +Timor +Timor's +Timothy +Timothy's +Timur +Timur's +Timurid +Timurid's +Tina +Tina's +Ting +Ting's +Tinkerbell +Tinkerbell's +Tinkertoy +Tinkertoy's +Tinseltown +Tinseltown's +Tintoretto +Tintoretto's +Tippecanoe +Tippecanoe's +Tipperary +Tipperary's +Tirana +Tirana's +Tiresias +Tiresias's +Tisha +Tisha's +Tishri +Tishri's +Titan +Titan's +Titania +Titania's +Titanic +Titanic's +Titian +Titian's +Titicaca +Titicaca's +Tito +Tito's +Titus +Titus's +Tlaloc +Tlaloc's +Tlingit +Tlingit's +Tobago +Tobago's +Toby +Toby's +Tocantins +Tocantins's +Tocqueville +Tocqueville's +Tod +Tod's +Todd +Todd's +Togo +Togo's +Tojo +Tojo's +Tokay +Tokay's +Tokugawa +Tokugawa's +Tokyo +Tokyo's +Toledo +Toledo's +Toledos +Tolkien +Tolkien's +Tolstoy +Tolstoy's +Toltec +Toltec's +Tolyatti +Tolyatti's +Tom +Tom's +Tomas +Tomas's +Tombaugh +Tombaugh's +Tomlin +Tomlin's +Tommie +Tommie's +Tommy +Tommy's +Tompkins +Tompkins's +Tomsk +Tomsk's +Tonga +Tonga's +Tongan +Tongan's +Tongans +Toni +Toni's +Tonia +Tonia's +Tonto +Tonto's +Tony +Tony's +Tonya +Tonya's +Topeka +Topeka's +Topsy +Topsy's +Torah +Torah's +Torahs +Tories +Toronto +Toronto's +Torquemada +Torquemada's +Torrance +Torrance's +Torrens +Torrens's +Torres +Torres's +Torricelli +Torricelli's +Tortola +Tortola's +Tortuga +Tortuga's +Torvalds +Torvalds's +Tory +Tory's +Tosca +Tosca's +Toscanini +Toscanini's +Toshiba +Toshiba's +Toto +Toto's +Toulouse +Toulouse's +Townes +Townes's +Townsend +Townsend's +Toynbee +Toynbee's +Toyoda +Toyoda's +Toyota +Toyota's +Tracey +Tracey's +Traci +Traci's +Tracie +Tracie's +Tracy +Tracy's +Trafalgar +Trafalgar's +Trailways +Trailways's +Trajan +Trajan's +Tran +Tran's +Transcaucasia +Transcaucasia's +Transvaal +Transvaal's +Transylvania +Transylvania's +Trappist +Trappist's +Travis +Travis's +Travolta +Travolta's +Treasuries +Treasury +Treasury's +Treblinka +Treblinka's +Trekkie +Trekkie's +Trent +Trent's +Trenton +Trenton's +Trevelyan +Trevelyan's +Trevino +Trevino's +Trevor +Trevor's +Trey +Trey's +Triangulum +Triangulum's +Triassic +Triassic's +Tricia +Tricia's +Trident +Trident's +Trieste +Trieste's +Trimurti +Trimurti's +Trina +Trina's +Trinidad +Trinidad's +Trinities +Trinity +Trinity's +Tripitaka +Tripitaka's +Tripoli +Tripoli's +Trippe +Trippe's +Trisha +Trisha's +Tristan +Tristan's +Triton +Triton's +Trobriand +Trobriand's +Troilus +Troilus's +Trojan +Trojan's +Trojans +Trollope +Trollope's +Trondheim +Trondheim's +Tropicana +Tropicana's +Trotsky +Trotsky's +Troy +Troy's +Troyes +Truckee +Truckee's +Trudeau +Trudeau's +Trudy +Trudy's +Truffaut +Truffaut's +Trujillo +Trujillo's +Truman +Truman's +Trumbull +Trumbull's +Trump +Trump's +Truth +Truth's +Tsimshian +Tsimshian's +Tsingtao +Tsingtao's +Tsiolkovsky +Tsiolkovsky's +Tsitsihar +Tsitsihar's +Tsongkhapa +Tsongkhapa's +Tswana +Tswana's +Tuamotu +Tuamotu's +Tuareg +Tuareg's +Tubman +Tubman's +Tucker +Tucker's +Tucson +Tucson's +Tucuman +Tucuman's +Tudor +Tudor's +Tuesday +Tuesday's +Tuesdays +Tulane +Tulane's +Tull +Tull's +Tulsa +Tulsa's +Tulsidas +Tulsidas's +Tums +Tums's +Tungus +Tungus's +Tunguska +Tunguska's +Tunis +Tunis's +Tunisia +Tunisia's +Tunisian +Tunisian's +Tunisians +Tunney +Tunney's +Tupi +Tupi's +Tupperware +Tupperware's +Tupungato +Tupungato's +Turgenev +Turgenev's +Turin +Turin's +Turing +Turing's +Turk +Turk's +Turkestan +Turkestan's +Turkey +Turkey's +Turkish +Turkish's +Turkmenistan +Turkmenistan's +Turks +Turner +Turner's +Turpin +Turpin's +Tuscaloosa +Tuscaloosa's +Tuscan +Tuscan's +Tuscany +Tuscany's +Tuscarora +Tuscarora's +Tuscon +Tuscon's +Tuskegee +Tuskegee's +Tussaud +Tussaud's +Tut +Tut's +Tutankhamen +Tutankhamen's +Tutsi +Tutsi's +Tutu +Tutu's +Tuvalu +Tuvalu's +Twain +Twain's +Tweed +Tweed's +Tweedledee +Tweedledee's +Tweedledum +Tweedledum's +Twila +Twila's +Twinkies +Twinkies's +Twitter +Twitter's +Twizzlers +Twizzlers's +Ty +Ty's +Tycho +Tycho's +Tylenol +Tylenol's +Tyler +Tyler's +Tyndale +Tyndale's +Tyndall +Tyndall's +Tyre +Tyre's +Tyree +Tyree's +Tyrone +Tyrone's +Tyson +Tyson's +U +U's +UBS +UBS's +UCLA +UCLA's +UPS +UPS's +Ubangi +Ubangi's +Ubuntu +Ubuntu's +Ucayali +Ucayali's +Uccello +Uccello's +Udall +Udall's +Ufa +Ufa's +Uganda +Uganda's +Ugandan +Ugandan's +Ugandans +Uighur +Uighur's +Ujungpandang +Ujungpandang's +Ukraine +Ukraine's +Ukrainian +Ukrainian's +Ukrainians +Ulster +Ulster's +Ultrasuede +Ultrasuede's +Ulyanovsk +Ulyanovsk's +Ulysses +Ulysses's +Umbriel +Umbriel's +Underwood +Underwood's +Ungava +Ungava's +Unicode +Unicode's +Unilever +Unilever's +Union +Union's +Unions +Uniroyal +Uniroyal's +Unitarian +Unitarian's +Unitarianism +Unitarianism's +Unitarianisms +Unitarians +Unitas +Unitas's +Unukalhai +Unukalhai's +Upanishads +Upanishads's +Updike +Updike's +Upjohn +Upjohn's +Upton +Upton's +Ur +Ur's +Ural +Ural's +Urals +Urals's +Urania +Urania's +Uranus +Uranus's +Urban +Urban's +Urdu +Urdu's +Urey +Urey's +Uriah +Uriah's +Uriel +Uriel's +Uris +Uris's +Urquhart +Urquhart's +Ursa +Ursa's +Ursula +Ursula's +Ursuline +Ursuline's +Uruguay +Uruguay's +Uruguayan +Uruguayan's +Uruguayans +Urumqi +Urumqi's +Usenet +Usenet's +Ustinov +Ustinov's +Utah +Utah's +Ute +Ute's +Utopia +Utopia's +Utopian +Utopian's +Utopians +Utopias +Utrecht +Utrecht's +Utrillo +Utrillo's +Uzbek +Uzbek's +Uzbekistan +Uzbekistan's +Uzi +Uzi's +V +V's +VBA +VBA's +Vader +Vader's +Vaduz +Vaduz's +Val +Val's +Valarie +Valarie's +Valdez +Valdez's +Valencia +Valencia's +Valenti +Valenti's +Valentin +Valentin's +Valentine +Valentine's +Valentino +Valentino's +Valenzuela +Valenzuela's +Valeria +Valeria's +Valerian +Valerian's +Valerie +Valerie's +Valhalla +Valhalla's +Valium +Valium's +Valiums +Valkyrie +Valkyrie's +Valkyries +Valletta +Valletta's +Valois +Valois's +Valparaiso +Valparaiso's +Valvoline +Valvoline's +Valéry +Valéry's +Van +Van's +Vance +Vance's +Vancouver +Vancouver's +Vandal +Vandal's +Vanderbilt +Vanderbilt's +Vandyke +Vandyke's +Vanessa +Vanessa's +Vang +Vang's +Vanuatu +Vanuatu's +Vanzetti +Vanzetti's +Varanasi +Varanasi's +Varese +Varese's +Vargas +Vargas's +Vaseline +Vaseline's +Vaselines +Vasquez +Vasquez's +Vassar +Vassar's +Vatican +Vatican's +Vauban +Vauban's +Vaughan +Vaughan's +Vaughn +Vaughn's +Vazquez +Vazquez's +Veblen +Veblen's +Veda +Veda's +Vedanta +Vedanta's +Vedas +Vega +Vega's +Vegas +Vegas's +Vegemite +Vegemite's +Vela +Vela's +Velcro +Velcro's +Velcros +Velez +Velez's +Velma +Velma's +Velveeta +Velveeta's +Velásquez +Velásquez's +Velázquez +Velázquez's +Venetian +Venetian's +Venetians +Venezuela +Venezuela's +Venezuelan +Venezuelan's +Venezuelans +Venice +Venice's +Venn +Venn's +Ventolin +Ventolin's +Venus +Venus's +Venuses +Venusian +Venusian's +Vera +Vera's +Veracruz +Veracruz's +Verde +Verde's +Verdi +Verdi's +Verdun +Verdun's +Vergil +Vergil's +Verizon +Verizon's +Verlaine +Verlaine's +Vermeer +Vermeer's +Vermont +Vermont's +Vermonter +Vermonter's +Vern +Vern's +Verna +Verna's +Verne +Verne's +Vernon +Vernon's +Verona +Verona's +Veronese +Veronese's +Veronica +Veronica's +Versailles +Versailles's +Vesalius +Vesalius's +Vespasian +Vespasian's +Vespucci +Vespucci's +Vesta +Vesta's +Vesuvius +Vesuvius's +Viacom +Viacom's +Viagra +Viagra's +Vicente +Vicente's +Vichy +Vichy's +Vicki +Vicki's +Vickie +Vickie's +Vicksburg +Vicksburg's +Vicky +Vicky's +Victor +Victor's +Victoria +Victoria's +Victorian +Victorian's +Victorians +Victrola +Victrola's +Vidal +Vidal's +Vienna +Vienna's +Viennese +Viennese's +Vientiane +Vientiane's +Vietcong +Vietcong's +Vietminh +Vietminh's +Vietnam +Vietnam's +Vietnamese +Vietnamese's +Vijayanagar +Vijayanagar's +Vijayawada +Vijayawada's +Viking +Viking's +Vikings +Vila +Vila's +Villa +Villa's +Villarreal +Villarreal's +Villon +Villon's +Vilma +Vilma's +Vilnius +Vilnius's +Vilyui +Vilyui's +Vince +Vince's +Vincent +Vincent's +Vindemiatrix +Vindemiatrix's +Vinson +Vinson's +Viola +Viola's +Violet +Violet's +Virgie +Virgie's +Virgil +Virgil's +Virginia +Virginia's +Virginian +Virginian's +Virginians +Virgo +Virgo's +Virgos +Visa +Visa's +Visakhapatnam +Visakhapatnam's +Visayans +Visayans's +Vishnu +Vishnu's +Visigoth +Visigoth's +Vistula +Vistula's +Vitim +Vitim's +Vito +Vito's +Vitus +Vitus's +Vivaldi +Vivaldi's +Vivekananda +Vivekananda's +Vivian +Vivian's +Vivienne +Vivienne's +Vlad +Vlad's +Vladimir +Vladimir's +Vladivostok +Vladivostok's +Vlaminck +Vlaminck's +Vlasic +Vlasic's +VoIP +Vogue +Vogue's +Volcker +Volcker's +Voldemort +Voldemort's +Volga +Volga's +Volgograd +Volgograd's +Volkswagen +Volkswagen's +Volstead +Volstead's +Volta +Volta's +Voltaire +Voltaire's +Volvo +Volvo's +Vonda +Vonda's +Vonnegut +Vonnegut's +Voronezh +Voronezh's +Vorster +Vorster's +Voyager +Voyager's +Vuitton +Vuitton's +Vulcan +Vulcan's +Vulgate +Vulgate's +Vulgates +W +W's +Wabash +Wabash's +Waco +Waco's +Wade +Wade's +Wagner +Wagner's +Wagnerian +Wagnerian's +Wahhabi +Wahhabi's +Waikiki +Waikiki's +Waite +Waite's +Wake +Wake's +Waksman +Waksman's +Wald +Wald's +Waldemar +Waldemar's +Walden +Walden's +Waldensian +Waldensian's +Waldheim +Waldheim's +Waldo +Waldo's +Waldorf +Waldorf's +Wales +Wales's +Walesa +Walesa's +Walgreen +Walgreen's +Walgreens +Walgreens's +Walker +Walker's +Walkman +Walkman's +Wall +Wall's +Wallace +Wallace's +Wallenstein +Wallenstein's +Waller +Waller's +Wallis +Wallis's +Walloon +Walloon's +Walls +Walls's +Walmart +Walmart's +Walpole +Walpole's +Walpurgisnacht +Walpurgisnacht's +Walsh +Walsh's +Walt +Walt's +Walter +Walter's +Walters +Walters's +Walton +Walton's +Wanamaker +Wanamaker's +Wanda +Wanda's +Wang +Wang's +Wankel +Wankel's +Ward +Ward's +Ware +Ware's +Warhol +Warhol's +Waring +Waring's +Warner +Warner's +Warren +Warren's +Warsaw +Warsaw's +Warwick +Warwick's +Wasatch +Wasatch's +Washington +Washington's +Washingtonian +Washingtonian's +Washingtonians +Wasp +Wassermann +Wassermann's +Waterbury +Waterbury's +Waterford +Waterford's +Watergate +Watergate's +Waterloo +Waterloo's +Waterloos +Waters +Waters's +Watkins +Watkins's +Watson +Watson's +Watt +Watt's +Watteau +Watteau's +Watts +Watts's +Watusi +Watusi's +Waugh +Waugh's +Wayne +Wayne's +Weaver +Weaver's +Webb +Webb's +Weber +Weber's +Webern +Webern's +Webster +Webster's +Websters +Weddell +Weddell's +Wedgwood +Wedgwood's +Wednesday +Wednesday's +Wednesdays +Weeks +Weeks's +Wehrmacht +Wehrmacht's +Wei +Wei's +Weierstrass +Weierstrass's +Weill +Weill's +Weinberg +Weinberg's +Weiss +Weiss's +Weissmuller +Weissmuller's +Weizmann +Weizmann's +Welch +Welch's +Weldon +Weldon's +Welland +Welland's +Weller +Weller's +Welles +Welles's +Wellington +Wellington's +Wellingtons +Wells +Wells's +Welsh +Welsh's +Welshman +Welshman's +Welshmen +Welshmen's +Wendell +Wendell's +Wendi +Wendi's +Wendy +Wendy's +Wesak +Wesak's +Wesley +Wesley's +Wesleyan +Wesleyan's +Wessex +Wessex's +Wesson +Wesson's +West +West's +Western +Western's +Westerner +Westerns +Westinghouse +Westinghouse's +Westminster +Westminster's +Weston +Weston's +Westphalia +Westphalia's +Wests +Weyden +Weyden's +Wezen +Wezen's +Wharton +Wharton's +Wheaties +Wheaties's +Wheatstone +Wheatstone's +Wheeler +Wheeler's +Wheeling +Wheeling's +Whig +Whig's +Whigs +Whipple +Whipple's +Whirlpool +Whirlpool's +Whistler +Whistler's +Whitaker +Whitaker's +White +White's +Whitefield +Whitefield's +Whitehall +Whitehall's +Whitehead +Whitehead's +Whitehorse +Whitehorse's +Whiteley +Whiteley's +Whites +Whitfield +Whitfield's +Whitley +Whitley's +Whitman +Whitman's +Whitney +Whitney's +Whitsunday +Whitsunday's +Whitsundays +Whittier +Whittier's +WiFi +Wicca +Wicca's +Wichita +Wichita's +Wiemar +Wiemar's +Wiesel +Wiesel's +Wiesenthal +Wiesenthal's +Wiggins +Wiggins's +Wigner +Wigner's +Wii +Wii's +Wikileaks +Wikipedia +Wikipedia's +Wilberforce +Wilberforce's +Wilbert +Wilbert's +Wilbur +Wilbur's +Wilburn +Wilburn's +Wilcox +Wilcox's +Wilda +Wilda's +Wilde +Wilde's +Wilder +Wilder's +Wiles +Wiles's +Wiley +Wiley's +Wilford +Wilford's +Wilfred +Wilfred's +Wilfredo +Wilfredo's +Wilhelm +Wilhelm's +Wilhelmina +Wilhelmina's +Wilkerson +Wilkerson's +Wilkes +Wilkes's +Wilkins +Wilkins's +Wilkinson +Wilkinson's +Will +Will's +Willa +Willa's +Willamette +Willamette's +Willard +Willard's +Willemstad +Willemstad's +William +William's +Williams +Williams's +Williamson +Williamson's +Willie +Willie's +Willis +Willis's +Willy +Willy's +Wilma +Wilma's +Wilmer +Wilmer's +Wilmington +Wilmington's +Wilson +Wilson's +Wilsonian +Wilsonian's +Wilton +Wilton's +Wimbledon +Wimbledon's +Wimsey +Wimsey's +Winchell +Winchell's +Winchester +Winchester's +Windbreaker +Windbreaker's +Windex +Windex's +Windhoek +Windhoek's +Windows +Windows's +Windsor +Windsor's +Windsors +Windward +Windward's +Winesap +Winesap's +Winfred +Winfred's +Winfrey +Winfrey's +Winifred +Winifred's +Winkle +Winkle's +Winnebago +Winnebago's +Winnie +Winnie's +Winnipeg +Winnipeg's +Winston +Winston's +Winters +Winters's +Winthrop +Winthrop's +Wisconsin +Wisconsin's +Wisconsinite +Wisconsinite's +Wisconsinites +Wise +Wise's +Witt +Witt's +Wittgenstein +Wittgenstein's +Witwatersrand +Witwatersrand's +Wm +Wm's +Wobegon +Wobegon's +Wodehouse +Wodehouse's +Wolf +Wolf's +Wolfe +Wolfe's +Wolff +Wolff's +Wolfgang +Wolfgang's +Wollongong +Wollongong's +Wollstonecraft +Wollstonecraft's +Wolsey +Wolsey's +Wonder +Wonder's +Wonderbra +Wonderbra's +Wong +Wong's +Wood +Wood's +Woodard +Woodard's +Woodhull +Woodhull's +Woodrow +Woodrow's +Woods +Woods's +Woodstock +Woodstock's +Woodward +Woodward's +Woolf +Woolf's +Woolite +Woolite's +Woolongong +Woolongong's +Woolworth +Woolworth's +Wooster +Wooster's +Wooten +Wooten's +Worcester +Worcester's +Worcesters +Worcestershire +Worcestershire's +WordPress +WordPress's +Wordsworth +Wordsworth's +Workman +Workman's +Worms +Worms's +Wotan +Wotan's +Wovoka +Wovoka's +Wozniak +Wozniak's +Wozzeck +Wozzeck's +Wrangell +Wrangell's +Wren +Wren's +Wright +Wright's +Wrigley +Wrigley's +Wroclaw +Wroclaw's +Wu +Wu's +Wuhan +Wuhan's +Wurlitzer +Wurlitzer's +Wyatt +Wyatt's +Wycherley +Wycherley's +Wycliffe +Wycliffe's +Wyeth +Wyeth's +Wylie +Wylie's +Wynn +Wynn's +Wyoming +Wyoming's +Wyomingite +Wyomingite's +Wyomingites +X +X's +XEmacs +XEmacs's +Xamarin +Xamarin's +Xanadu +Xanadu's +Xanthippe +Xanthippe's +Xavier +Xavier's +Xe +Xe's +Xenakis +Xenakis's +Xenia +Xenia's +Xenophon +Xenophon's +Xerox +Xerox's +Xeroxes +Xerxes +Xerxes's +Xhosa +Xhosa's +Xi'an +Xi'an's +Xiaoping +Xiaoping's +Ximenes +Ximenes's +Xingu +Xingu's +Xiongnu +Xiongnu's +Xmas +Xmas's +Xmases +Xochipilli +Xochipilli's +Xuzhou +Xuzhou's +Y +Y's +Yacc +Yacc's +Yahoo +Yahoo's +Yahtzee +Yahtzee's +Yahweh +Yahweh's +Yakima +Yakima's +Yakut +Yakut's +Yakutsk +Yakutsk's +Yale +Yale's +Yalow +Yalow's +Yalta +Yalta's +Yalu +Yalu's +Yamagata +Yamagata's +Yamaha +Yamaha's +Yamoussoukro +Yamoussoukro's +Yang +Yang's +Yangon +Yangon's +Yangtze +Yangtze's +Yank +Yank's +Yankee +Yankee's +Yankees +Yanks +Yaobang +Yaobang's +Yaounde +Yaounde's +Yaqui +Yaqui's +Yaroslavl +Yaroslavl's +Yataro +Yataro's +Yates +Yates's +Yeager +Yeager's +Yeats +Yeats's +Yekaterinburg +Yekaterinburg's +Yellowknife +Yellowknife's +Yellowstone +Yellowstone's +Yeltsin +Yeltsin's +Yemen +Yemen's +Yemeni +Yemeni's +Yemenis +Yenisei +Yenisei's +Yerevan +Yerevan's +Yerkes +Yerkes's +Yesenia +Yesenia's +Yevtushenko +Yevtushenko's +Yggdrasil +Yggdrasil's +Yiddish +Yiddish's +Ymir +Ymir's +Yoda +Yoda's +Yoknapatawpha +Yoknapatawpha's +Yoko +Yoko's +Yokohama +Yokohama's +Yolanda +Yolanda's +Yong +Yong's +Yonkers +Yonkers's +York +York's +Yorkie +Yorkie's +Yorkshire +Yorkshire's +Yorktown +Yorktown's +Yoruba +Yoruba's +Yosemite +Yosemite's +Yossarian +Yossarian's +YouTube +YouTube's +Young +Young's +Youngstown +Youngstown's +Ypres +Ypres's +Ypsilanti +Ypsilanti's +Yuan +Yuan's +Yucatan +Yucatan's +Yugoslav +Yugoslav's +Yugoslavia +Yugoslavia's +Yugoslavian +Yugoslavian's +Yugoslavians +Yukon +Yukon's +Yule +Yule's +Yules +Yuletide +Yuletide's +Yuletides +Yunnan +Yunnan's +Yuri +Yuri's +Yves +Yves's +Yvette +Yvette's +Yvonne +Yvonne's +Z +Z's +Zachariah +Zachariah's +Zachary +Zachary's +Zachery +Zachery's +Zagreb +Zagreb's +Zaire +Zaire's +Zairian +Zambezi +Zambezi's +Zambia +Zambia's +Zambian +Zambian's +Zambians +Zamboni +Zamboni's +Zamenhof +Zamenhof's +Zamora +Zamora's +Zane +Zane's +Zanuck +Zanuck's +Zanzibar +Zanzibar's +Zapata +Zapata's +Zaporozhye +Zaporozhye's +Zapotec +Zapotec's +Zappa +Zappa's +Zara +Zara's +Zealand +Zealand's +Zebedee +Zebedee's +Zechariah +Zechariah's +Zedekiah +Zedekiah's +Zedong +Zedong's +Zeffirelli +Zeffirelli's +Zeke +Zeke's +Zelig +Zelig's +Zelma +Zelma's +Zen +Zen's +Zenger +Zenger's +Zeno +Zeno's +Zens +Zephaniah +Zephaniah's +Zephyrus +Zephyrus's +Zeppelin +Zeppelin's +Zest +Zest's +Zeus +Zeus's +Zhengzhou +Zhengzhou's +Zhivago +Zhivago's +Zhukov +Zhukov's +Zibo +Zibo's +Ziegfeld +Ziegfeld's +Ziegler +Ziegler's +Ziggy +Ziggy's +Zimbabwe +Zimbabwe's +Zimbabwean +Zimbabwean's +Zimbabweans +Zimmerman +Zimmerman's +Zinfandel +Zinfandel's +Zion +Zion's +Zionism +Zionism's +Zionisms +Zionist +Zionist's +Zionists +Zions +Ziploc +Ziploc's +Zn +Zn's +Zoe +Zoe's +Zola +Zola's +Zollverein +Zollverein's +Zoloft +Zoloft's +Zomba +Zomba's +Zorn +Zorn's +Zoroaster +Zoroaster's +Zoroastrian +Zoroastrian's +Zoroastrianism +Zoroastrianism's +Zoroastrianisms +Zorro +Zorro's +Zosma +Zosma's +Zr +Zr's +Zsigmondy +Zsigmondy's +Zubenelgenubi +Zubenelgenubi's +Zubeneschamali +Zubeneschamali's +Zukor +Zukor's +Zulu +Zulu's +Zulus +Zuni +Zuni's +Zwingli +Zwingli's +Zworykin +Zworykin's +Zyrtec +Zyrtec's +Zyuganov +Zyuganov's +Zürich +Zürich's +a +aardvark +aardvark's +aardvarks +abaci +aback +abacus +abacus's +abacuses +abaft +abalone +abalone's +abalones +abandon +abandoned +abandoning +abandonment +abandonment's +abandons +abase +abased +abasement +abasement's +abases +abash +abashed +abashes +abashing +abasing +abate +abated +abatement +abatement's +abates +abating +abattoir +abattoir's +abattoirs +abbess +abbess's +abbesses +abbey +abbey's +abbeys +abbot +abbot's +abbots +abbreviate +abbreviated +abbreviates +abbreviating +abbreviation +abbreviation's +abbreviations +abbé +abbé's +abbés +abdicate +abdicated +abdicates +abdicating +abdication +abdication's +abdications +abdomen +abdomen's +abdomens +abdominal +abduct +abducted +abductee +abductee's +abductees +abducting +abduction +abduction's +abductions +abductor +abductor's +abductors +abducts +abeam +abed +aberrant +aberration +aberration's +aberrations +abet +abets +abetted +abetter +abetter's +abetters +abetting +abettor +abettor's +abettors +abeyance +abeyance's +abhor +abhorred +abhorrence +abhorrence's +abhorrent +abhorring +abhors +abide +abided +abides +abiding +abilities +ability +ability's +abject +abjectly +abjuration +abjuration's +abjurations +abjure +abjured +abjures +abjuring +ablative +ablative's +ablatives +ablaze +able +abler +ablest +abloom +ablution +ablution's +ablutions +ably +abnegate +abnegated +abnegates +abnegating +abnegation +abnegation's +abnormal +abnormalities +abnormality +abnormality's +abnormally +aboard +abode +abode's +abodes +abolish +abolished +abolishes +abolishing +abolition +abolition's +abolitionist +abolitionist's +abolitionists +abominable +abominably +abominate +abominated +abominates +abominating +abomination +abomination's +abominations +aboriginal +aboriginal's +aboriginals +aborigine +aborigine's +aborigines +abort +aborted +aborting +abortion +abortion's +abortionist +abortionist's +abortionists +abortions +abortive +aborts +abound +abounded +abounding +abounds +about +above +above's +aboveboard +abracadabra +abracadabra's +abrade +abraded +abrades +abrading +abrasion +abrasion's +abrasions +abrasive +abrasive's +abrasively +abrasiveness +abrasiveness's +abrasives +abreast +abridge +abridged +abridgement +abridgement's +abridgements +abridges +abridging +abridgment +abridgment's +abridgments +abroad +abrogate +abrogated +abrogates +abrogating +abrogation +abrogation's +abrogations +abrupt +abrupter +abruptest +abruptly +abruptness +abruptness's +abscess +abscess's +abscessed +abscesses +abscessing +abscissa +abscissa's +abscissae +abscissas +abscond +absconded +absconding +absconds +absence +absence's +absences +absent +absented +absentee +absentee's +absenteeism +absenteeism's +absentees +absenting +absently +absents +absinth +absinth's +absinthe +absinthe's +absolute +absolute's +absolutely +absolutes +absolutest +absolution +absolution's +absolutism +absolutism's +absolve +absolved +absolves +absolving +absorb +absorbed +absorbency +absorbency's +absorbent +absorbent's +absorbents +absorbing +absorbs +absorption +absorption's +abstain +abstained +abstainer +abstainer's +abstainers +abstaining +abstains +abstemious +abstention +abstention's +abstentions +abstinence +abstinence's +abstinent +abstract +abstract's +abstracted +abstractedly +abstracting +abstraction +abstraction's +abstractions +abstractly +abstractness +abstractness's +abstractnesses +abstracts +abstruse +abstrusely +abstruseness +abstruseness's +absurd +absurder +absurdest +absurdities +absurdity +absurdity's +absurdly +abundance +abundance's +abundances +abundant +abundantly +abuse +abuse's +abused +abuser +abuser's +abusers +abuses +abusing +abusive +abusively +abusiveness +abusiveness's +abut +abutment +abutment's +abutments +abuts +abutted +abutting +abuzz +abysmal +abysmally +abyss +abyss's +abysses +acacia +acacia's +acacias +academia +academia's +academic +academic's +academical +academically +academician +academician's +academicians +academics +academies +academy +academy's +acanthi +acanthus +acanthus's +acanthuses +accede +acceded +accedes +acceding +accelerate +accelerated +accelerates +accelerating +acceleration +acceleration's +accelerations +accelerator +accelerator's +accelerators +accent +accent's +accented +accenting +accents +accentuate +accentuated +accentuates +accentuating +accentuation +accentuation's +accept +acceptability +acceptability's +acceptable +acceptably +acceptance +acceptance's +acceptances +accepted +accepting +accepts +access +access's +accessed +accesses +accessibility +accessibility's +accessible +accessibly +accessing +accession +accession's +accessioned +accessioning +accessions +accessories +accessory +accessory's +accident +accident's +accidental +accidental's +accidentally +accidentals +accidents +acclaim +acclaim's +acclaimed +acclaiming +acclaims +acclamation +acclamation's +acclimate +acclimated +acclimates +acclimating +acclimation +acclimation's +acclimatization +acclimatization's +acclimatize +acclimatized +acclimatizes +acclimatizing +accolade +accolade's +accolades +accommodate +accommodated +accommodates +accommodating +accommodation +accommodation's +accommodations +accompanied +accompanies +accompaniment +accompaniment's +accompaniments +accompanist +accompanist's +accompanists +accompany +accompanying +accomplice +accomplice's +accomplices +accomplish +accomplished +accomplishes +accomplishing +accomplishment +accomplishment's +accomplishments +accord +accord's +accordance +accordance's +accorded +according +accordingly +accordion +accordion's +accordions +accords +accost +accost's +accosted +accosting +accosts +account +account's +accountability +accountability's +accountable +accountancy +accountancy's +accountant +accountant's +accountants +accounted +accounting +accounting's +accounts +accouterments +accouterments's +accoutrements +accredit +accreditation +accreditation's +accredited +accrediting +accredits +accretion +accretion's +accretions +accrual +accrual's +accruals +accrue +accrued +accrues +accruing +acculturation +acculturation's +accumulate +accumulated +accumulates +accumulating +accumulation +accumulation's +accumulations +accumulative +accumulator +accuracy +accuracy's +accurate +accurately +accurateness +accurateness's +accursed +accurst +accusation +accusation's +accusations +accusative +accusative's +accusatives +accusatory +accuse +accused +accuser +accuser's +accusers +accuses +accusing +accusingly +accustom +accustomed +accustoming +accustoms +ace +ace's +aced +acerbic +acerbity +acerbity's +aces +acetaminophen +acetaminophen's +acetate +acetate's +acetates +acetic +acetone +acetone's +acetylene +acetylene's +ache +ache's +ached +aches +achier +achiest +achievable +achieve +achieved +achievement +achievement's +achievements +achiever +achiever's +achievers +achieves +achieving +aching +achoo +achoo's +achromatic +achy +acid +acid's +acidic +acidified +acidifies +acidify +acidifying +acidity +acidity's +acidly +acids +acidulous +acing +acknowledge +acknowledged +acknowledgement +acknowledgement's +acknowledgements +acknowledges +acknowledging +acknowledgment +acknowledgment's +acknowledgments +acme +acme's +acmes +acne +acne's +acolyte +acolyte's +acolytes +aconite +aconite's +aconites +acorn +acorn's +acorns +acoustic +acoustical +acoustically +acoustics +acoustics's +acquaint +acquaintance +acquaintance's +acquaintances +acquainted +acquainting +acquaints +acquiesce +acquiesced +acquiescence +acquiescence's +acquiescent +acquiesces +acquiescing +acquirable +acquire +acquired +acquirement +acquirement's +acquires +acquiring +acquisition +acquisition's +acquisitions +acquisitive +acquisitiveness +acquisitiveness's +acquit +acquits +acquittal +acquittal's +acquittals +acquitted +acquitting +acre +acre's +acreage +acreage's +acreages +acres +acrid +acrider +acridest +acrimonious +acrimony +acrimony's +acrobat +acrobat's +acrobatic +acrobatics +acrobatics's +acrobats +acronym +acronym's +acronyms +across +acrostic +acrostic's +acrostics +acrylic +acrylic's +acrylics +act +act's +acted +acting +acting's +actinium +actinium's +action +action's +actionable +actions +activate +activated +activates +activating +activation +activation's +active +active's +actively +actives +activism +activism's +activist +activist's +activists +activities +activity +activity's +actor +actor's +actors +actress +actress's +actresses +acts +actual +actualities +actuality +actuality's +actualization +actualization's +actualize +actualized +actualizes +actualizing +actually +actuarial +actuaries +actuary +actuary's +actuate +actuated +actuates +actuating +actuator +actuator's +actuators +acuity +acuity's +acumen +acumen's +acupuncture +acupuncture's +acupuncturist +acupuncturist's +acupuncturists +acute +acute's +acutely +acuteness +acuteness's +acuter +acutes +acutest +ad +ad's +adage +adage's +adages +adagio +adagio's +adagios +adamant +adamant's +adamantly +adapt +adaptability +adaptability's +adaptable +adaptation +adaptation's +adaptations +adapted +adapter +adapter's +adapters +adapting +adaptive +adaptor +adaptor's +adaptors +adapts +add +added +addend +addend's +addenda +addends +addendum +addendum's +addendums +adder +adder's +adders +addict +addict's +addicted +addicting +addiction +addiction's +addictions +addictive +addicts +adding +addition +addition's +additional +additionally +additions +additive +additive's +additives +addle +addled +addles +addling +address +address's +addressable +addressed +addressee +addressee's +addressees +addresses +addressing +adds +adduce +adduced +adduces +adducing +adenoid +adenoid's +adenoidal +adenoids +adept +adept's +adeptly +adeptness +adeptness's +adepts +adequacy +adequacy's +adequate +adequately +adhere +adhered +adherence +adherence's +adherent +adherent's +adherents +adheres +adhering +adhesion +adhesion's +adhesive +adhesive's +adhesives +adiabatic +adieu +adieu's +adieus +adieux +adipose +adiós +adjacent +adjacently +adjectival +adjectivally +adjective +adjective's +adjectives +adjoin +adjoined +adjoining +adjoins +adjourn +adjourned +adjourning +adjournment +adjournment's +adjournments +adjourns +adjudge +adjudged +adjudges +adjudging +adjudicate +adjudicated +adjudicates +adjudicating +adjudication +adjudication's +adjudicator +adjudicator's +adjudicators +adjunct +adjunct's +adjuncts +adjuration +adjuration's +adjurations +adjure +adjured +adjures +adjuring +adjust +adjustable +adjusted +adjuster +adjuster's +adjusters +adjusting +adjustment +adjustment's +adjustments +adjustor +adjustor's +adjustors +adjusts +adjutant +adjutant's +adjutants +adman +adman's +admen +administer +administered +administering +administers +administrate +administrated +administrates +administrating +administration +administration's +administrations +administrative +administratively +administrator +administrator's +administrators +admirable +admirably +admiral +admiral's +admirals +admiralty +admiralty's +admiration +admiration's +admire +admired +admirer +admirer's +admirers +admires +admiring +admiringly +admissibility +admissibility's +admissible +admission +admission's +admissions +admit +admits +admittance +admittance's +admitted +admittedly +admitting +admixture +admixture's +admixtures +admonish +admonished +admonishes +admonishing +admonishment +admonishment's +admonishments +admonition +admonition's +admonitions +admonitory +ado +ado's +adobe +adobe's +adobes +adolescence +adolescence's +adolescences +adolescent +adolescent's +adolescents +adopt +adopted +adopting +adoption +adoption's +adoptions +adoptive +adopts +adorable +adorably +adoration +adoration's +adore +adored +adores +adoring +adoringly +adorn +adorned +adorning +adornment +adornment's +adornments +adorns +adrenal +adrenal's +adrenaline +adrenaline's +adrenals +adrift +adroit +adroitly +adroitness +adroitness's +ads +adulate +adulated +adulates +adulating +adulation +adulation's +adult +adult's +adulterant +adulterant's +adulterants +adulterate +adulterated +adulterates +adulterating +adulteration +adulteration's +adulterer +adulterer's +adulterers +adulteress +adulteress's +adulteresses +adulteries +adulterous +adultery +adultery's +adulthood +adulthood's +adults +adumbrate +adumbrated +adumbrates +adumbrating +adumbration +adumbration's +advance +advance's +advanced +advancement +advancement's +advancements +advances +advancing +advantage +advantage's +advantaged +advantageous +advantageously +advantages +advantaging +advent +advent's +adventitious +advents +adventure +adventure's +adventured +adventurer +adventurer's +adventurers +adventures +adventuresome +adventuress +adventuress's +adventuresses +adventuring +adventurous +adventurously +adverb +adverb's +adverbial +adverbial's +adverbials +adverbs +adversarial +adversaries +adversary +adversary's +adverse +adversely +adverser +adversest +adversities +adversity +adversity's +advert +advert's +adverted +adverting +advertise +advertised +advertisement +advertisement's +advertisements +advertiser +advertiser's +advertisers +advertises +advertising +advertising's +adverts +advice +advice's +advisability +advisability's +advisable +advise +advised +advisedly +advisement +advisement's +adviser +adviser's +advisers +advises +advising +advisor +advisor's +advisories +advisors +advisory +advisory's +advocacy +advocacy's +advocate +advocate's +advocated +advocates +advocating +adware +adz +adz's +adze +adze's +adzes +aegis +aegis's +aeon +aeon's +aeons +aerate +aerated +aerates +aerating +aeration +aeration's +aerator +aerator's +aerators +aerial +aerial's +aerialist +aerialist's +aerialists +aerials +aerie +aerie's +aeries +aerobatics +aerobatics's +aerobic +aerobics +aerobics's +aerodynamic +aerodynamically +aerodynamics +aerodynamics's +aeronautical +aeronautics +aeronautics's +aerosol +aerosol's +aerosols +aerospace +aerospace's +aery +aery's +aesthete +aesthete's +aesthetes +aesthetic +aesthetically +aesthetics +aesthetics's +afar +affability +affability's +affable +affably +affair +affair's +affairs +affect +affect's +affectation +affectation's +affectations +affected +affecting +affection +affection's +affectionate +affectionately +affections +affects +affidavit +affidavit's +affidavits +affiliate +affiliate's +affiliated +affiliates +affiliating +affiliation +affiliation's +affiliations +affinities +affinity +affinity's +affirm +affirmation +affirmation's +affirmations +affirmative +affirmative's +affirmatively +affirmatives +affirmed +affirming +affirms +affix +affix's +affixed +affixes +affixing +afflict +afflicted +afflicting +affliction +affliction's +afflictions +afflicts +affluence +affluence's +affluent +affluently +afford +affordable +afforded +affording +affords +afforest +afforestation +afforestation's +afforested +afforesting +afforests +affray +affray's +affrays +affront +affront's +affronted +affronting +affronts +afghan +afghan's +afghans +aficionado +aficionado's +aficionados +afield +afire +aflame +afloat +aflutter +afoot +aforementioned +aforesaid +aforethought +afoul +afraid +afresh +aft +after +afterbirth +afterbirth's +afterbirths +afterburner +afterburner's +afterburners +aftercare +aftercare's +aftereffect +aftereffect's +aftereffects +afterglow +afterglow's +afterglows +afterlife +afterlife's +afterlives +aftermath +aftermath's +aftermaths +afternoon +afternoon's +afternoons +aftershave +aftershave's +aftershaves +aftershock +aftershock's +aftershocks +aftertaste +aftertaste's +aftertastes +afterthought +afterthought's +afterthoughts +afterward +afterwards +afterword +afterword's +afterwords +again +against +agape +agape's +agar +agar's +agate +agate's +agates +agave +agave's +age +age's +aged +ageing +ageing's +ageings +ageism +ageism's +ageless +agencies +agency +agency's +agenda +agenda's +agendas +agent +agent's +agents +ages +agglomerate +agglomerate's +agglomerated +agglomerates +agglomerating +agglomeration +agglomeration's +agglomerations +agglutinate +agglutinated +agglutinates +agglutinating +agglutination +agglutination's +agglutinations +aggrandize +aggrandized +aggrandizement +aggrandizement's +aggrandizes +aggrandizing +aggravate +aggravated +aggravates +aggravating +aggravation +aggravation's +aggravations +aggregate +aggregate's +aggregated +aggregates +aggregating +aggregation +aggregation's +aggregations +aggression +aggression's +aggressive +aggressively +aggressiveness +aggressiveness's +aggressor +aggressor's +aggressors +aggrieve +aggrieved +aggrieves +aggrieving +aghast +agile +agilely +agility +agility's +aging +aging's +agings +agism +agitate +agitated +agitates +agitating +agitation +agitation's +agitations +agitator +agitator's +agitators +agleam +aglitter +aglow +agnostic +agnostic's +agnosticism +agnosticism's +agnostics +ago +agog +agonies +agonize +agonized +agonizes +agonizing +agonizingly +agony +agony's +agrarian +agrarian's +agrarians +agree +agreeable +agreeably +agreed +agreeing +agreement +agreement's +agreements +agrees +agribusiness +agribusiness's +agribusinesses +agricultural +agriculturalist +agriculturalist's +agriculturalists +agriculture +agriculture's +agronomist +agronomist's +agronomists +agronomy +agronomy's +aground +ague +ague's +ah +aha +ahead +ahem +ahoy +aid +aid's +aide +aide's +aided +aides +aiding +aids +ail +ailed +aileron +aileron's +ailerons +ailing +ailment +ailment's +ailments +ails +aim +aim's +aimed +aiming +aimless +aimlessly +aimlessness +aimlessness's +aims +ain't +air +air's +airborne +airbrush +airbrush's +airbrushed +airbrushes +airbrushing +aircraft +aircraft's +airdrop +airdrop's +airdropped +airdropping +airdrops +aired +airfare +airfare's +airfares +airfield +airfield's +airfields +airfoil +airfoil's +airfoils +airhead +airhead's +airheads +airier +airiest +airily +airiness +airiness's +airing +airing's +airings +airless +airlift +airlift's +airlifted +airlifting +airlifts +airline +airline's +airliner +airliner's +airliners +airlines +airmail +airmail's +airmailed +airmailing +airmails +airman +airman's +airmen +airplane +airplane's +airplanes +airport +airport's +airports +airs +airship +airship's +airships +airsick +airsickness +airsickness's +airspace +airspace's +airstrip +airstrip's +airstrips +airtight +airwaves +airwaves's +airway +airway's +airways +airworthy +airy +aisle +aisle's +aisles +ajar +akimbo +akin +alabaster +alabaster's +alacrity +alacrity's +alarm +alarm's +alarmed +alarming +alarmingly +alarmist +alarmist's +alarmists +alarms +alas +alb +alb's +albacore +albacore's +albacores +albatross +albatross's +albatrosses +albeit +albino +albino's +albinos +albs +album +album's +albumen +albumen's +albumin +albumin's +albums +alchemist +alchemist's +alchemists +alchemy +alchemy's +alcohol +alcohol's +alcoholic +alcoholic's +alcoholics +alcoholism +alcoholism's +alcohols +alcove +alcove's +alcoves +alder +alder's +alderman +alderman's +aldermen +alders +alderwoman +alderwoman's +alderwomen +ale +ale's +alert +alert's +alerted +alerting +alertly +alertness +alertness's +alerts +ales +alfalfa +alfalfa's +alfresco +alga +alga's +algae +algebra +algebra's +algebraic +algebraically +algebras +algorithm +algorithm's +algorithmic +algorithms +alias +alias's +aliased +aliases +aliasing +alibi +alibi's +alibied +alibiing +alibis +alien +alien's +alienable +alienate +alienated +alienates +alienating +alienation +alienation's +aliened +aliening +aliens +alight +alighted +alighting +alights +align +aligned +aligning +alignment +alignment's +alignments +aligns +alike +alimentary +alimony +alimony's +aline +alined +alinement +alinement's +alinements +alines +alining +alit +alive +alkali +alkali's +alkalies +alkaline +alkalinity +alkalinity's +alkalis +alkaloid +alkaloid's +alkaloids +all +all's +allay +allayed +allaying +allays +allegation +allegation's +allegations +allege +alleged +allegedly +alleges +allegiance +allegiance's +allegiances +alleging +allegorical +allegorically +allegories +allegory +allegory's +allegro +allegro's +allegros +alleluia +alleluia's +alleluias +allergen +allergen's +allergenic +allergens +allergic +allergies +allergist +allergist's +allergists +allergy +allergy's +alleviate +alleviated +alleviates +alleviating +alleviation +alleviation's +alley +alley's +alleys +alleyway +alleyway's +alleyways +alliance +alliance's +alliances +allied +allies +alligator +alligator's +alligators +alliteration +alliteration's +alliterations +alliterative +allocate +allocated +allocates +allocating +allocation +allocation's +allocations +allot +allotment +allotment's +allotments +allots +allotted +allotting +allover +allow +allowable +allowance +allowance's +allowances +allowed +allowing +allows +alloy +alloy's +alloyed +alloying +alloys +allspice +allspice's +allude +alluded +alludes +alluding +allure +allure's +allured +allures +alluring +allusion +allusion's +allusions +allusive +allusively +alluvia +alluvial +alluvial's +alluvium +alluvium's +alluviums +ally +ally's +allying +almanac +almanac's +almanacs +almighty +almond +almond's +almonds +almost +alms +alms's +aloe +aloe's +aloes +aloft +aloha +aloha's +alohas +alone +along +alongside +aloof +aloofness +aloofness's +aloud +alpaca +alpaca's +alpacas +alpha +alpha's +alphabet +alphabet's +alphabetic +alphabetical +alphabetically +alphabetize +alphabetized +alphabetizes +alphabetizing +alphabets +alphanumeric +alphas +alpine +already +alright +also +altar +altar's +altars +alter +alterable +alteration +alteration's +alterations +altercation +altercation's +altercations +altered +altering +alternate +alternate's +alternated +alternately +alternates +alternating +alternation +alternation's +alternations +alternative +alternative's +alternatively +alternatives +alternator +alternator's +alternators +alters +altho +although +altimeter +altimeter's +altimeters +altitude +altitude's +altitudes +alto +alto's +altogether +altos +altruism +altruism's +altruist +altruist's +altruistic +altruistically +altruists +alum +alum's +aluminum +aluminum's +alumna +alumna's +alumnae +alumni +alumnus +alumnus's +alums +always +am +amalgam +amalgam's +amalgamate +amalgamated +amalgamates +amalgamating +amalgamation +amalgamation's +amalgamations +amalgams +amanuenses +amanuensis +amanuensis's +amaranth +amaranth's +amaranths +amaryllis +amaryllis's +amaryllises +amass +amassed +amasses +amassing +amateur +amateur's +amateurish +amateurism +amateurism's +amateurs +amatory +amaze +amaze's +amazed +amazement +amazement's +amazes +amazing +amazingly +amazon +amazon's +amazons +ambassador +ambassador's +ambassadorial +ambassadors +ambassadorship +ambassadorship's +ambassadorships +amber +amber's +ambergris +ambergris's +ambiance +ambiance's +ambiances +ambidextrous +ambidextrously +ambience +ambience's +ambiences +ambient +ambiguities +ambiguity +ambiguity's +ambiguous +ambiguously +ambition +ambition's +ambitions +ambitious +ambitiously +ambitiousness +ambitiousness's +ambivalence +ambivalence's +ambivalent +ambivalently +amble +amble's +ambled +ambles +ambling +ambrosia +ambrosia's +ambulance +ambulance's +ambulances +ambulatories +ambulatory +ambulatory's +ambush +ambush's +ambushed +ambushes +ambushing +ameba +ameba's +amebae +amebas +amebic +ameer +ameer's +ameers +ameliorate +ameliorated +ameliorates +ameliorating +amelioration +amelioration's +amen +amenable +amend +amendable +amended +amending +amendment +amendment's +amendments +amends +amenities +amenity +amenity's +amethyst +amethyst's +amethysts +amiability +amiability's +amiable +amiably +amicability +amicability's +amicable +amicably +amid +amidships +amidst +amigo +amigo's +amigos +amino +amir +amir's +amirs +amiss +amity +amity's +ammeter +ammeter's +ammeters +ammo +ammo's +ammonia +ammonia's +ammunition +ammunition's +amnesia +amnesia's +amnesiac +amnesiac's +amnesiacs +amnestied +amnesties +amnesty +amnesty's +amnestying +amniocenteses +amniocentesis +amniocentesis's +amoeba +amoeba's +amoebae +amoebas +amoebic +amok +among +amongst +amoral +amorality +amorality's +amorally +amorous +amorously +amorousness +amorousness's +amorphous +amorphously +amorphousness +amorphousness's +amortization +amortization's +amortizations +amortize +amortized +amortizes +amortizing +amount +amount's +amounted +amounting +amounts +amour +amour's +amours +amp +amp's +amperage +amperage's +ampere +ampere's +amperes +ampersand +ampersand's +ampersands +amphetamine +amphetamine's +amphetamines +amphibian +amphibian's +amphibians +amphibious +amphitheater +amphitheater's +amphitheaters +amphitheatre +amphitheatre's +amphitheatres +ample +ampler +amplest +amplification +amplification's +amplifications +amplified +amplifier +amplifier's +amplifiers +amplifies +amplify +amplifying +amplitude +amplitude's +amplitudes +amply +ampoule +ampoule's +ampoules +amps +ampul +ampul's +ampule +ampule's +ampules +ampuls +amputate +amputated +amputates +amputating +amputation +amputation's +amputations +amputee +amputee's +amputees +amuck +amulet +amulet's +amulets +amuse +amused +amusement +amusement's +amusements +amuses +amusing +amusingly +an +anachronism +anachronism's +anachronisms +anachronistic +anaconda +anaconda's +anacondas +anaemia +anaemia's +anaemic +anaerobic +anaesthesia +anaesthesia's +anaesthetic +anaesthetic's +anaesthetics +anaesthetist +anaesthetist's +anaesthetists +anaesthetize +anaesthetized +anaesthetizes +anaesthetizing +anagram +anagram's +anagrams +anal +analgesia +analgesia's +analgesic +analgesic's +analgesics +analog +analog's +analogies +analogous +analogously +analogs +analogue +analogue's +analogues +analogy +analogy's +analyses +analysis +analysis's +analyst +analyst's +analysts +analytic +analytical +analyticalally +analytically +analyze +analyzed +analyzer +analyzer's +analyzers +analyzes +analyzing +anapest +anapest's +anapests +anarchic +anarchically +anarchism +anarchism's +anarchist +anarchist's +anarchistic +anarchists +anarchy +anarchy's +anathema +anathema's +anathemas +anatomic +anatomical +anatomically +anatomies +anatomist +anatomist's +anatomists +anatomy +anatomy's +ancestor +ancestor's +ancestors +ancestral +ancestress +ancestress's +ancestresses +ancestries +ancestry +ancestry's +anchor +anchor's +anchorage +anchorage's +anchorages +anchored +anchoring +anchorite +anchorite's +anchorites +anchorman +anchorman's +anchormen +anchorpeople +anchorperson +anchorperson's +anchorpersons +anchors +anchorwoman +anchorwoman's +anchorwomen +anchovies +anchovy +anchovy's +ancient +ancient's +ancienter +ancientest +ancients +ancillaries +ancillary +ancillary's +and +andante +andante's +andantes +andiron +andiron's +andirons +androgen +androgen's +androgynous +android +android's +androids +anecdota +anecdotal +anecdote +anecdote's +anecdotes +anemia +anemia's +anemic +anemometer +anemometer's +anemometers +anemone +anemone's +anemones +anesthesia +anesthesia's +anesthesiologist +anesthesiologist's +anesthesiologists +anesthesiology +anesthesiology's +anesthetic +anesthetic's +anesthetics +anesthetist +anesthetist's +anesthetists +anesthetize +anesthetized +anesthetizes +anesthetizing +aneurism +aneurism's +aneurisms +aneurysm +aneurysm's +aneurysms +anew +angel +angel's +angelic +angelically +angels +anger +anger's +angered +angering +angers +angina +angina's +angioplasties +angioplasty +angioplasty's +angiosperm +angiosperm's +angiosperms +angle +angle's +angled +angler +angler's +anglers +angles +angleworm +angleworm's +angleworms +angling +angling's +angora +angora's +angoras +angrier +angriest +angrily +angry +angst +angst's +angstrom +angstrom's +angstroms +anguish +anguish's +anguished +anguishes +anguishing +angular +angularities +angularity +angularity's +ani +animal +animal's +animals +animate +animated +animatedly +animates +animating +animation +animation's +animations +animator +animator's +animators +anime +anime's +animism +animism's +animist +animist's +animistic +animists +animosities +animosity +animosity's +animus +animus's +anion +anion's +anions +anise +anise's +aniseed +aniseed's +ankh +ankh's +ankhs +ankle +ankle's +ankles +anklet +anklet's +anklets +annals +annals's +anneal +annealed +annealing +anneals +annex +annex's +annexation +annexation's +annexations +annexed +annexes +annexing +annihilate +annihilated +annihilates +annihilating +annihilation +annihilation's +annihilator +annihilator's +annihilators +anniversaries +anniversary +anniversary's +annotate +annotated +annotates +annotating +annotation +annotation's +annotations +announce +announced +announcement +announcement's +announcements +announcer +announcer's +announcers +announces +announcing +annoy +annoyance +annoyance's +annoyances +annoyed +annoying +annoyingly +annoys +annual +annual's +annually +annuals +annuities +annuity +annuity's +annul +annular +annulled +annulling +annulment +annulment's +annulments +annuls +anode +anode's +anodes +anodyne +anodyne's +anodynes +anoint +anointed +anointing +anointment +anointment's +anoints +anomalies +anomalous +anomaly +anomaly's +anon +anons +anonymity +anonymity's +anonymous +anonymously +anopheles +anopheles's +anorak +anorak's +anoraks +anorexia +anorexia's +anorexic +anorexic's +anorexics +another +answer +answer's +answerable +answered +answering +answers +ant +ant's +antacid +antacid's +antacids +antagonism +antagonism's +antagonisms +antagonist +antagonist's +antagonistic +antagonistically +antagonists +antagonize +antagonized +antagonizes +antagonizing +antarctic +ante +ante's +anteater +anteater's +anteaters +antebellum +antecedent +antecedent's +antecedents +antechamber +antechamber's +antechambers +anted +antedate +antedated +antedates +antedating +antediluvian +anteed +anteing +antelope +antelope's +antelopes +antenna +antenna's +antennae +antennas +anterior +anteroom +anteroom's +anterooms +antes +anthem +anthem's +anthems +anther +anther's +anthers +anthill +anthill's +anthills +anthologies +anthologist +anthologist's +anthologists +anthologize +anthologized +anthologizes +anthologizing +anthology +anthology's +anthracite +anthracite's +anthrax +anthrax's +anthropocentric +anthropoid +anthropoid's +anthropoids +anthropological +anthropologist +anthropologist's +anthropologists +anthropology +anthropology's +anthropomorphic +anthropomorphism +anthropomorphism's +anti +anti's +antiabortion +antiaircraft +antibiotic +antibiotic's +antibiotics +antibodies +antibody +antibody's +antic +antic's +anticipate +anticipated +anticipates +anticipating +anticipation +anticipation's +anticipations +anticipatory +anticked +anticking +anticlimactic +anticlimax +anticlimax's +anticlimaxes +anticlockwise +antics +anticyclone +anticyclone's +anticyclones +antidepressant +antidepressant's +antidepressants +antidote +antidote's +antidotes +antifreeze +antifreeze's +antigen +antigen's +antigens +antihero +antihero's +antiheroes +antihistamine +antihistamine's +antihistamines +antiknock +antiknock's +antimatter +antimatter's +antimony +antimony's +antiparticle +antiparticle's +antiparticles +antipasti +antipasto +antipasto's +antipastos +antipathetic +antipathies +antipathy +antipathy's +antipersonnel +antiperspirant +antiperspirant's +antiperspirants +antiphonal +antiphonal's +antiphonals +antipodes +antipodes's +antiquarian +antiquarian's +antiquarians +antiquaries +antiquary +antiquary's +antiquate +antiquated +antiquates +antiquating +antique +antique's +antiqued +antiques +antiquing +antiquities +antiquity +antiquity's +antis +antiseptic +antiseptic's +antiseptically +antiseptics +antislavery +antisocial +antitheses +antithesis +antithesis's +antithetical +antithetically +antitoxin +antitoxin's +antitoxins +antitrust +antiviral +antiviral's +antivirals +antivirus +antiwar +antler +antler's +antlered +antlers +antonym +antonym's +antonyms +ants +anus +anus's +anuses +anvil +anvil's +anvils +anxieties +anxiety +anxiety's +anxious +anxiously +any +anybodies +anybody +anybody's +anyhow +anymore +anyone +anyone's +anyplace +anything +anything's +anythings +anytime +anyway +anywhere +aorta +aorta's +aortae +aortas +apace +apart +apartheid +apartheid's +apartment +apartment's +apartments +apathetic +apathetically +apathy +apathy's +ape +ape's +aped +aperitif +aperitif's +aperitifs +aperture +aperture's +apertures +apes +apex +apex's +apexes +aphasia +aphasia's +aphasic +aphasic's +aphasics +aphelia +aphelion +aphelion's +aphelions +aphid +aphid's +aphids +aphorism +aphorism's +aphorisms +aphoristic +aphrodisiac +aphrodisiac's +aphrodisiacs +apiaries +apiary +apiary's +apices +apiece +aping +aplenty +aplomb +aplomb's +apocalypse +apocalypse's +apocalypses +apocalyptic +apocryphal +apogee +apogee's +apogees +apolitical +apologetic +apologetically +apologia +apologia's +apologias +apologies +apologist +apologist's +apologists +apologize +apologized +apologizes +apologizing +apology +apology's +apoplectic +apoplexies +apoplexy +apoplexy's +apostasies +apostasy +apostasy's +apostate +apostate's +apostates +apostle +apostle's +apostles +apostolic +apostrophe +apostrophe's +apostrophes +apothecaries +apothecary +apothecary's +apotheoses +apotheosis +apotheosis's +appal +appall +appalled +appalling +appallingly +appalls +appals +apparatus +apparatus's +apparatuses +apparel +apparel's +appareled +appareling +apparelled +apparelling +apparels +apparent +apparently +apparition +apparition's +apparitions +appeal +appeal's +appealed +appealing +appeals +appear +appearance +appearance's +appearances +appeared +appearing +appears +appease +appeased +appeasement +appeasement's +appeasements +appeaser +appeaser's +appeasers +appeases +appeasing +appellant +appellant's +appellants +appellate +appellation +appellation's +appellations +append +appendage +appendage's +appendages +appendectomies +appendectomy +appendectomy's +appended +appendices +appendicitis +appendicitis's +appending +appendix +appendix's +appendixes +appends +appertain +appertained +appertaining +appertains +appetite +appetite's +appetites +appetizer +appetizer's +appetizers +appetizing +appetizingly +applaud +applauded +applauding +applauds +applause +applause's +apple +apple's +applejack +applejack's +apples +applesauce +applesauce's +appliance +appliance's +appliances +applicability +applicability's +applicable +applicant +applicant's +applicants +application +application's +applications +applicator +applicator's +applicators +applied +applies +appliqué +appliqué's +appliquéd +appliquéing +appliqués +apply +applying +appoint +appointed +appointee +appointee's +appointees +appointing +appointive +appointment +appointment's +appointments +appoints +apportion +apportioned +apportioning +apportionment +apportionment's +apportions +apposite +appositely +appositeness +appositeness's +apposition +apposition's +appositive +appositive's +appositives +appraisal +appraisal's +appraisals +appraise +appraised +appraiser +appraiser's +appraisers +appraises +appraising +appreciable +appreciably +appreciate +appreciated +appreciates +appreciating +appreciation +appreciation's +appreciations +appreciative +appreciatively +apprehend +apprehended +apprehending +apprehends +apprehension +apprehension's +apprehensions +apprehensive +apprehensively +apprehensiveness +apprehensiveness's +apprentice +apprentice's +apprenticed +apprentices +apprenticeship +apprenticeship's +apprenticeships +apprenticing +apprise +apprised +apprises +apprising +approach +approach's +approachable +approached +approaches +approaching +approbation +approbation's +approbations +appropriate +appropriated +appropriately +appropriateness +appropriateness's +appropriates +appropriating +appropriation +appropriation's +appropriations +approval +approval's +approvals +approve +approved +approves +approving +approvingly +approximate +approximated +approximately +approximates +approximating +approximation +approximation's +approximations +apps +appurtenance +appurtenance's +appurtenances +apricot +apricot's +apricots +apron +apron's +aprons +apropos +apse +apse's +apses +apt +apter +aptest +aptitude +aptitude's +aptitudes +aptly +aptness +aptness's +aqua +aqua's +aquaculture +aquaculture's +aquae +aquamarine +aquamarine's +aquamarines +aquanaut +aquanaut's +aquanauts +aquaplane +aquaplane's +aquaplaned +aquaplanes +aquaplaning +aquaria +aquarium +aquarium's +aquariums +aquas +aquatic +aquatic's +aquatics +aquavit +aquavit's +aqueduct +aqueduct's +aqueducts +aqueous +aquiculture +aquiculture's +aquifer +aquifer's +aquifers +aquiline +arabesque +arabesque's +arabesques +arable +arachnid +arachnid's +arachnids +arbiter +arbiter's +arbiters +arbitrarily +arbitrariness +arbitrariness's +arbitrary +arbitrate +arbitrated +arbitrates +arbitrating +arbitration +arbitration's +arbitrator +arbitrator's +arbitrators +arbor +arbor's +arboreal +arboreta +arboretum +arboretum's +arboretums +arbors +arborvitae +arborvitae's +arborvitaes +arbutus +arbutus's +arbutuses +arc +arc's +arcade +arcade's +arcades +arcane +arced +arch +arch's +archaeological +archaeologist +archaeologist's +archaeologists +archaeology +archaeology's +archaic +archaically +archaism +archaism's +archaisms +archangel +archangel's +archangels +archbishop +archbishop's +archbishopric +archbishopric's +archbishoprics +archbishops +archdeacon +archdeacon's +archdeacons +archdiocese +archdiocese's +archdioceses +archduke +archduke's +archdukes +arched +archenemies +archenemy +archenemy's +archeological +archeologist +archeologist's +archeologists +archeology +archeology's +archer +archer's +archers +archery +archery's +arches +archest +archetypal +archetype +archetype's +archetypes +arching +archipelago +archipelago's +archipelagoes +archipelagos +architect +architect's +architects +architectural +architecturally +architecture +architecture's +architectures +archive +archive's +archived +archives +archiving +archivist +archivist's +archivists +archly +archness +archness's +archway +archway's +archways +arcing +arcked +arcking +arcs +arctic +arctic's +arctics +ardent +ardently +ardor +ardor's +ardors +arduous +arduously +arduousness +arduousness's +are +are's +area +area's +areas +aren't +arena +arena's +arenas +ares +argon +argon's +argosies +argosy +argosy's +argot +argot's +argots +arguable +arguably +argue +argued +argues +arguing +argument +argument's +argumentation +argumentation's +argumentative +arguments +argyle +argyle's +argyles +aria +aria's +arias +arid +aridity +aridity's +aright +arise +arisen +arises +arising +aristocracies +aristocracy +aristocracy's +aristocrat +aristocrat's +aristocratic +aristocratically +aristocrats +arithmetic +arithmetic's +arithmetical +arithmetically +ark +ark's +arks +arm +arm's +armada +armada's +armadas +armadillo +armadillo's +armadillos +armament +armament's +armaments +armature +armature's +armatures +armband +armband's +armbands +armchair +armchair's +armchairs +armed +armful +armful's +armfuls +armhole +armhole's +armholes +armies +arming +armistice +armistice's +armistices +armlet +armlet's +armlets +armor +armor's +armored +armorer +armorer's +armorers +armories +armoring +armors +armory +armory's +armpit +armpit's +armpits +armrest +armrest's +armrests +arms +armsful +army +army's +aroma +aroma's +aromas +aromatherapy +aromatherapy's +aromatic +aromatic's +aromatics +arose +around +arousal +arousal's +arouse +aroused +arouses +arousing +arpeggio +arpeggio's +arpeggios +arraign +arraigned +arraigning +arraignment +arraignment's +arraignments +arraigns +arrange +arranged +arrangement +arrangement's +arrangements +arranger +arranger's +arrangers +arranges +arranging +arrant +array +array's +arrayed +arraying +arrays +arrears +arrears's +arrest +arrest's +arrested +arresting +arrests +arrival +arrival's +arrivals +arrive +arrived +arrives +arriving +arrogance +arrogance's +arrogant +arrogantly +arrogate +arrogated +arrogates +arrogating +arrow +arrow's +arrowhead +arrowhead's +arrowheads +arrowroot +arrowroot's +arrows +arroyo +arroyo's +arroyos +arsenal +arsenal's +arsenals +arsenic +arsenic's +arson +arson's +arsonist +arsonist's +arsonists +art +art's +artefact +artefact's +artefacts +arterial +arteries +arteriosclerosis +arteriosclerosis's +artery +artery's +artful +artfully +artfulness +artfulness's +arthritic +arthritic's +arthritics +arthritis +arthritis's +arthropod +arthropod's +arthropods +artichoke +artichoke's +artichokes +article +article's +articles +articulate +articulated +articulately +articulateness +articulateness's +articulates +articulating +articulation +articulation's +articulations +artier +artiest +artifact +artifact's +artifacts +artifice +artifice's +artificer +artificer's +artificers +artifices +artificial +artificiality +artificiality's +artificially +artillery +artillery's +artisan +artisan's +artisans +artist +artist's +artiste +artiste's +artistes +artistic +artistically +artistry +artistry's +artists +artless +artlessly +artlessness +artlessness's +arts +artsier +artsiest +artsy +artwork +artwork's +artworks +arty +as +asbestos +asbestos's +ascend +ascendancy +ascendancy's +ascendant +ascendant's +ascendants +ascended +ascendency +ascendency's +ascendent +ascendent's +ascendents +ascending +ascends +ascension +ascension's +ascensions +ascent +ascent's +ascents +ascertain +ascertainable +ascertained +ascertaining +ascertains +ascetic +ascetic's +asceticism +asceticism's +ascetics +ascot +ascot's +ascots +ascribable +ascribe +ascribed +ascribes +ascribing +ascription +ascription's +aseptic +asexual +asexually +ash +ash's +ashamed +ashamedly +ashcan +ashcan's +ashcans +ashed +ashen +ashes +ashier +ashiest +ashing +ashore +ashram +ashram's +ashrams +ashtray +ashtray's +ashtrays +ashy +aside +aside's +asides +asinine +asininities +asininity +asininity's +ask +askance +asked +askew +asking +asks +aslant +asleep +asocial +asp +asp's +asparagus +asparagus's +aspartame +aspartame's +aspect +aspect's +aspects +aspen +aspen's +aspens +asperities +asperity +asperity's +aspersion +aspersion's +aspersions +asphalt +asphalt's +asphalted +asphalting +asphalts +asphyxia +asphyxia's +asphyxiate +asphyxiated +asphyxiates +asphyxiating +asphyxiation +asphyxiation's +asphyxiations +aspic +aspic's +aspics +aspirant +aspirant's +aspirants +aspirate +aspirate's +aspirated +aspirates +aspirating +aspiration +aspiration's +aspirations +aspire +aspired +aspires +aspirin +aspirin's +aspiring +aspirins +asps +ass +ass's +assail +assailable +assailant +assailant's +assailants +assailed +assailing +assails +assassin +assassin's +assassinate +assassinated +assassinates +assassinating +assassination +assassination's +assassinations +assassins +assault +assault's +assaulted +assaulter +assaulting +assaults +assay +assay's +assayed +assaying +assays +assemblage +assemblage's +assemblages +assemble +assembled +assembler +assembler's +assemblers +assembles +assemblies +assembling +assembly +assembly's +assemblyman +assemblyman's +assemblymen +assemblywoman +assemblywoman's +assemblywomen +assent +assent's +assented +assenting +assents +assert +asserted +asserting +assertion +assertion's +assertions +assertive +assertively +assertiveness +assertiveness's +asserts +asses +assess +assessed +assesses +assessing +assessment +assessment's +assessments +assessor +assessor's +assessors +asset +asset's +assets +asseverate +asseverated +asseverates +asseverating +asshole +asshole's +assholes +assiduous +assiduously +assiduousness +assiduousness's +assign +assign's +assignable +assignation +assignation's +assignations +assigned +assigning +assignment +assignment's +assignments +assigns +assimilate +assimilated +assimilates +assimilating +assimilation +assimilation's +assist +assist's +assistance +assistance's +assistant +assistant's +assistants +assisted +assisting +assists +assize +assize's +assizes +associate +associate's +associated +associates +associating +association +association's +associations +associative +assonance +assonance's +assort +assorted +assorting +assortment +assortment's +assortments +assorts +assuage +assuaged +assuages +assuaging +assume +assumed +assumes +assuming +assumption +assumption's +assumptions +assurance +assurance's +assurances +assure +assured +assured's +assuredly +assureds +assures +assuring +aster +aster's +asterisk +asterisk's +asterisked +asterisking +asterisks +astern +asteroid +asteroid's +asteroids +asters +asthma +asthma's +asthmatic +asthmatic's +asthmatics +astigmatic +astigmatism +astigmatism's +astigmatisms +astir +astonish +astonished +astonishes +astonishing +astonishingly +astonishment +astonishment's +astound +astounded +astounding +astoundingly +astounds +astrakhan +astrakhan's +astral +astray +astride +astringency +astringency's +astringent +astringent's +astringents +astrologer +astrologer's +astrologers +astrological +astrology +astrology's +astronaut +astronaut's +astronautics +astronautics's +astronauts +astronomer +astronomer's +astronomers +astronomic +astronomical +astronomically +astronomy +astronomy's +astrophysicist +astrophysicist's +astrophysicists +astrophysics +astrophysics's +astute +astutely +astuteness +astuteness's +astuter +astutest +asunder +asylum +asylum's +asylums +asymmetric +asymmetrical +asymmetrically +asymmetry +asymmetry's +asymptotic +asymptotically +asynchronous +asynchronously +at +atavism +atavism's +atavistic +ate +atelier +atelier's +ateliers +atheism +atheism's +atheist +atheist's +atheistic +atheists +atherosclerosis +atherosclerosis's +athlete +athlete's +athletes +athletic +athletically +athletics +athletics's +atlas +atlas's +atlases +atmosphere +atmosphere's +atmospheres +atmospheric +atmospherically +atoll +atoll's +atolls +atom +atom's +atomic +atomizer +atomizer's +atomizers +atoms +atonal +atonality +atonality's +atone +atoned +atonement +atonement's +atones +atoning +atop +atria +atrium +atrium's +atriums +atrocious +atrociously +atrociousness +atrociousness's +atrocities +atrocity +atrocity's +atrophied +atrophies +atrophy +atrophy's +atrophying +attach +attached +attaching +attachment +attachment's +attachments +attaché +attaché's +attachés +attack +attack's +attacked +attacker +attacker's +attackers +attacking +attacks +attain +attainable +attained +attaining +attainment +attainment's +attainments +attains +attar +attar's +attempt +attempt's +attempted +attempting +attempts +attend +attendance +attendance's +attendances +attendant +attendant's +attendants +attended +attender +attending +attends +attention +attention's +attentions +attentive +attentively +attentiveness +attentiveness's +attenuate +attenuated +attenuates +attenuating +attenuation +attenuation's +attest +attestation +attestation's +attestations +attested +attesting +attests +attic +attic's +attics +attire +attire's +attired +attires +attiring +attitude +attitude's +attitudes +attitudinize +attitudinized +attitudinizes +attitudinizing +attorney +attorney's +attorneys +attract +attracted +attracting +attraction +attraction's +attractions +attractive +attractively +attractiveness +attractiveness's +attracts +attributable +attribute +attribute's +attributed +attributes +attributing +attribution +attribution's +attributions +attributive +attributive's +attributively +attributives +attrition +attrition's +attune +attuned +attunes +attuning +atwitter +atypical +atypically +auburn +auburn's +auction +auction's +auctioned +auctioneer +auctioneer's +auctioneers +auctioning +auctions +audacious +audaciously +audaciousness +audaciousness's +audacity +audacity's +audibility +audibility's +audible +audible's +audibles +audibly +audience +audience's +audiences +audio +audio's +audiophile +audiophile's +audiophiles +audios +audiovisual +audit +audit's +audited +auditing +audition +audition's +auditioned +auditioning +auditions +auditor +auditor's +auditoria +auditorium +auditorium's +auditoriums +auditors +auditory +audits +auger +auger's +augers +aught +aught's +aughts +augment +augmentation +augmentation's +augmentations +augmented +augmenting +augments +augur +augur's +augured +auguries +auguring +augurs +augury +augury's +august +auguster +augustest +auk +auk's +auks +aunt +aunt's +aunts +aura +aura's +aurae +aural +aurally +auras +aureola +aureola's +aureolas +aureole +aureole's +aureoles +auricle +auricle's +auricles +auspice +auspice's +auspices +auspicious +auspiciously +auspiciousness +auspiciousness's +austere +austerely +austerer +austerest +austerities +austerity +austerity's +authentic +authentically +authenticate +authenticated +authenticates +authenticating +authentication +authentication's +authentications +authenticity +authenticity's +author +author's +authored +authoring +authoritarian +authoritarian's +authoritarianism +authoritarianism's +authoritarians +authoritative +authoritatively +authoritativeness +authoritativeness's +authorities +authority +authority's +authorization +authorization's +authorizations +authorize +authorized +authorizes +authorizing +authors +authorship +authorship's +autism +autism's +autistic +auto +auto's +autobiographical +autobiographies +autobiography +autobiography's +autocracies +autocracy +autocracy's +autocrat +autocrat's +autocratic +autocratically +autocrats +autograph +autograph's +autographed +autographing +autographs +autoimmune +automata +automate +automated +automates +automatic +automatic's +automatically +automatics +automating +automation +automation's +automaton +automaton's +automatons +automobile +automobile's +automobiled +automobiles +automobiling +automotive +autonomous +autonomously +autonomy +autonomy's +autopilot +autopilot's +autopilots +autopsied +autopsies +autopsy +autopsy's +autopsying +autos +autoworker +autoworker's +autoworkers +autumn +autumn's +autumnal +autumns +auxiliaries +auxiliary +auxiliary's +avail +avail's +availability +availability's +available +availed +availing +avails +avalanche +avalanche's +avalanches +avarice +avarice's +avaricious +avariciously +avast +avatar +avatar's +avatars +avenge +avenged +avenger +avenger's +avengers +avenges +avenging +avenue +avenue's +avenues +aver +average +average's +averaged +averages +averaging +averred +averring +avers +averse +aversion +aversion's +aversions +avert +averted +averting +averts +avian +aviaries +aviary +aviary's +aviation +aviation's +aviator +aviator's +aviators +aviatrices +aviatrix +aviatrix's +aviatrixes +avid +avidity +avidity's +avidly +avionics +avionics's +avocado +avocado's +avocadoes +avocados +avocation +avocation's +avocations +avoid +avoidable +avoidably +avoidance +avoidance's +avoided +avoiding +avoids +avoirdupois +avoirdupois's +avow +avowal +avowal's +avowals +avowed +avowedly +avowing +avows +avuncular +await +awaited +awaiting +awaits +awake +awaked +awaken +awakened +awakening +awakening's +awakenings +awakens +awakes +awaking +award +award's +awarded +awarding +awards +aware +awareness +awareness's +awash +away +awe +awe's +awed +aweigh +awes +awesome +awesomely +awestricken +awestruck +awful +awfuller +awfullest +awfully +awfulness +awfulness's +awhile +awing +awkward +awkwarder +awkwardest +awkwardly +awkwardness +awkwardness's +awl +awl's +awls +awning +awning's +awnings +awoke +awoken +awol +awry +ax +ax's +axe +axe's +axed +axes +axial +axing +axiom +axiom's +axiomatic +axiomatically +axioms +axis +axis's +axle +axle's +axles +axon +axon's +axons +ay +ay's +ayatollah +ayatollah's +ayatollahs +aye +aye's +ayes +azalea +azalea's +azaleas +azimuth +azimuth's +azimuths +azure +azure's +azures +b +baa +baa's +baaed +baaing +baas +babble +babble's +babbled +babbler +babbler's +babblers +babbles +babbling +babe +babe's +babel +babel's +babels +babes +babied +babier +babies +babiest +baboon +baboon's +baboons +babushka +babushka's +babushkas +baby +baby's +babyhood +babyhood's +babying +babyish +babysat +babysit +babysits +babysitter +babysitter's +babysitters +babysitting +baccalaureate +baccalaureate's +baccalaureates +bacchanal +bacchanal's +bacchanalian +bacchanalian's +bacchanalians +bacchanals +bachelor +bachelor's +bachelors +bacilli +bacillus +bacillus's +back +back's +backache +backache's +backaches +backbit +backbite +backbiter +backbiter's +backbiters +backbites +backbiting +backbitten +backboard +backboard's +backboards +backbone +backbone's +backbones +backbreaking +backdate +backdated +backdates +backdating +backdrop +backdrop's +backdrops +backed +backer +backer's +backers +backfield +backfield's +backfields +backfire +backfire's +backfired +backfires +backfiring +backgammon +backgammon's +background +background's +backgrounds +backhand +backhand's +backhanded +backhanding +backhands +backhoe +backhoe's +backhoes +backing +backing's +backings +backlash +backlash's +backlashes +backless +backlog +backlog's +backlogged +backlogging +backlogs +backpack +backpack's +backpacked +backpacker +backpacker's +backpackers +backpacking +backpacks +backpedal +backpedaled +backpedaling +backpedalled +backpedalling +backpedals +backrest +backrest's +backrests +backs +backside +backside's +backsides +backslapper +backslapper's +backslappers +backslash +backslash's +backslashes +backslid +backslidden +backslide +backslider +backslider's +backsliders +backslides +backsliding +backspace +backspace's +backspaced +backspaces +backspacing +backspin +backspin's +backstabbing +backstage +backstage's +backstairs +backstop +backstop's +backstopped +backstopping +backstops +backstories +backstory +backstretch +backstretch's +backstretches +backstroke +backstroke's +backstroked +backstrokes +backstroking +backtrack +backtracked +backtracking +backtracks +backup +backup's +backups +backward +backwardness +backwardness's +backwards +backwash +backwash's +backwater +backwater's +backwaters +backwoods +backwoods's +backyard +backyard's +backyards +bacon +bacon's +bacteria +bacteria's +bacterial +bacterias +bacteriological +bacteriologist +bacteriologist's +bacteriologists +bacteriology +bacteriology's +bacterium +bacterium's +bad +bad's +badder +baddest +bade +badge +badge's +badger +badger's +badgered +badgering +badgers +badges +badinage +badinage's +badlands +badlands's +badly +badminton +badminton's +badmouth +badmouthed +badmouthing +badmouths +badness +badness's +baffle +baffle's +baffled +bafflement +bafflement's +baffles +baffling +bag +bag's +bagatelle +bagatelle's +bagatelles +bagel +bagel's +bagels +baggage +baggage's +bagged +baggier +baggiest +bagginess +bagginess's +bagging +baggy +bagpipe +bagpipe's +bagpipes +bags +bah +bail +bail's +bailed +bailiff +bailiffs +bailing +bailiwick +bailiwick's +bailiwicks +bailout +bailout's +bailouts +bails +bait +bait's +baited +baiting +baits +baize +baize's +bake +bake's +baked +baker +baker's +bakeries +bakers +bakery +bakery's +bakes +baking +balalaika +balalaika's +balalaikas +balance +balance's +balanced +balances +balancing +balconies +balcony +balcony's +bald +balded +balder +balderdash +balderdash's +baldest +balding +baldly +baldness +baldness's +balds +bale +bale's +baled +baleen +baleen's +baleful +balefully +bales +baling +balk +balk's +balked +balkier +balkiest +balking +balks +balky +ball +ball's +ballad +ballad's +balladeer +balladeer's +balladeers +ballads +ballast +ballast's +ballasted +ballasting +ballasts +balled +ballerina +ballerina's +ballerinas +ballet +ballet's +ballets +balling +ballistic +ballistics +ballistics's +balloon +balloon's +ballooned +ballooning +balloonist +balloonist's +balloonists +balloons +ballot +ballot's +balloted +balloting +ballots +ballpark +ballpark's +ballparks +ballplayer +ballplayer's +ballplayers +ballpoint +ballpoint's +ballpoints +ballroom +ballroom's +ballrooms +balls +ballsier +ballsiest +ballsy +ballyhoo +ballyhoo's +ballyhooed +ballyhooing +ballyhoos +balm +balm's +balmier +balmiest +balminess +balminess's +balms +balmy +baloney +baloney's +balsa +balsa's +balsam +balsam's +balsams +balsas +baluster +baluster's +balusters +balustrade +balustrade's +balustrades +bamboo +bamboo's +bamboos +bamboozle +bamboozled +bamboozles +bamboozling +ban +ban's +banal +banalities +banality +banality's +banana +banana's +bananas +band +band's +bandage +bandage's +bandaged +bandages +bandaging +bandana +bandana's +bandanas +bandanna +bandanna's +bandannas +banded +bandied +bandier +bandies +bandiest +banding +bandit +bandit's +banditry +banditry's +bandits +banditti +bandoleer +bandoleer's +bandoleers +bandolier +bandolier's +bandoliers +bands +bandstand +bandstand's +bandstands +bandwagon +bandwagon's +bandwagons +bandwidth +bandy +bandying +bane +bane's +baneful +banes +bang +bang's +banged +banging +bangle +bangle's +bangles +bangs +bani +banish +banished +banishes +banishing +banishment +banishment's +banister +banister's +banisters +banjo +banjo's +banjoes +banjoist +banjoist's +banjoists +banjos +bank +bank's +bankbook +bankbook's +bankbooks +banked +banker +banker's +bankers +banking +banking's +banknote +banknote's +banknotes +bankroll +bankroll's +bankrolled +bankrolling +bankrolls +bankrupt +bankrupt's +bankruptcies +bankruptcy +bankruptcy's +bankrupted +bankrupting +bankrupts +banks +banned +banner +banner's +banners +banning +bannister +bannister's +bannisters +banns +banns's +banquet +banquet's +banqueted +banqueting +banquets +bans +banshee +banshee's +banshees +bantam +bantam's +bantams +bantamweight +bantamweight's +bantamweights +banter +banter's +bantered +bantering +banters +banyan +banyan's +banyans +baobab +baobab's +baobabs +baptism +baptism's +baptismal +baptisms +baptist +baptisteries +baptistery +baptistery's +baptistries +baptistry +baptistry's +baptists +baptize +baptized +baptizes +baptizing +bar +bar's +barb +barb's +barbacoa +barbarian +barbarian's +barbarians +barbaric +barbarism +barbarism's +barbarisms +barbarities +barbarity +barbarity's +barbarous +barbarously +barbecue +barbecue's +barbecued +barbecues +barbecuing +barbed +barbell +barbell's +barbells +barbeque +barbeque's +barbequed +barbeques +barbequing +barber +barber's +barbered +barbering +barberries +barberry +barberry's +barbers +barbershop +barbershop's +barbershops +barbing +barbiturate +barbiturate's +barbiturates +barbs +bard +bard's +bards +bare +bareback +bared +barefaced +barefoot +barefooted +barehanded +bareheaded +barely +bareness +bareness's +barer +bares +barest +barf +barf's +barfed +barfing +barfs +bargain +bargain's +bargained +bargainer +bargaining +bargains +barge +barge's +barged +barges +barging +baring +barista +barista's +baristas +baritone +baritone's +baritones +barium +barium's +bark +bark's +barked +barker +barker's +barkers +barking +barks +barley +barley's +barmaid +barmaid's +barmaids +barman +barn +barn's +barnacle +barnacle's +barnacles +barns +barnstorm +barnstormed +barnstorming +barnstorms +barnyard +barnyard's +barnyards +barometer +barometer's +barometers +barometric +baron +baron's +baroness +baroness's +baronesses +baronet +baronet's +baronets +baronial +barons +baroque +baroque's +barrack +barrack's +barracks +barracuda +barracuda's +barracudas +barrage +barrage's +barraged +barrages +barraging +barred +barrel +barrel's +barreled +barreling +barrelled +barrelling +barrels +barren +barren's +barrener +barrenest +barrenness +barrenness's +barrens +barrette +barrette's +barrettes +barricade +barricade's +barricaded +barricades +barricading +barrier +barrier's +barriers +barring +barrings +barrio +barrio's +barrios +barrister +barrister's +barristers +barroom +barroom's +barrooms +barrow +barrow's +barrows +bars +bartender +bartender's +bartenders +barter +barter's +bartered +bartering +barters +basal +basalt +basalt's +base +base's +baseball +baseball's +baseballs +baseboard +baseboard's +baseboards +based +baseless +baseline +baseline's +baselines +basely +baseman +baseman's +basemen +basement +basement's +basements +baseness +baseness's +baser +bases +basest +bash +bash's +bashed +bashes +bashful +bashfully +bashfulness +bashfulness's +bashing +bashing's +basic +basic's +basically +basics +basil +basil's +basilica +basilica's +basilicas +basin +basin's +basing +basins +basis +basis's +bask +basked +basket +basket's +basketball +basketball's +basketballs +baskets +basking +basks +bass +bass's +basses +bassi +bassinet +bassinet's +bassinets +bassist +bassist's +bassists +basso +basso's +bassoon +bassoon's +bassoonist +bassoonist's +bassoonists +bassoons +bassos +bast +bast's +bastard +bastard's +bastardize +bastardized +bastardizes +bastardizing +bastards +baste +basted +bastes +basting +bastion +bastion's +bastions +bat +bat's +batch +batch's +batched +batches +batching +bate +bated +bates +bath +bath's +bathe +bathe's +bathed +bather +bather's +bathers +bathes +bathhouse +bathhouse's +bathhouses +bathing +bathmat +bathmat's +bathmats +bathos +bathos's +bathrobe +bathrobe's +bathrobes +bathroom +bathroom's +bathrooms +baths +bathtub +bathtub's +bathtubs +batik +batik's +batiks +bating +baton +baton's +batons +bats +batsman +batsman's +batsmen +battalion +battalion's +battalions +batted +batten +batten's +battened +battening +battens +batter +batter's +battered +batteries +battering +batters +battery +battery's +battier +battiest +batting +batting's +battle +battle's +battled +battlefield +battlefield's +battlefields +battleground +battleground's +battlegrounds +battlement +battlement's +battlements +battles +battleship +battleship's +battleships +battling +batty +bauble +bauble's +baubles +baud +baud's +bauds +bauxite +bauxite's +bawdier +bawdiest +bawdily +bawdiness +bawdiness's +bawdy +bawl +bawl's +bawled +bawling +bawls +bay +bay's +bayberries +bayberry +bayberry's +bayed +baying +bayonet +bayonet's +bayoneted +bayoneting +bayonets +bayonetted +bayonetting +bayou +bayou's +bayous +bays +bazaar +bazaar's +bazaars +bazillion +bazillions +bazooka +bazooka's +bazookas +be +beach +beach's +beachcomber +beachcomber's +beachcombers +beached +beaches +beachhead +beachhead's +beachheads +beaching +beacon +beacon's +beacons +bead +bead's +beaded +beadier +beadiest +beading +beads +beady +beagle +beagle's +beagles +beak +beak's +beaked +beaker +beaker's +beakers +beaks +beam +beam's +beamed +beaming +beams +bean +bean's +beanbag +beanbag's +beanbags +beaned +beaning +beans +bear +bear's +bearable +beard +beard's +bearded +bearding +beards +bearer +bearer's +bearers +bearing +bearing's +bearings +bearish +bears +bearskin +bearskin's +bearskins +beast +beast's +beastlier +beastliest +beastliness +beastliness's +beastly +beastly's +beasts +beat +beat's +beaten +beater +beater's +beaters +beatific +beatification +beatification's +beatifications +beatified +beatifies +beatify +beatifying +beating +beating's +beatings +beatitude +beatitude's +beatitudes +beatnik +beatnik's +beatniks +beats +beau +beau's +beaus +beauteous +beauteously +beautician +beautician's +beauticians +beauties +beautification +beautification's +beautified +beautifier +beautifier's +beautifiers +beautifies +beautiful +beautifully +beautify +beautifying +beauty +beauty's +beaux +beaver +beaver's +beavered +beavering +beavers +bebop +bebop's +bebops +becalm +becalmed +becalming +becalms +became +because +beck +beck's +beckon +beckoned +beckoning +beckons +becks +become +becomes +becoming +becomingly +bed +bed's +bedazzle +bedazzled +bedazzles +bedazzling +bedbug +bedbug's +bedbugs +bedclothes +bedclothes's +bedded +bedder +bedding +bedding's +bedeck +bedecked +bedecking +bedecks +bedevil +bedeviled +bedeviling +bedevilled +bedevilling +bedevilment +bedevilment's +bedevils +bedfellow +bedfellow's +bedfellows +bedlam +bedlam's +bedlams +bedpan +bedpan's +bedpans +bedraggle +bedraggled +bedraggles +bedraggling +bedridden +bedrock +bedrock's +bedrocks +bedroll +bedroll's +bedrolls +bedroom +bedroom's +bedrooms +beds +bedside +bedside's +bedsides +bedsore +bedsore's +bedsores +bedspread +bedspread's +bedspreads +bedstead +bedstead's +bedsteads +bedtime +bedtime's +bedtimes +bee +bee's +beech +beech's +beeches +beechnut +beechnut's +beechnuts +beef +beef's +beefburger +beefed +beefier +beefiest +beefing +beefs +beefsteak +beefsteak's +beefsteaks +beefy +beehive +beehive's +beehives +beekeeper +beekeeper's +beekeepers +beekeeping +beekeeping's +beeline +beeline's +beelines +been +beep +beep's +beeped +beeper +beeper's +beepers +beeping +beeps +beer +beer's +beers +bees +beeswax +beeswax's +beet +beet's +beetle +beetle's +beetled +beetles +beetling +beets +beeves +befall +befallen +befalling +befalls +befell +befit +befits +befitted +befitting +befog +befogged +befogging +befogs +before +beforehand +befoul +befouled +befouling +befouls +befriend +befriended +befriending +befriends +befuddle +befuddled +befuddles +befuddling +beg +began +begat +beget +begets +begetting +beggar +beggar's +beggared +beggaring +beggarly +beggars +begged +begging +begin +beginner +beginner's +beginners +beginning +beginning's +beginnings +begins +begone +begonia +begonia's +begonias +begot +begotten +begrudge +begrudged +begrudges +begrudging +begrudgingly +begs +beguile +beguiled +beguiles +beguiling +beguilingly +begun +behalf +behalf's +behalves +behave +behaved +behaves +behaving +behavior +behavior's +behavioral +behead +beheaded +beheading +beheads +beheld +behemoth +behemoth's +behemoths +behest +behest's +behests +behind +behind's +behinds +behold +beholden +beholder +beholder's +beholders +beholding +beholds +behoove +behooved +behooves +behooving +beige +beige's +being +being's +beings +belabor +belabored +belaboring +belabors +belated +belatedly +belay +belayed +belaying +belays +belch +belch's +belched +belches +belching +beleaguer +beleaguered +beleaguering +beleaguers +belfries +belfry +belfry's +belie +belied +belief +belief's +beliefs +belies +believable +believe +believed +believer +believer's +believers +believes +believing +belittle +belittled +belittles +belittling +bell +bell's +belladonna +belladonna's +bellboy +bellboy's +bellboys +belle +belle's +belled +belles +bellhop +bellhop's +bellhops +bellicose +bellicosity +bellicosity's +bellied +bellies +belligerence +belligerence's +belligerency +belligerency's +belligerent +belligerent's +belligerently +belligerents +belling +bellow +bellow's +bellowed +bellowing +bellows +bells +bellwether +bellwether's +bellwethers +belly +belly's +bellyache +bellyache's +bellyached +bellyaches +bellyaching +bellybutton +bellybutton's +bellybuttons +bellyful +bellyful's +bellyfuls +bellying +belong +belonged +belonging +belonging's +belongings +belongs +beloved +beloved's +beloveds +below +belt +belt's +belted +belting +belts +beltway +beltway's +beltways +belying +bemoan +bemoaned +bemoaning +bemoans +bemuse +bemused +bemuses +bemusing +bench +bench's +benched +benches +benching +benchmark +benchmark's +benchmarks +bend +bend's +bender +bending +bends +beneath +benediction +benediction's +benedictions +benefaction +benefaction's +benefactions +benefactor +benefactor's +benefactors +benefactress +benefactress's +benefactresses +benefice +benefice's +beneficence +beneficence's +beneficent +beneficently +benefices +beneficial +beneficially +beneficiaries +beneficiary +beneficiary's +benefit +benefit's +benefited +benefiting +benefits +benefitted +benefitting +benevolence +benevolence's +benevolences +benevolent +benevolently +benighted +benign +benignly +bent +bent's +bents +benumb +benumbed +benumbing +benumbs +benzene +benzene's +bequeath +bequeathed +bequeathing +bequeaths +bequest +bequest's +bequests +berate +berated +berates +berating +bereave +bereaved +bereavement +bereavement's +bereavements +bereaves +bereaving +bereft +beret +beret's +berets +berg +berg's +bergs +beriberi +beriberi's +berm +berm's +berms +berried +berries +berry +berry's +berrying +berserk +berth +berth's +berthed +berthing +berths +beryl +beryl's +beryllium +beryllium's +beryls +beseech +beseeched +beseeches +beseeching +beset +besets +besetting +beside +besides +besiege +besieged +besieger +besieger's +besiegers +besieges +besieging +besmirch +besmirched +besmirches +besmirching +besom +besom's +besoms +besot +besots +besotted +besotting +besought +bespeak +bespeaking +bespeaks +bespoke +bespoken +best +best's +bested +bestial +bestiality +bestiality's +bestiaries +bestiary +bestiary's +besting +bestir +bestirred +bestirring +bestirs +bestow +bestowal +bestowal's +bestowals +bestowed +bestowing +bestows +bestrid +bestridden +bestride +bestrides +bestriding +bestrode +bests +bestseller +bestseller's +bestsellers +bet +bet's +beta +beta's +betake +betaken +betakes +betaking +betas +betcha +bethink +bethinking +bethinks +bethought +betide +betided +betides +betiding +betoken +betokened +betokening +betokens +betook +betray +betrayal +betrayal's +betrayals +betrayed +betrayer +betrayer's +betrayers +betraying +betrays +betroth +betrothal +betrothal's +betrothals +betrothed +betrothed's +betrothing +betroths +bets +betted +better +better's +bettered +bettering +betterment +betterment's +betters +betting +bettor +bettor's +bettors +between +betwixt +bevel +bevel's +beveled +beveling +bevelled +bevelling +bevels +beverage +beverage's +beverages +bevies +bevy +bevy's +bewail +bewailed +bewailing +bewails +beware +bewared +bewares +bewaring +bewilder +bewildered +bewildering +bewilderment +bewilderment's +bewilders +bewitch +bewitched +bewitches +bewitching +beyond +biannual +biannually +bias +bias's +biased +biases +biasing +biassed +biassing +biathlon +biathlon's +biathlons +bib +bib's +bible +bible's +bibles +biblical +bibliographer +bibliographer's +bibliographers +bibliographic +bibliographical +bibliographies +bibliography +bibliography's +bibliophile +bibliophile's +bibliophiles +bibs +bibulous +bicameral +bicentennial +bicentennial's +bicentennials +bicep +bicep's +biceps +biceps's +bicepses +bicker +bicker's +bickered +bickering +bickers +bicuspid +bicuspid's +bicuspids +bicycle +bicycle's +bicycled +bicycles +bicycling +bicyclist +bicyclist's +bicyclists +bid +bid's +bidden +bidder +bidder's +bidders +biddies +bidding +bidding's +biddy +biddy's +bide +bided +bides +bidet +bidet's +bidets +biding +bidirectional +bids +biennial +biennial's +biennially +biennials +bier +bier's +biers +bifocal +bifocals +bifocals's +bifurcate +bifurcated +bifurcates +bifurcating +bifurcation +bifurcation's +bifurcations +big +bigamist +bigamist's +bigamists +bigamous +bigamy +bigamy's +bigger +biggest +biggie +biggie's +biggies +bighearted +bighorn +bighorn's +bighorns +bight +bight's +bights +bigmouth +bigmouth's +bigmouths +bigness +bigness's +bigot +bigot's +bigoted +bigotries +bigotry +bigotry's +bigots +bigwig +bigwig's +bigwigs +bike +bike's +biked +biker +biker's +bikers +bikes +biking +bikini +bikini's +bikinis +bilateral +bilaterally +bile +bile's +bilge +bilge's +bilges +bilingual +bilingual's +bilinguals +bilious +bilk +bilked +bilking +bilks +bill +bill's +billboard +billboard's +billboards +billed +billet +billet's +billeted +billeting +billets +billfold +billfold's +billfolds +billiards +billiards's +billies +billing +billing's +billings +billion +billion's +billionaire +billionaire's +billionaires +billions +billionth +billionth's +billionths +billow +billow's +billowed +billowing +billows +billowy +bills +billy +billy's +bimbo +bimbo's +bimboes +bimbos +bimonthlies +bimonthly +bimonthly's +bin +bin's +binaries +binary +binary's +bind +bind's +binder +binder's +binderies +binders +bindery +bindery's +binding +binding's +bindings +binds +binge +binge's +binged +bingeing +binges +binging +bingo +bingo's +binnacle +binnacle's +binnacles +binned +binning +binocular +binocular's +binoculars +binomial +binomial's +binomials +bins +biochemical +biochemical's +biochemicals +biochemist +biochemist's +biochemistry +biochemistry's +biochemists +biodegradable +biodiversity +biodiversity's +biofeedback +biofeedback's +biographer +biographer's +biographers +biographical +biographies +biography +biography's +biological +biologically +biologist +biologist's +biologists +biology +biology's +biomedical +bionic +biophysicist +biophysicist's +biophysicists +biophysics +biophysics's +biopsied +biopsies +biopsy +biopsy's +biopsying +biorhythm +biorhythm's +biorhythms +biosphere +biosphere's +biospheres +biotechnology +biotechnology's +bipartisan +bipartite +biped +biped's +bipedal +bipeds +biplane +biplane's +biplanes +bipolar +biracial +birch +birch's +birched +birches +birching +bird +bird's +birdbath +birdbath's +birdbaths +birdbrained +birdcage +birdcages +birded +birdhouse +birdhouse's +birdhouses +birdie +birdie's +birdied +birdieing +birdies +birding +birds +birdseed +birdseed's +birdwatcher +birdwatcher's +birdwatchers +biretta +biretta's +birettas +birth +birth's +birthday +birthday's +birthdays +birthed +birther +birther's +birthers +birthing +birthmark +birthmark's +birthmarks +birthplace +birthplace's +birthplaces +birthrate +birthrate's +birthrates +birthright +birthright's +birthrights +births +birthstone +birthstone's +birthstones +biscuit +biscuit's +biscuits +bisect +bisected +bisecting +bisection +bisection's +bisections +bisector +bisector's +bisectors +bisects +bisexual +bisexual's +bisexuality +bisexuality's +bisexuals +bishop +bishop's +bishopric +bishopric's +bishoprics +bishops +bismuth +bismuth's +bison +bison's +bisons +bisque +bisque's +bistro +bistro's +bistros +bit +bit's +bitch +bitch's +bitched +bitches +bitchier +bitchiest +bitching +bitchy +bitcoin +bitcoin's +bitcoins +bite +bite's +bites +biting +bitingly +bitmap +bits +bitten +bitter +bitter's +bitterer +bitterest +bitterly +bittern +bittern's +bitterness +bitterness's +bitterns +bitters +bitters's +bittersweet +bittersweet's +bittersweets +bitumen +bitumen's +bituminous +bivalve +bivalve's +bivalves +bivouac +bivouac's +bivouacked +bivouacking +bivouacs +biweeklies +biweekly +biweekly's +bizarre +bizarrely +blab +blab's +blabbed +blabbermouth +blabbermouth's +blabbermouths +blabbing +blabs +black +black's +blackball +blackball's +blackballed +blackballing +blackballs +blackberries +blackberry +blackberry's +blackberrying +blackbird +blackbird's +blackbirds +blackboard +blackboard's +blackboards +blackcurrant +blacked +blacken +blackened +blackening +blackens +blacker +blackest +blackguard +blackguard's +blackguards +blackhead +blackhead's +blackheads +blacking +blackish +blackjack +blackjack's +blackjacked +blackjacking +blackjacks +blacklist +blacklist's +blacklisted +blacklisting +blacklists +blackmail +blackmail's +blackmailed +blackmailer +blackmailer's +blackmailers +blackmailing +blackmails +blackness +blackness's +blackout +blackout's +blackouts +blacks +blacksmith +blacksmith's +blacksmiths +blackthorn +blackthorn's +blackthorns +blacktop +blacktop's +blacktopped +blacktopping +blacktops +bladder +bladder's +bladders +blade +blade's +blades +blah +blah's +blame +blame's +blamed +blameless +blamelessly +blamer +blames +blameworthy +blaming +blanch +blanched +blanches +blanching +blancmange +bland +blander +blandest +blandishment +blandishment's +blandishments +blandly +blandness +blandness's +blank +blank's +blanked +blanker +blankest +blanket +blanket's +blanketed +blanketing +blankets +blanking +blankly +blankness +blankness's +blanks +blare +blare's +blared +blares +blaring +blarney +blarney's +blarneyed +blarneying +blarneys +blaspheme +blasphemed +blasphemer +blasphemer's +blasphemers +blasphemes +blasphemies +blaspheming +blasphemous +blasphemously +blasphemy +blasphemy's +blast +blast's +blasted +blaster +blaster's +blasters +blasting +blastoff +blastoff's +blastoffs +blasts +blasé +blatant +blatantly +blaze +blaze's +blazed +blazer +blazer's +blazers +blazes +blazing +blazon +blazon's +blazoned +blazoning +blazons +bleach +bleach's +bleached +bleacher +bleacher's +bleachers +bleaches +bleaching +bleak +bleaker +bleakest +bleakly +bleakness +bleakness's +blearier +bleariest +blearily +bleary +bleat +bleat's +bleated +bleating +bleats +bled +bleed +bleeder +bleeder's +bleeders +bleeding +bleeding's +bleeds +bleep +bleep's +bleeped +bleeping +bleeps +blemish +blemish's +blemished +blemishes +blemishing +blench +blenched +blenches +blenching +blend +blend's +blended +blender +blender's +blenders +blending +blends +blent +bless +blessed +blessedly +blessedness +blessedness's +blesses +blessing +blessing's +blessings +blest +blew +blight +blight's +blighted +blighting +blights +blimp +blimp's +blimps +blind +blind's +blinded +blinder +blinder's +blinders +blindest +blindfold +blindfold's +blindfolded +blindfolding +blindfolds +blinding +blindingly +blindly +blindness +blindness's +blinds +blindside +blindsided +blindsides +blindsiding +bling +blink +blink's +blinked +blinker +blinker's +blinkered +blinkering +blinkers +blinking +blinks +blintz +blintz's +blintze +blintze's +blintzes +blip +blip's +blips +bliss +bliss's +blissful +blissfully +blissfulness +blissfulness's +blister +blister's +blistered +blistering +blisters +blithe +blithely +blither +blithest +blitz +blitz's +blitzed +blitzes +blitzing +blizzard +blizzard's +blizzards +bloat +bloated +bloating +bloats +blob +blob's +blobbed +blobbing +blobs +bloc +bloc's +block +block's +blockade +blockade's +blockaded +blockades +blockading +blockage +blockage's +blockages +blockbuster +blockbuster's +blockbusters +blocked +blockhead +blockhead's +blockheads +blockhouse +blockhouse's +blockhouses +blocking +blocks +blocs +blog +blog's +blogged +blogger +blogger's +bloggers +blogging +blogs +blond +blond's +blonde +blonde's +blonder +blondes +blondest +blondness +blondness's +blonds +blood +blood's +bloodbath +bloodbath's +bloodbaths +bloodcurdling +blooded +bloodhound +bloodhound's +bloodhounds +bloodied +bloodier +bloodies +bloodiest +blooding +bloodless +bloodlessly +bloodmobile +bloodmobile's +bloodmobiles +bloods +bloodshed +bloodshed's +bloodshot +bloodstain +bloodstain's +bloodstained +bloodstains +bloodstream +bloodstream's +bloodstreams +bloodsucker +bloodsucker's +bloodsuckers +bloodthirstier +bloodthirstiest +bloodthirstiness +bloodthirstiness's +bloodthirsty +bloody +bloodying +bloom +bloom's +bloomed +bloomer +bloomer's +bloomers +blooming +blooms +blooper +blooper's +bloopers +blossom +blossom's +blossomed +blossoming +blossoms +blot +blot's +blotch +blotch's +blotched +blotches +blotchier +blotchiest +blotching +blotchy +blots +blotted +blotter +blotter's +blotters +blotting +blouse +blouse's +bloused +blouses +blousing +blow +blow's +blower +blower's +blowers +blowgun +blowgun's +blowguns +blowing +blown +blowout +blowout's +blowouts +blows +blowsier +blowsiest +blowsy +blowtorch +blowtorch's +blowtorches +blowup +blowup's +blowups +blowzier +blowziest +blowzy +blubber +blubber's +blubbered +blubbering +blubbers +bludgeon +bludgeon's +bludgeoned +bludgeoning +bludgeons +blue +blue's +bluebell +bluebell's +bluebells +blueberries +blueberry +blueberry's +bluebird +bluebird's +bluebirds +bluebottle +bluebottle's +bluebottles +blued +bluefish +bluefish's +bluefishes +bluegrass +bluegrass's +blueing +blueing's +bluejacket +bluejacket's +bluejackets +bluejay +bluejay's +bluejays +bluenose +bluenose's +bluenoses +blueprint +blueprint's +blueprinted +blueprinting +blueprints +bluer +blues +bluest +bluestocking +bluestocking's +bluestockings +bluff +bluff's +bluffed +bluffer +bluffer's +bluffers +bluffest +bluffing +bluffs +bluing +bluing's +bluish +blunder +blunder's +blunderbuss +blunderbuss's +blunderbusses +blundered +blunderer +blunderer's +blunderers +blundering +blunders +blunt +blunted +blunter +bluntest +blunting +bluntly +bluntness +bluntness's +blunts +blur +blur's +blurb +blurb's +blurbs +blurred +blurrier +blurriest +blurring +blurry +blurs +blurt +blurted +blurting +blurts +blush +blush's +blushed +blusher +blusher's +blushers +blushes +blushing +bluster +bluster's +blustered +blustering +blusters +blustery +bo's'n +bo's'n's +bo's'ns +bo'sun +bo'sun's +bo'suns +boa +boa's +boar +boar's +board +board's +boarded +boarder +boarder's +boarders +boarding +boardinghouse +boardinghouse's +boardinghouses +boardroom +boardroom's +boardrooms +boards +boardwalk +boardwalk's +boardwalks +boars +boas +boast +boast's +boasted +boaster +boaster's +boasters +boastful +boastfully +boastfulness +boastfulness's +boasting +boasts +boat +boat's +boated +boater +boater's +boaters +boating +boatman +boatman's +boatmen +boats +boatswain +boatswain's +boatswains +bob +bob's +bobbed +bobbies +bobbin +bobbin's +bobbing +bobbins +bobble +bobble's +bobbled +bobbles +bobbling +bobby +bobby's +bobcat +bobcat's +bobcats +bobolink +bobolink's +bobolinks +bobs +bobsled +bobsled's +bobsledded +bobsledding +bobsleds +bobtail +bobtail's +bobtails +bobwhite +bobwhite's +bobwhites +bode +boded +bodega +bodega's +bodegas +bodes +bodice +bodice's +bodices +bodies +bodily +boding +bodkin +bodkin's +bodkins +body +body's +bodybuilding +bodybuilding's +bodyguard +bodyguard's +bodyguards +bodywork +bodywork's +bog +bog's +bogey +bogey's +bogeyed +bogeying +bogeyman +bogeyman's +bogeymen +bogeys +bogged +boggier +boggiest +bogging +boggle +boggled +boggles +boggling +boggy +bogie +bogie's +bogied +bogies +bogs +bogus +bogy +bogy's +bohemian +bohemian's +bohemians +boil +boil's +boiled +boiler +boiler's +boilerplate +boilerplate's +boilers +boiling +boilings +boils +boisterous +boisterously +boisterousness +boisterousness's +bola +bola's +bolas +bold +bolder +boldest +boldface +boldface's +boldly +boldness +boldness's +bole +bole's +bolero +bolero's +boleros +boles +boll +boll's +bolls +bologna +bologna's +boloney +boloney's +bolster +bolster's +bolstered +bolstering +bolsters +bolt +bolt's +bolted +bolting +bolts +bomb +bomb's +bombard +bombarded +bombardier +bombardier's +bombardiers +bombarding +bombardment +bombardment's +bombardments +bombards +bombast +bombast's +bombastic +bombed +bomber +bomber's +bombers +bombing +bombings +bombs +bombshell +bombshell's +bombshells +bonanza +bonanza's +bonanzas +bonbon +bonbon's +bonbons +bond +bond's +bondage +bondage's +bonded +bonding +bonding's +bonds +bondsman +bondsman's +bondsmen +bone +bone's +boned +bonehead +bonehead's +boneheads +boneless +boner +boner's +boners +bones +boney +boneyer +boneyest +bonfire +bonfire's +bonfires +bong +bong's +bonged +bonging +bongo +bongo's +bongoes +bongos +bongs +bonier +boniest +boning +bonito +bonito's +bonitoes +bonitos +bonkers +bonnet +bonnet's +bonnets +bonnie +bonnier +bonniest +bonny +bonsai +bonsai's +bonus +bonus's +bonuses +bony +boo +boo's +boob +boob's +boobed +boobies +boobing +boobs +booby +booby's +boodle +boodle's +boodles +booed +boogie +boogie's +boogied +boogieing +boogies +booing +book +book's +bookcase +bookcase's +bookcases +booked +bookend +bookend's +bookends +bookie +bookie's +bookies +booking +booking's +bookings +bookish +bookkeeper +bookkeeper's +bookkeepers +bookkeeping +bookkeeping's +booklet +booklet's +booklets +bookmaker +bookmaker's +bookmakers +bookmaking +bookmaking's +bookmark +bookmark's +bookmarked +bookmarking +bookmarks +bookmobile +bookmobile's +bookmobiles +books +bookseller +bookseller's +booksellers +bookshelf +bookshelf's +bookshelves +bookshop +bookshop's +bookshops +bookstore +bookstore's +bookstores +bookworm +bookworm's +bookworms +boom +boom's +boomed +boomerang +boomerang's +boomeranged +boomeranging +boomerangs +booming +booms +boon +boon's +boondocks +boondocks's +boondoggle +boondoggle's +boondoggled +boondoggles +boondoggling +boons +boor +boor's +boorish +boorishly +boors +boos +boost +boost's +boosted +booster +booster's +boosters +boosting +boosts +boot +boot's +bootblack +bootblack's +bootblacks +booted +bootee +bootee's +bootees +booth +booth's +booths +bootie +bootie's +booties +booting +bootleg +bootleg's +bootlegged +bootlegger +bootlegger's +bootleggers +bootlegging +bootlegs +bootless +boots +bootstrap +bootstrap's +bootstraps +booty +booty's +booze +booze's +boozed +boozer +boozer's +boozers +boozes +boozier +booziest +boozing +boozy +bop +bop's +bopped +bopping +bops +borax +borax's +bordello +bordello's +bordellos +border +border's +bordered +bordering +borderland +borderland's +borderlands +borderline +borderline's +borderlines +borders +bore +bore's +bored +boredom +boredom's +borer +borer's +borers +bores +boring +boringly +born +borne +boron +boron's +borough +borough's +boroughs +borrow +borrowed +borrower +borrower's +borrowers +borrowing +borrows +borsch +borsch's +borscht +borscht's +bos'n +bos'n's +bos'ns +bosh +bosh's +bosom +bosom's +bosoms +boss +boss's +bossed +bosses +bossier +bossiest +bossily +bossiness +bossiness's +bossing +bossy +bosun +bosun's +bosuns +botanical +botanist +botanist's +botanists +botany +botany's +botch +botch's +botched +botches +botching +both +bother +bother's +bothered +bothering +bothers +bothersome +botnet +botnet's +botnets +bottle +bottle's +bottled +bottleneck +bottleneck's +bottlenecks +bottles +bottling +bottom +bottom's +bottomed +bottoming +bottomless +bottoms +botulism +botulism's +boudoir +boudoir's +boudoirs +bouffant +bouffant's +bouffants +bough +bough's +boughs +bought +bouillabaisse +bouillabaisse's +bouillabaisses +bouillon +bouillon's +bouillons +boulder +boulder's +boulders +boulevard +boulevard's +boulevards +bounce +bounce's +bounced +bouncer +bouncer's +bouncers +bounces +bouncier +bounciest +bouncing +bouncy +bound +bound's +boundaries +boundary +boundary's +bounded +bounden +bounder +bounder's +bounders +bounding +boundless +bounds +bounteous +bounties +bountiful +bountifully +bounty +bounty's +bouquet +bouquet's +bouquets +bourbon +bourbon's +bourgeois +bourgeois's +bourgeoisie +bourgeoisie's +bout +bout's +boutique +boutique's +boutiques +boutonnière +boutonnière's +boutonnières +bouts +bovine +bovine's +bovines +bow +bow's +bowdlerize +bowdlerized +bowdlerizes +bowdlerizing +bowed +bowel +bowel's +bowels +bower +bower's +bowers +bowing +bowl +bowl's +bowlder +bowlder's +bowlders +bowled +bowlegged +bowler +bowler's +bowlers +bowling +bowling's +bowls +bowman +bowman's +bowmen +bows +bowsprit +bowsprit's +bowsprits +bowstring +bowstring's +bowstrings +box +box's +boxcar +boxcar's +boxcars +boxed +boxer +boxer's +boxers +boxes +boxing +boxing's +boxwood +boxwood's +boy +boy's +boycott +boycott's +boycotted +boycotting +boycotts +boyfriend +boyfriend's +boyfriends +boyhood +boyhood's +boyhoods +boyish +boyishly +boyishness +boyishness's +boys +boysenberries +boysenberry +boysenberry's +bozo +bozo's +bozos +bra +bra's +brace +brace's +braced +bracelet +bracelet's +bracelets +braces +bracing +bracken +bracken's +bracket +bracket's +bracketed +bracketing +brackets +brackish +bract +bract's +bracts +brad +brad's +brads +brag +brag's +braggart +braggart's +braggarts +bragged +bragger +bragger's +braggers +bragging +brags +braid +braid's +braided +braiding +braids +braille +braille's +brain +brain's +brainchild +brainchild's +brainchildren +brainchildren's +brained +brainier +brainiest +braining +brainless +brains +brainstorm +brainstorm's +brainstormed +brainstorming +brainstorming's +brainstorms +brainteaser +brainteaser's +brainteasers +brainwash +brainwashed +brainwashes +brainwashing +brainwashing's +brainy +braise +braised +braises +braising +brake +brake's +braked +brakeman +brakeman's +brakemen +brakes +braking +bramble +bramble's +brambles +bran +bran's +branch +branch's +branched +branches +branching +brand +brand's +branded +brandied +brandies +branding +brandish +brandished +brandishes +brandishing +brands +brandy +brandy's +brandying +bras +brash +brasher +brashest +brashly +brashness +brashness's +brass +brass's +brasses +brassier +brassiere +brassiere's +brassieres +brassiest +brassy +brat +brat's +brats +brattier +brattiest +bratty +bravado +bravado's +brave +brave's +braved +bravely +braver +bravery +bravery's +braves +bravest +braving +bravo +bravo's +bravos +bravura +bravura's +bravuras +brawl +brawl's +brawled +brawler +brawler's +brawlers +brawling +brawls +brawn +brawn's +brawnier +brawniest +brawniness +brawniness's +brawny +bray +bray's +brayed +braying +brays +brazen +brazened +brazening +brazenly +brazenness +brazenness's +brazens +brazier +brazier's +braziers +breach +breach's +breached +breaches +breaching +bread +bread's +breadbasket +breadbasket's +breadbaskets +breaded +breadfruit +breadfruit's +breadfruits +breading +breads +breadth +breadth's +breadths +breadwinner +breadwinner's +breadwinners +break +break's +breakable +breakable's +breakables +breakage +breakage's +breakages +breakdown +breakdown's +breakdowns +breaker +breaker's +breakers +breakfast +breakfast's +breakfasted +breakfasting +breakfasts +breaking +breakneck +breakpoints +breaks +breakthrough +breakthrough's +breakthroughs +breakup +breakup's +breakups +breakwater +breakwater's +breakwaters +breast +breast's +breastbone +breastbone's +breastbones +breasted +breasting +breastplate +breastplate's +breastplates +breasts +breaststroke +breaststroke's +breaststrokes +breastwork +breastwork's +breastworks +breath +breath's +breathable +breathe +breathed +breather +breather's +breathers +breathes +breathier +breathiest +breathing +breathing's +breathless +breathlessly +breathlessness +breathlessness's +breaths +breathtaking +breathtakingly +breathy +bred +breech +breech's +breeches +breed +breed's +breeder +breeder's +breeders +breeding +breeding's +breeds +breeze +breeze's +breezed +breezes +breezier +breeziest +breezily +breeziness +breeziness's +breezing +breezy +brethren +breviaries +breviary +breviary's +brevity +brevity's +brew +brew's +brewed +brewer +brewer's +breweries +brewers +brewery +brewery's +brewing +brews +briar +briar's +briars +bribe +bribe's +bribed +bribery +bribery's +bribes +bribing +brick +brick's +brickbat +brickbat's +brickbats +bricked +bricking +bricklayer +bricklayer's +bricklayers +bricklaying +bricklaying's +bricks +bridal +bridal's +bridals +bride +bride's +bridegroom +bridegroom's +bridegrooms +brides +bridesmaid +bridesmaid's +bridesmaids +bridge +bridge's +bridged +bridgehead +bridgehead's +bridgeheads +bridges +bridgework +bridgework's +bridging +bridle +bridle's +bridled +bridles +bridling +brief +brief's +briefcase +briefcase's +briefcases +briefed +briefer +briefest +briefing +briefing's +briefings +briefly +briefness +briefness's +briefs +brier +brier's +briers +brig +brig's +brigade +brigade's +brigades +brigand +brigand's +brigandage +brigandage's +brigands +brigantine +brigantine's +brigantines +bright +brighten +brightened +brightening +brightens +brighter +brightest +brightly +brightness +brightness's +brigs +brilliance +brilliance's +brilliancy +brilliancy's +brilliant +brilliant's +brilliantly +brilliants +brim +brim's +brimful +brimfull +brimmed +brimming +brims +brimstone +brimstone's +brindled +brine +brine's +bring +bringing +brings +brinier +briniest +brink +brink's +brinkmanship +brinkmanship's +brinks +brinksmanship +brinksmanship's +briny +briquet +briquet's +briquets +briquette +briquette's +briquettes +brisk +brisked +brisker +briskest +brisket +brisket's +briskets +brisking +briskly +briskness +briskness's +brisks +bristle +bristle's +bristled +bristles +bristlier +bristliest +bristling +bristly +britches +britches's +brittle +brittle's +brittleness +brittleness's +brittler +brittlest +broach +broach's +broached +broaches +broaching +broad +broad's +broadband +broadband's +broadcast +broadcast's +broadcasted +broadcaster +broadcaster's +broadcasters +broadcasting +broadcasts +broadcloth +broadcloth's +broaden +broadened +broadening +broadens +broader +broadest +broadloom +broadloom's +broadly +broadness +broadness's +broads +broadside +broadside's +broadsided +broadsides +broadsiding +broadsword +broadsword's +broadswords +brocade +brocade's +brocaded +brocades +brocading +broccoli +broccoli's +brochure +brochure's +brochures +brogan +brogan's +brogans +brogue +brogue's +brogues +broil +broil's +broiled +broiler +broiler's +broilers +broiling +broils +broke +broken +brokenhearted +broker +broker's +brokerage +brokerage's +brokerages +brokered +brokering +brokers +bromide +bromide's +bromides +bromine +bromine's +bronchi +bronchial +bronchitis +bronchitis's +broncho +broncho's +bronchos +bronchus +bronchus's +bronco +bronco's +broncos +brontosaur +brontosaur's +brontosauri +brontosaurs +brontosaurus +brontosaurus's +brontosauruses +bronze +bronze's +bronzed +bronzes +bronzing +brooch +brooch's +brooches +brood +brood's +brooded +brooder +brooder's +brooders +brooding +broods +brook +brook's +brooked +brooking +brooks +broom +broom's +brooms +broomstick +broomstick's +broomsticks +broth +broth's +brothel +brothel's +brothels +brother +brother's +brotherhood +brotherhood's +brotherhoods +brotherliness +brotherliness's +brotherly +brothers +broths +brought +brouhaha +brouhaha's +brouhahas +brow +brow's +browbeat +browbeaten +browbeating +browbeats +brown +brown's +browned +browner +brownest +brownie +brownie's +brownies +browning +brownish +brownout +brownout's +brownouts +browns +brownstone +brownstone's +brownstones +brows +browse +browse's +browsed +browser +browser's +browsers +browses +browsing +brr +bruin +bruin's +bruins +bruise +bruise's +bruised +bruiser +bruiser's +bruisers +bruises +bruising +brunch +brunch's +brunched +brunches +brunching +brunet +brunet's +brunets +brunette +brunette's +brunettes +brunt +brunt's +brush +brush's +brushed +brushes +brushing +brushwood +brushwood's +brusk +brusker +bruskest +bruskly +bruskness +bruskness's +brusque +brusquely +brusqueness +brusqueness's +brusquer +brusquest +brutal +brutalities +brutality +brutality's +brutalize +brutalized +brutalizes +brutalizing +brutally +brute +brute's +brutes +brutish +brutishly +bubble +bubble's +bubbled +bubbles +bubblier +bubbliest +bubbling +bubbly +bubbly's +buccaneer +buccaneer's +buccaneered +buccaneering +buccaneers +buck +buck's +buckboard +buckboard's +buckboards +bucked +bucket +bucket's +bucketed +bucketful +bucketful's +bucketfuls +bucketing +buckets +buckeye +buckeye's +buckeyes +bucking +buckle +buckle's +buckled +buckler +buckler's +bucklers +buckles +buckling +buckram +buckram's +bucks +bucksaw +bucksaw's +bucksaws +buckshot +buckshot's +buckskin +buckskin's +buckskins +buckteeth +bucktooth +bucktooth's +bucktoothed +buckwheat +buckwheat's +buckyball +buckyball's +buckyballs +bucolic +bucolic's +bucolics +bud +bud's +budded +buddies +budding +buddings +buddy +buddy's +budge +budged +budgerigar +budgerigar's +budgerigars +budges +budget +budget's +budgetary +budgeted +budgeting +budgets +budgie +budgie's +budgies +budging +buds +buff +buff's +buffalo +buffalo's +buffaloed +buffaloes +buffaloing +buffalos +buffed +buffer +buffer's +buffered +buffering +buffers +buffet +buffet's +buffeted +buffeting +buffets +buffing +buffoon +buffoon's +buffoonery +buffoonery's +buffoons +buffs +bug +bug's +bugaboo +bugaboo's +bugaboos +bugbear +bugbear's +bugbears +bugged +bugger +bugger's +buggers +buggier +buggies +buggiest +bugging +buggy +buggy's +bugle +bugle's +bugled +bugler +bugler's +buglers +bugles +bugling +bugs +build +build's +builder +builder's +builders +building +building's +buildings +builds +buildup +buildup's +buildups +built +builtin +bulb +bulb's +bulbous +bulbs +bulge +bulge's +bulged +bulges +bulgier +bulgiest +bulging +bulgy +bulimia +bulimia's +bulimic +bulimic's +bulimics +bulk +bulk's +bulked +bulkhead +bulkhead's +bulkheads +bulkier +bulkiest +bulkiness +bulkiness's +bulking +bulks +bulky +bull +bull's +bulldog +bulldog's +bulldogged +bulldogging +bulldogs +bulldoze +bulldozed +bulldozer +bulldozer's +bulldozers +bulldozes +bulldozing +bulled +bullet +bullet's +bulletin +bulletin's +bulletined +bulletining +bulletins +bulletproof +bulletproofed +bulletproofing +bulletproofs +bullets +bullfight +bullfight's +bullfighter +bullfighter's +bullfighters +bullfighting +bullfighting's +bullfights +bullfinch +bullfinch's +bullfinches +bullfrog +bullfrog's +bullfrogs +bullheaded +bullhorn +bullhorn's +bullhorns +bullied +bullies +bulling +bullion +bullion's +bullish +bullock +bullock's +bullocks +bullpen +bullpen's +bullpens +bullring +bullring's +bullrings +bulls +bullshit +bullshit's +bullshits +bullshitted +bullshitting +bully +bully's +bullying +bulrush +bulrush's +bulrushes +bulwark +bulwark's +bulwarks +bum +bum's +bumble +bumblebee +bumblebee's +bumblebees +bumbled +bumbler +bumbler's +bumblers +bumbles +bumbling +bummed +bummer +bummer's +bummers +bummest +bumming +bump +bump's +bumped +bumper +bumper's +bumpers +bumpier +bumpiest +bumping +bumpkin +bumpkin's +bumpkins +bumps +bumptious +bumpy +bums +bun +bun's +bunch +bunch's +bunched +bunches +bunching +buncombe +buncombe's +bundle +bundle's +bundled +bundles +bundling +bung +bung's +bungalow +bungalow's +bungalows +bunged +bunghole +bunghole's +bungholes +bunging +bungle +bungle's +bungled +bungler +bungler's +bunglers +bungles +bungling +bungs +bunion +bunion's +bunions +bunk +bunk's +bunked +bunker +bunker's +bunkers +bunkhouse +bunkhouse's +bunkhouses +bunking +bunks +bunkum +bunkum's +bunnies +bunny +bunny's +buns +bunt +bunt's +bunted +bunting +bunting's +buntings +bunts +buoy +buoy's +buoyancy +buoyancy's +buoyant +buoyantly +buoyed +buoying +buoys +bur +bur's +burble +burbled +burbles +burbling +burden +burden's +burdened +burdening +burdens +burdensome +burdock +burdock's +bureau +bureau's +bureaucracies +bureaucracy +bureaucracy's +bureaucrat +bureaucrat's +bureaucratic +bureaucratically +bureaucrats +bureaus +bureaux +burg +burg's +burgeon +burgeoned +burgeoning +burgeons +burger +burger's +burgers +burgher +burgher's +burghers +burglar +burglar's +burglaries +burglarize +burglarized +burglarizes +burglarizing +burglars +burglary +burglary's +burgle +burgled +burgles +burgling +burgs +burial +burial's +burials +buried +buries +burka +burka's +burkas +burlap +burlap's +burlesque +burlesque's +burlesqued +burlesques +burlesquing +burlier +burliest +burliness +burliness's +burly +burn +burn's +burned +burner +burner's +burners +burning +burnish +burnish's +burnished +burnishes +burnishing +burnoose +burnoose's +burnooses +burnous +burnous's +burnouses +burnout +burnout's +burnouts +burns +burnt +burp +burp's +burped +burping +burps +burr +burr's +burred +burring +burrito +burrito's +burritos +burro +burro's +burros +burrow +burrow's +burrowed +burrowing +burrows +burrs +burs +bursar +bursar's +bursars +bursitis +bursitis's +burst +burst's +bursted +bursting +bursts +bury +burying +bus +bus's +busbies +busboy +busboy's +busboys +busby +busby's +bused +buses +bush +bush's +bushed +bushel +bushel's +busheled +busheling +bushelled +bushelling +bushels +bushes +bushier +bushiest +bushiness +bushiness's +bushing +bushing's +bushings +bushman +bushman's +bushmen +bushwhack +bushwhacked +bushwhacker +bushwhacker's +bushwhackers +bushwhacking +bushwhacks +bushy +busied +busier +busies +busiest +busily +business +business's +businesses +businesslike +businessman +businessman's +businessmen +businesswoman +businesswoman's +businesswomen +busing +busing's +buss +buss's +bussed +busses +bussing +bussing's +bust +bust's +busted +buster +buster's +busters +busting +bustle +bustle's +bustled +bustles +bustling +busts +busy +busybodies +busybody +busybody's +busying +busyness +busyness's +busywork +busywork's +but +butane +butane's +butch +butch's +butcher +butcher's +butchered +butcheries +butchering +butchers +butchery +butchery's +butches +butler +butler's +butlers +buts +butt +butt's +butte +butte's +butted +butter +butter's +buttercup +buttercup's +buttercups +buttered +butterfat +butterfat's +butterfingers +butterfingers's +butterflied +butterflies +butterfly +butterfly's +butterflying +butterier +butteries +butteriest +buttering +buttermilk +buttermilk's +butternut +butternut's +butternuts +butters +butterscotch +butterscotch's +buttery +buttery's +buttes +butting +buttock +buttock's +buttocks +button +button's +buttoned +buttonhole +buttonhole's +buttonholed +buttonholes +buttonholing +buttoning +buttons +buttress +buttress's +buttressed +buttresses +buttressing +butts +buxom +buy +buy's +buyer +buyer's +buyers +buying +buyout +buyout's +buyouts +buys +buzz +buzz's +buzzard +buzzard's +buzzards +buzzed +buzzer +buzzer's +buzzers +buzzes +buzzing +buzzkill +buzzkill's +buzzkills +buzzword +buzzword's +buzzwords +by +by's +bye +bye's +byelaw +byelaw's +byelaws +byes +bygone +bygone's +bygones +bylaw +bylaw's +bylaws +byline +byline's +bylines +bypass +bypass's +bypassed +bypasses +bypassing +bypast +byplay +byplay's +byproduct +byproduct's +byproducts +bystander +bystander's +bystanders +byte +byte's +bytes +byway +byway's +byways +byword +byword's +bywords +c +cab +cab's +cabal +cabal's +cabals +cabana +cabana's +cabanas +cabaret +cabaret's +cabarets +cabbage +cabbage's +cabbages +cabbed +cabbie +cabbie's +cabbies +cabbing +cabby +cabby's +cabin +cabin's +cabinet +cabinet's +cabinetmaker +cabinetmaker's +cabinetmakers +cabinets +cabins +cable +cable's +cablecast +cablecast's +cablecasted +cablecasting +cablecasts +cabled +cablegram +cablegram's +cablegrams +cables +cabling +caboodle +caboodle's +caboose +caboose's +cabooses +cabs +cacao +cacao's +cacaos +cache +cache's +cached +caches +cachet +cachet's +cachets +caching +cackle +cackle's +cackled +cackles +cackling +cacophonies +cacophonous +cacophony +cacophony's +cacti +cactus +cactus's +cactuses +cad +cad's +cadaver +cadaver's +cadaverous +cadavers +caddie +caddie's +caddied +caddies +caddish +caddy +caddy's +caddying +cadence +cadence's +cadences +cadenza +cadenza's +cadenzas +cadet +cadet's +cadets +cadge +cadged +cadger +cadger's +cadgers +cadges +cadging +cadmium +cadmium's +cadre +cadre's +cadres +cads +caducei +caduceus +caduceus's +caesarean +caesarean's +caesareans +caesarian +caesarian's +caesarians +caesura +caesura's +caesurae +caesuras +cafeteria +cafeteria's +cafeterias +caffeinated +caffeine +caffeine's +caftan +caftan's +caftans +café +café's +cafés +cage +cage's +caged +cages +cagey +cageyness +cageyness's +cagier +cagiest +cagily +caginess +caginess's +caging +cagy +cahoot +cahoot's +cahoots +cairn +cairn's +cairns +caisson +caisson's +caissons +cajole +cajoled +cajolery +cajolery's +cajoles +cajoling +cake +cake's +caked +cakes +caking +calabash +calabash's +calabashes +calamine +calamine's +calamities +calamitous +calamity +calamity's +calcified +calcifies +calcify +calcifying +calcine +calcined +calcines +calcining +calcite +calcite's +calcium +calcium's +calculable +calculate +calculated +calculates +calculating +calculation +calculation's +calculations +calculator +calculator's +calculators +calculi +calculus +calculus's +calculuses +caldron +caldron's +caldrons +calendar +calendar's +calendared +calendaring +calendars +calf +calf's +calfs +calfskin +calfskin's +caliber +caliber's +calibers +calibrate +calibrated +calibrates +calibrating +calibration +calibration's +calibrations +calibrator +calibrator's +calibrators +calico +calico's +calicoes +calicos +calif +calif's +califs +caliper +caliper's +calipered +calipering +calipers +caliph +caliph's +caliphate +caliphate's +caliphates +caliphs +calisthenic +calisthenics +calisthenics's +calk +calk's +calked +calking +calking's +calkings +calks +call +call's +callable +called +caller +caller's +callers +calligrapher +calligrapher's +calligraphers +calligraphy +calligraphy's +calling +calling's +callings +calliope +calliope's +calliopes +calliper +calliper's +callipered +callipering +callipers +callisthenics +callous +calloused +callouses +callousing +callously +callousness +callousness's +callow +callower +callowest +calls +callus +callus's +callused +calluses +callusing +calm +calm's +calmed +calmer +calmest +calming +calmly +calmness +calmness's +calms +caloric +calorie +calorie's +calories +calorific +calumniate +calumniated +calumniates +calumniating +calumnies +calumny +calumny's +calve +calved +calves +calving +calyces +calypso +calypso's +calypsos +calyx +calyx's +calyxes +cam +cam's +camaraderie +camaraderie's +camber +camber's +cambered +cambering +cambers +cambia +cambium +cambium's +cambiums +cambric +cambric's +camcorder +camcorder's +camcorders +came +camel +camel's +camellia +camellia's +camellias +camels +cameo +cameo's +cameos +camera +camera's +cameraman +cameraman's +cameramen +cameras +camerawoman +camerawoman's +camerawomen +camisole +camisole's +camisoles +camomile +camomile's +camomiles +camouflage +camouflage's +camouflaged +camouflages +camouflaging +camp +camp's +campaign +campaign's +campaigned +campaigner +campaigner's +campaigners +campaigning +campaigns +campanile +campanile's +campaniles +campanili +camped +camper +camper's +campers +campfire +campfire's +campfires +campground +campground's +campgrounds +camphor +camphor's +campier +campiest +camping +camping's +camps +campsite +campsite's +campsites +campus +campus's +campuses +campy +cams +camshaft +camshaft's +camshafts +can +can's +can't +canal +canal's +canals +canapé +canapé's +canapés +canard +canard's +canards +canaries +canary +canary's +canasta +canasta's +cancan +cancan's +cancans +cancel +cancelation +canceled +canceling +cancellation +cancellation's +cancellations +cancelled +cancelling +cancels +cancer +cancer's +cancerous +cancers +candelabra +candelabra's +candelabras +candelabrum +candelabrum's +candelabrums +candid +candidacies +candidacy +candidacy's +candidate +candidate's +candidates +candidly +candidness +candidness's +candied +candies +candle +candle's +candled +candlelight +candlelight's +candles +candlestick +candlestick's +candlesticks +candling +candor +candor's +candy +candy's +candying +cane +cane's +caned +canes +canine +canine's +canines +caning +canister +canister's +canisters +canker +canker's +cankered +cankering +cankerous +cankers +cannabis +cannabis's +cannabises +canned +canneries +cannery +cannery's +cannibal +cannibal's +cannibalism +cannibalism's +cannibalistic +cannibalize +cannibalized +cannibalizes +cannibalizing +cannibals +cannier +canniest +cannily +canniness +canniness's +canning +cannon +cannon's +cannonade +cannonade's +cannonaded +cannonades +cannonading +cannonball +cannonball's +cannonballs +cannoned +cannoning +cannons +cannot +canny +canoe +canoe's +canoed +canoeing +canoeist +canoeist's +canoeists +canoes +canon +canon's +canonical +canonization +canonization's +canonizations +canonize +canonized +canonizes +canonizing +canons +canopied +canopies +canopy +canopy's +canopying +cans +cant +cant's +cantaloup +cantaloup's +cantaloupe +cantaloupe's +cantaloupes +cantaloups +cantankerous +cantankerously +cantankerousness +cantankerousness's +cantata +cantata's +cantatas +canted +canteen +canteen's +canteens +canter +canter's +cantered +cantering +canters +canticle +canticle's +canticles +cantilever +cantilever's +cantilevered +cantilevering +cantilevers +canting +canto +canto's +canton +canton's +cantons +cantor +cantor's +cantors +cantos +cants +canvas +canvas's +canvasback +canvasback's +canvasbacks +canvased +canvases +canvasing +canvass +canvass's +canvassed +canvasser +canvasser's +canvassers +canvasses +canvassing +canyon +canyon's +canyons +cap +cap's +capabilities +capability +capability's +capable +capably +capacious +capaciously +capaciousness +capaciousness's +capacitance +capacities +capacitor +capacitor's +capacitors +capacity +capacity's +caparison +caparison's +caparisoned +caparisoning +caparisons +cape +cape's +caped +caper +caper's +capered +capering +capers +capes +capillaries +capillary +capillary's +capital +capital's +capitalism +capitalism's +capitalist +capitalist's +capitalistic +capitalists +capitalization +capitalization's +capitalize +capitalized +capitalizes +capitalizing +capitals +capitol +capitol's +capitols +capitulate +capitulated +capitulates +capitulating +capitulation +capitulation's +capitulations +caplet +caplet's +caplets +capon +capon's +capons +capped +capping +cappuccino +cappuccino's +cappuccinos +caprice +caprice's +caprices +capricious +capriciously +capriciousness +capriciousness's +caps +capsize +capsized +capsizes +capsizing +capstan +capstan's +capstans +capsule +capsule's +capsuled +capsules +capsuling +captain +captain's +captaincies +captaincy +captaincy's +captained +captaining +captains +caption +caption's +captioned +captioning +captions +captious +captivate +captivated +captivates +captivating +captivation +captivation's +captive +captive's +captives +captivities +captivity +captivity's +captor +captor's +captors +capture +capture's +captured +captures +capturing +car +car's +caracul +caracul's +carafe +carafe's +carafes +caramel +caramel's +caramels +carapace +carapace's +carapaces +carat +carat's +carats +caravan +caravan's +caravans +caraway +caraway's +caraways +carbide +carbide's +carbides +carbine +carbine's +carbines +carbohydrate +carbohydrate's +carbohydrates +carbon +carbon's +carbonate +carbonate's +carbonated +carbonates +carbonating +carbonation +carbonation's +carbons +carboy +carboy's +carboys +carbs +carbuncle +carbuncle's +carbuncles +carburetor +carburetor's +carburetors +carcass +carcass's +carcasses +carcinogen +carcinogen's +carcinogenic +carcinogenic's +carcinogenics +carcinogens +carcinoma +carcinoma's +carcinomas +carcinomata +card +card's +cardboard +cardboard's +carded +cardiac +cardigan +cardigan's +cardigans +cardinal +cardinal's +cardinals +carding +cardio +cardiogram +cardiogram's +cardiograms +cardiologist +cardiologist's +cardiologists +cardiology +cardiology's +cardiopulmonary +cardiovascular +cards +cardsharp +cardsharp's +cardsharps +care +care's +cared +careen +careened +careening +careens +career +career's +careered +careering +careers +carefree +careful +carefuller +carefullest +carefully +carefulness +carefulness's +caregiver +caregiver's +caregivers +careless +carelessly +carelessness +carelessness's +cares +caress +caress's +caressed +caresses +caressing +caret +caret's +caretaker +caretaker's +caretakers +carets +careworn +carfare +carfare's +cargo +cargo's +cargoes +cargos +caribou +caribou's +caribous +caricature +caricature's +caricatured +caricatures +caricaturing +caricaturist +caricaturist's +caricaturists +caries +caries's +carillon +carillon's +carillons +caring +caring's +carjack +carjacked +carjacker +carjacker's +carjackers +carjacking +carjacking's +carjackings +carjacks +carmine +carmine's +carmines +carnage +carnage's +carnal +carnally +carnation +carnation's +carnations +carnelian +carnelian's +carnelians +carnival +carnival's +carnivals +carnivore +carnivore's +carnivores +carnivorous +carol +carol's +caroled +caroler +caroler's +carolers +caroling +carolled +caroller +caroller's +carollers +carolling +carols +carom +carom's +caromed +caroming +caroms +carotid +carotid's +carotids +carousal +carousal's +carousals +carouse +carouse's +caroused +carousel +carousel's +carousels +carouser +carouser's +carousers +carouses +carousing +carp +carp's +carpal +carpal's +carpals +carped +carpel +carpel's +carpels +carpenter +carpenter's +carpentered +carpentering +carpenters +carpentry +carpentry's +carpet +carpet's +carpetbag +carpetbag's +carpetbagged +carpetbagger +carpetbagger's +carpetbaggers +carpetbagging +carpetbags +carpeted +carpeting +carpeting's +carpets +carpi +carping +carport +carport's +carports +carps +carpus +carpus's +carrel +carrel's +carrels +carriage +carriage's +carriages +carriageway +carried +carrier +carrier's +carriers +carries +carrion +carrion's +carrot +carrot's +carrots +carrousel +carrousel's +carrousels +carry +carry's +carryall +carryall's +carryalls +carrying +carryout +cars +carsick +carsickness +carsickness's +cart +cart's +carted +cartel +cartel's +cartels +cartilage +cartilage's +cartilages +cartilaginous +carting +cartographer +cartographer's +cartographers +cartography +cartography's +carton +carton's +cartons +cartoon +cartoon's +cartooned +cartooning +cartoonist +cartoonist's +cartoonists +cartoons +cartridge +cartridge's +cartridges +carts +cartwheel +cartwheel's +cartwheeled +cartwheeling +cartwheels +carve +carved +carver +carver's +carvers +carves +carving +carving's +carvings +caryatid +caryatid's +caryatides +caryatids +cascade +cascade's +cascaded +cascades +cascading +case +case's +cased +casein +casein's +caseload +caseload's +caseloads +casement +casement's +casements +cases +casework +casework's +caseworker +caseworker's +caseworkers +cash +cash's +cashback +cashback's +cashed +cashes +cashew +cashew's +cashews +cashier +cashier's +cashiered +cashiering +cashiers +cashing +cashmere +cashmere's +casing +casing's +casings +casino +casino's +casinos +cask +cask's +casket +casket's +caskets +casks +cassava +cassava's +cassavas +casserole +casserole's +casseroled +casseroles +casseroling +cassette +cassette's +cassettes +cassia +cassia's +cassias +cassino +cassino's +cassinos +cassock +cassock's +cassocks +cast +cast's +castanet +castanet's +castanets +castaway +castaway's +castaways +caste +caste's +caster +caster's +casters +castes +castigate +castigated +castigates +castigating +castigation +castigation's +castigator +castigator's +castigators +casting +casting's +castings +castle +castle's +castled +castles +castling +castoff +castoff's +castoffs +castor +castor's +castors +castrate +castrated +castrates +castrating +castration +castration's +castrations +casts +casual +casual's +casually +casualness +casualness's +casuals +casualties +casualty +casualty's +casuist +casuist's +casuistry +casuistry's +casuists +cat +cat's +cataclysm +cataclysm's +cataclysmic +cataclysms +catacomb +catacomb's +catacombs +catafalque +catafalque's +catafalques +catalepsy +catalepsy's +cataleptic +cataleptic's +cataleptics +catalog +catalog's +cataloged +cataloger +cataloger's +catalogers +cataloging +catalogs +catalogue +catalogue's +catalogued +cataloguer +cataloguer's +cataloguers +catalogues +cataloguing +catalpa +catalpa's +catalpas +catalysis +catalysis's +catalyst +catalyst's +catalysts +catalytic +catalytic's +catalyze +catalyzed +catalyzes +catalyzing +catamaran +catamaran's +catamarans +catapult +catapult's +catapulted +catapulting +catapults +cataract +cataract's +cataracts +catarrh +catarrh's +catastrophe +catastrophe's +catastrophes +catastrophic +catastrophically +catatonic +catatonic's +catatonics +catbird +catbird's +catbirds +catboat +catboat's +catboats +catcall +catcall's +catcalled +catcalling +catcalls +catch +catch's +catchall +catchall's +catchalls +catcher +catcher's +catchers +catches +catchier +catchiest +catching +catchings +catchment +catchphrase +catchup +catchup's +catchword +catchword's +catchwords +catchy +catechise +catechised +catechises +catechising +catechism +catechism's +catechisms +catechize +catechized +catechizes +catechizing +categorical +categorically +categories +categorization +categorization's +categorizations +categorize +categorized +categorizes +categorizing +category +category's +cater +catered +caterer +caterer's +caterers +catering +caterings +caterpillar +caterpillar's +caterpillars +caters +caterwaul +caterwaul's +caterwauled +caterwauling +caterwauls +catfish +catfish's +catfishes +catgut +catgut's +catharses +catharsis +catharsis's +cathartic +cathartic's +cathartics +cathedral +cathedral's +cathedrals +catheter +catheter's +catheters +cathode +cathode's +cathodes +catholic +catholicity +catholicity's +cation +cation's +cations +catkin +catkin's +catkins +catnap +catnap's +catnapped +catnapping +catnaps +catnip +catnip's +cats +catsup +catsup's +cattail +cattail's +cattails +cattier +cattiest +cattily +cattiness +cattiness's +cattle +cattle's +cattleman +cattleman's +cattlemen +catty +catwalk +catwalk's +catwalks +caucus +caucus's +caucused +caucuses +caucusing +caucussed +caucussing +caudal +caught +cauldron +cauldron's +cauldrons +cauliflower +cauliflower's +cauliflowers +caulk +caulk's +caulked +caulking +caulking's +caulkings +caulks +causal +causalities +causality +causality's +causally +causation +causation's +causative +cause +cause's +caused +causeless +causes +causeway +causeway's +causeways +causing +caustic +caustic's +caustically +caustics +cauterize +cauterized +cauterizes +cauterizing +caution +caution's +cautionary +cautioned +cautioning +cautions +cautious +cautiously +cautiousness +cautiousness's +cavalcade +cavalcade's +cavalcades +cavalier +cavalier's +cavaliers +cavalries +cavalry +cavalry's +cavalryman +cavalryman's +cavalrymen +cave +cave's +caveat +caveat's +caveats +caved +caveman +caveman's +cavemen +cavern +cavern's +cavernous +caverns +caves +caviar +caviar's +caviare +caviare's +cavil +cavil's +caviled +caviling +cavilled +cavilling +cavils +caving +cavities +cavity +cavity's +cavort +cavorted +cavorting +cavorts +caw +caw's +cawed +cawing +caws +cayenne +cayenne's +cease +cease's +ceased +ceasefire +ceaseless +ceaselessly +ceases +ceasing +cedar +cedar's +cedars +cede +ceded +cedes +cedilla +cedilla's +cedillas +ceding +ceiling +ceiling's +ceilings +celebrant +celebrant's +celebrants +celebrate +celebrated +celebrates +celebrating +celebration +celebration's +celebrations +celebratory +celebrities +celebrity +celebrity's +celerity +celerity's +celery +celery's +celesta +celesta's +celestas +celestial +celibacy +celibacy's +celibate +celibate's +celibates +cell +cell's +cellar +cellar's +cellars +celli +cellist +cellist's +cellists +cello +cello's +cellophane +cellophane's +cellos +cells +cellular +cellular's +cellulars +cellulite +cellulite's +celluloid +celluloid's +cellulose +cellulose's +cement +cement's +cemented +cementing +cements +cemeteries +cemetery +cemetery's +cenotaph +cenotaph's +cenotaphs +censer +censer's +censers +censor +censor's +censored +censoring +censorious +censoriously +censors +censorship +censorship's +censure +censure's +censured +censures +censuring +census +census's +censused +censuses +censusing +cent +cent's +centaur +centaur's +centaurs +centenarian +centenarian's +centenarians +centenaries +centenary +centenary's +centennial +centennial's +centennials +center +center's +centered +centerfold +centerfold's +centerfolds +centering +centerpiece +centerpiece's +centerpieces +centers +centigrade +centigram +centigram's +centigramme +centigramme's +centigrammes +centigrams +centiliter +centiliter's +centiliters +centime +centime's +centimes +centimeter +centimeter's +centimeters +centipede +centipede's +centipedes +central +central's +centralization +centralization's +centralize +centralized +centralizes +centralizing +centrally +centrals +centrifugal +centrifuge +centrifuge's +centrifuged +centrifuges +centrifuging +centripetal +centrist +centrist's +centrists +cents +centuries +centurion +centurion's +centurions +century +century's +cephalic +ceramic +ceramic's +ceramics +ceramics's +cereal +cereal's +cereals +cerebella +cerebellum +cerebellum's +cerebellums +cerebra +cerebral +cerebrum +cerebrum's +cerebrums +ceremonial +ceremonial's +ceremonially +ceremonials +ceremonies +ceremonious +ceremoniously +ceremony +ceremony's +cerise +cerise's +certain +certainly +certainties +certainty +certainty's +certifiable +certificate +certificate's +certificated +certificates +certificating +certification +certification's +certifications +certified +certifies +certify +certifying +certitude +certitude's +cerulean +cerulean's +cervical +cervices +cervix +cervix's +cervixes +cesarean +cesarean's +cesareans +cesarian +cesarian's +cesarians +cesium +cesium's +cessation +cessation's +cessations +cession +cession's +cessions +cesspool +cesspool's +cesspools +cetacean +cetacean's +cetaceans +chafe +chafed +chafes +chaff +chaff's +chaffed +chaffinch +chaffinch's +chaffinches +chaffing +chaffs +chafing +chagrin +chagrin's +chagrined +chagrining +chagrinned +chagrinning +chagrins +chain +chain's +chained +chaining +chains +chainsaw +chainsaw's +chainsawed +chainsawing +chainsaws +chair +chair's +chaired +chairing +chairlift +chairlift's +chairlifts +chairman +chairman's +chairmanship +chairmanship's +chairmen +chairperson +chairperson's +chairpersons +chairs +chairwoman +chairwoman's +chairwomen +chaise +chaise's +chaises +chalet +chalet's +chalets +chalice +chalice's +chalices +chalk +chalk's +chalkboard +chalkboard's +chalkboards +chalked +chalkier +chalkiest +chalking +chalks +chalky +challenge +challenge's +challenged +challenger +challenger's +challengers +challenges +challenging +chamber +chamber's +chamberlain +chamberlain's +chamberlains +chambermaid +chambermaid's +chambermaids +chambers +chambray +chambray's +chameleon +chameleon's +chameleons +chammies +chammy +chammy's +chamois +chamois's +chamoix +chamomile +chamomile's +chamomiles +champ +champ's +champagne +champagne's +champagnes +champed +champing +champion +champion's +championed +championing +champions +championship +championship's +championships +champs +chance +chance's +chanced +chancel +chancel's +chancelleries +chancellery +chancellery's +chancellor +chancellor's +chancellors +chancels +chanceries +chancery +chancery's +chances +chancier +chanciest +chancing +chancy +chandelier +chandelier's +chandeliers +chandler +chandler's +chandlers +change +change's +changeable +changed +changeling +changeling's +changelings +changeover +changeover's +changeovers +changes +changing +channel +channel's +channeled +channeling +channelled +channelling +channels +chant +chant's +chanted +chanter +chanter's +chanters +chantey +chantey's +chanteys +chanticleer +chanticleer's +chanticleers +chanties +chanting +chants +chanty +chanty's +chaos +chaos's +chaotic +chaotically +chap +chap's +chaparral +chaparral's +chaparrals +chapel +chapel's +chapels +chaperon +chaperon's +chaperone +chaperone's +chaperoned +chaperones +chaperoning +chaperons +chaplain +chaplain's +chaplaincies +chaplaincy +chaplaincy's +chaplains +chaplet +chaplet's +chaplets +chapped +chapping +chaps +chapt +chapter +chapter's +chapters +char +char's +character +character's +characteristic +characteristic's +characteristically +characteristics +characterization +characterization's +characterizations +characterize +characterized +characterizes +characterizing +characters +charade +charade's +charades +charbroil +charbroiled +charbroiling +charbroils +charcoal +charcoal's +charcoals +charge +charge's +chargeable +charged +charger +charger's +chargers +charges +charging +charier +chariest +charily +chariot +chariot's +charioteer +charioteer's +charioteers +chariots +charisma +charisma's +charismatic +charismatic's +charismatics +charitable +charitably +charities +charity +charity's +charlatan +charlatan's +charlatans +charm +charm's +charmed +charmer +charmer's +charmers +charming +charmingly +charms +charred +charring +chars +chart +chart's +charted +charter +charter's +chartered +chartering +charters +charting +chartreuse +chartreuse's +charts +charwoman +charwoman's +charwomen +chary +chase +chase's +chased +chaser +chaser's +chasers +chases +chasing +chasm +chasm's +chasms +chassis +chassis's +chaste +chastely +chasten +chastened +chastening +chastens +chaster +chastest +chastise +chastised +chastisement +chastisement's +chastisements +chastises +chastising +chastity +chastity's +chasuble +chasuble's +chasubles +chat +chat's +chateaus +chats +chatted +chattel +chattel's +chattels +chatter +chatter's +chatterbox +chatterbox's +chatterboxes +chattered +chatterer +chatterer's +chatterers +chattering +chatters +chattier +chattiest +chattily +chattiness +chattiness's +chatting +chatty +chauffeur +chauffeur's +chauffeured +chauffeuring +chauffeurs +chauvinism +chauvinism's +chauvinist +chauvinist's +chauvinistic +chauvinists +cheap +cheapen +cheapened +cheapening +cheapens +cheaper +cheapest +cheaply +cheapness +cheapness's +cheapskate +cheapskate's +cheapskates +cheat +cheat's +cheated +cheater +cheater's +cheaters +cheating +cheats +check +check's +checkbook +checkbook's +checkbooks +checked +checker +checker's +checkerboard +checkerboard's +checkerboards +checkered +checkering +checkers +checkers's +checking +checklist +checklist's +checklists +checkmate +checkmate's +checkmated +checkmates +checkmating +checkout +checkout's +checkouts +checkpoint +checkpoint's +checkpoints +checkroom +checkroom's +checkrooms +checks +checkup +checkup's +checkups +cheddar +cheddar's +cheek +cheek's +cheekbone +cheekbone's +cheekbones +cheeked +cheekier +cheekiest +cheekily +cheekiness +cheekiness's +cheeking +cheeks +cheeky +cheep +cheep's +cheeped +cheeping +cheeps +cheer +cheer's +cheered +cheerful +cheerfuller +cheerfullest +cheerfully +cheerfulness +cheerfulness's +cheerier +cheeriest +cheerily +cheeriness +cheeriness's +cheering +cheerleader +cheerleader's +cheerleaders +cheerless +cheerlessly +cheerlessness +cheerlessness's +cheers +cheery +cheese +cheese's +cheeseburger +cheeseburger's +cheeseburgers +cheesecake +cheesecake's +cheesecakes +cheesecloth +cheesecloth's +cheesed +cheeses +cheesier +cheesiest +cheesing +cheesy +cheetah +cheetah's +cheetahs +chef +chef's +chefs +chemical +chemical's +chemically +chemicals +chemise +chemise's +chemises +chemist +chemist's +chemistry +chemistry's +chemists +chemotherapy +chemotherapy's +chenille +chenille's +cherish +cherished +cherishes +cherishing +cheroot +cheroot's +cheroots +cherries +cherry +cherry's +cherub +cherub's +cherubic +cherubim +cherubims +cherubs +chervil +chervil's +chess +chess's +chessboard +chessboard's +chessboards +chessman +chessman's +chessmen +chest +chest's +chestnut +chestnut's +chestnuts +chests +chevron +chevron's +chevrons +chew +chew's +chewed +chewer +chewer's +chewers +chewier +chewiest +chewing +chews +chewy +chi +chiaroscuro +chiaroscuro's +chic +chic's +chicaneries +chicanery +chicanery's +chicer +chicest +chichi +chichi's +chichis +chick +chick's +chickadee +chickadee's +chickadees +chicken +chicken's +chickened +chickening +chickenpox +chickenpox's +chickens +chickpea +chickpea's +chickpeas +chicks +chickweed +chickweed's +chicle +chicle's +chicories +chicory +chicory's +chid +chidden +chide +chided +chides +chiding +chief +chief's +chiefer +chiefest +chiefly +chiefs +chieftain +chieftain's +chieftains +chiffon +chiffon's +chigger +chigger's +chiggers +chignon +chignon's +chignons +chilblain +chilblain's +chilblains +child +child's +childbearing +childbearing's +childbirth +childbirth's +childbirths +childcare +childcare's +childhood +childhood's +childhoods +childish +childishly +childishness +childishness's +childless +childlessness +childlessness's +childlike +childproof +childproofed +childproofing +childproofs +children +children's +chile +chile's +chiles +chili +chili's +chilies +chilis +chill +chill's +chilled +chiller +chiller's +chillers +chillest +chilli +chilli's +chillier +chillies +chilliest +chilliness +chilliness's +chilling +chillings +chills +chilly +chimaera +chimaera's +chimaeras +chime +chime's +chimed +chimera +chimera's +chimeras +chimerical +chimes +chiming +chimney +chimney's +chimneys +chimp +chimp's +chimpanzee +chimpanzee's +chimpanzees +chimps +chin +chin's +china +china's +chinchilla +chinchilla's +chinchillas +chink +chink's +chinked +chinking +chinks +chinned +chinning +chino +chino's +chinos +chins +chinstrap +chinstrap's +chinstraps +chintz +chintz's +chintzier +chintziest +chintzy +chip +chip's +chipmunk +chipmunk's +chipmunks +chipped +chipper +chipper's +chippers +chipping +chips +chiropodist +chiropodist's +chiropodists +chiropody +chiropody's +chiropractic +chiropractic's +chiropractics +chiropractor +chiropractor's +chiropractors +chirp +chirp's +chirped +chirping +chirps +chirrup +chirrup's +chirruped +chirruping +chirrupped +chirrupping +chirrups +chisel +chisel's +chiseled +chiseler +chiseler's +chiselers +chiseling +chiselled +chiseller +chiseller's +chisellers +chiselling +chisels +chit +chit's +chitchat +chitchat's +chitchats +chitchatted +chitchatting +chitin +chitin's +chitlings +chitlings's +chitlins +chitlins's +chits +chitterlings +chitterlings's +chivalrous +chivalrously +chivalry +chivalry's +chive +chive's +chives +chloride +chloride's +chlorides +chlorinate +chlorinated +chlorinates +chlorinating +chlorination +chlorination's +chlorine +chlorine's +chlorofluorocarbon +chlorofluorocarbon's +chlorofluorocarbons +chloroform +chloroform's +chloroformed +chloroforming +chloroforms +chlorophyll +chlorophyll's +chock +chock's +chocked +chocking +chocks +chocolate +chocolate's +chocolates +choice +choice's +choicer +choices +choicest +choir +choir's +choirs +choke +choke's +choked +choker +choker's +chokers +chokes +choking +choler +choler's +cholera +cholera's +choleric +cholesterol +cholesterol's +chomp +chomp's +chomped +chomping +chomps +choose +chooses +choosey +choosier +choosiest +choosing +choosy +chop +chop's +chopped +chopper +chopper's +choppered +choppering +choppers +choppier +choppiest +choppily +choppiness +choppiness's +chopping +choppy +chops +chopstick +chopstick's +chopsticks +choral +choral's +chorale +chorale's +chorales +chorals +chord +chord's +chords +chore +chore's +choreograph +choreographed +choreographer +choreographer's +choreographers +choreographic +choreographing +choreographs +choreography +choreography's +chores +chorister +chorister's +choristers +chortle +chortle's +chortled +chortles +chortling +chorus +chorus's +chorused +choruses +chorusing +chorussed +chorussing +chose +chosen +chow +chow's +chowder +chowder's +chowders +chowed +chowing +chows +christen +christened +christening +christening's +christenings +christens +chromatic +chrome +chrome's +chromed +chromes +chroming +chromium +chromium's +chromosome +chromosome's +chromosomes +chronic +chronically +chronicle +chronicle's +chronicled +chronicler +chronicler's +chroniclers +chronicles +chronicling +chronological +chronologically +chronologies +chronology +chronology's +chronometer +chronometer's +chronometers +chrysalides +chrysalis +chrysalis's +chrysalises +chrysanthemum +chrysanthemum's +chrysanthemums +chubbier +chubbiest +chubbiness +chubbiness's +chubby +chuck +chuck's +chucked +chuckhole +chuckhole's +chuckholes +chucking +chuckle +chuckle's +chuckled +chuckles +chuckling +chucks +chug +chug's +chugged +chugging +chugs +chum +chum's +chummed +chummier +chummiest +chumminess +chumminess's +chumming +chummy +chump +chump's +chumps +chums +chunk +chunk's +chunkier +chunkiest +chunkiness +chunkiness's +chunks +chunky +church +church's +churches +churchgoer +churchgoer's +churchgoers +churchman +churchman's +churchmen +churchyard +churchyard's +churchyards +churl +churl's +churlish +churlishly +churlishness +churlishness's +churls +churn +churn's +churned +churning +churns +chute +chute's +chutes +chutney +chutney's +chutzpa +chutzpa's +chutzpah +chutzpah's +château +château's +châteaux +châtelaine +châtelaine's +châtelaines +ciabatta +ciabatta's +ciabattas +cicada +cicada's +cicadae +cicadas +cicatrice +cicatrice's +cicatrices +cicatrix +cicatrix's +cider +cider's +ciders +cigar +cigar's +cigaret +cigaret's +cigarets +cigarette +cigarette's +cigarettes +cigarillo +cigarillo's +cigarillos +cigars +cilantro +cilantro's +cilia +cilium +cilium's +cinch +cinch's +cinched +cinches +cinching +cinchona +cinchona's +cinchonas +cincture +cincture's +cinctures +cinder +cinder's +cindered +cindering +cinders +cinema +cinema's +cinemas +cinematic +cinematographer +cinematographer's +cinematographers +cinematography +cinematography's +cinnabar +cinnabar's +cinnamon +cinnamon's +cipher +cipher's +ciphered +ciphering +ciphers +circa +circadian +circle +circle's +circled +circles +circlet +circlet's +circlets +circling +circuit +circuit's +circuited +circuiting +circuitous +circuitously +circuitry +circuitry's +circuits +circular +circular's +circularity +circularity's +circularize +circularized +circularizes +circularizing +circulars +circulate +circulated +circulates +circulating +circulation +circulation's +circulations +circulatory +circumcise +circumcised +circumcises +circumcising +circumcision +circumcision's +circumcisions +circumference +circumference's +circumferences +circumflex +circumflex's +circumflexes +circumlocution +circumlocution's +circumlocutions +circumnavigate +circumnavigated +circumnavigates +circumnavigating +circumnavigation +circumnavigation's +circumnavigations +circumscribe +circumscribed +circumscribes +circumscribing +circumscription +circumscription's +circumscriptions +circumspect +circumspection +circumspection's +circumstance +circumstance's +circumstanced +circumstances +circumstancing +circumstantial +circumstantially +circumvent +circumvented +circumventing +circumvention +circumvention's +circumvents +circus +circus's +circuses +cirrhosis +cirrhosis's +cirrus +cirrus's +cistern +cistern's +cisterns +citadel +citadel's +citadels +citation +citation's +citations +cite +cite's +cited +cites +cities +citing +citizen +citizen's +citizenry +citizenry's +citizens +citizenship +citizenship's +citric +citron +citron's +citronella +citronella's +citrons +citrous +citrus +citrus's +citruses +city +city's +civet +civet's +civets +civic +civics +civics's +civies +civil +civilian +civilian's +civilians +civilities +civility +civility's +civilization +civilization's +civilizations +civilize +civilized +civilizes +civilizing +civilly +civvies +civvies's +clack +clack's +clacked +clacking +clacks +clad +claim +claim's +claimant +claimant's +claimants +claimed +claiming +claims +clairvoyance +clairvoyance's +clairvoyant +clairvoyant's +clairvoyants +clam +clam's +clambake +clambake's +clambakes +clamber +clamber's +clambered +clambering +clambers +clammed +clammier +clammiest +clamminess +clamminess's +clamming +clammy +clamor +clamor's +clamored +clamoring +clamorous +clamors +clamp +clamp's +clampdown +clampdown's +clampdowns +clamped +clamping +clamps +clams +clan +clan's +clandestine +clandestinely +clang +clang's +clanged +clanging +clangor +clangor's +clangs +clank +clank's +clanked +clanking +clanks +clannish +clans +clap +clap's +clapboard +clapboard's +clapboarded +clapboarding +clapboards +clapped +clapper +clapper's +clappers +clapping +claps +claptrap +claptrap's +claret +claret's +clarets +clarification +clarification's +clarifications +clarified +clarifies +clarify +clarifying +clarinet +clarinet's +clarinetist +clarinetist's +clarinetists +clarinets +clarinettist +clarinettist's +clarinettists +clarion +clarion's +clarioned +clarioning +clarions +clarity +clarity's +clash +clash's +clashed +clashes +clashing +clasp +clasp's +clasped +clasping +clasps +class +class's +classed +classes +classic +classic's +classical +classical's +classically +classicism +classicism's +classicist +classicist's +classicists +classics +classier +classiest +classifiable +classification +classification's +classifications +classified +classified's +classifieds +classifies +classify +classifying +classiness +classiness's +classing +classless +classmate +classmate's +classmates +classroom +classroom's +classrooms +classy +clatter +clatter's +clattered +clattering +clatters +clause +clause's +clauses +claustrophobia +claustrophobia's +claustrophobic +clavichord +clavichord's +clavichords +clavicle +clavicle's +clavicles +claw +claw's +clawed +clawing +claws +clay +clay's +clayey +clayier +clayiest +clean +cleaned +cleaner +cleaner's +cleaners +cleanest +cleaning +cleaning's +cleanings +cleanlier +cleanliest +cleanliness +cleanliness's +cleanly +cleanness +cleanness's +cleans +cleanse +cleansed +cleanser +cleanser's +cleansers +cleanses +cleansing +cleanup +cleanup's +cleanups +clear +clear's +clearance +clearance's +clearances +cleared +clearer +clearest +clearing +clearing's +clearinghouse +clearinghouse's +clearinghouses +clearings +clearly +clearness +clearness's +clears +cleat +cleat's +cleats +cleavage +cleavage's +cleavages +cleave +cleaved +cleaver +cleaver's +cleavers +cleaves +cleaving +clef +clef's +clefs +cleft +cleft's +clefts +clematis +clematis's +clematises +clemency +clemency's +clement +clench +clench's +clenched +clenches +clenching +clerestories +clerestory +clerestory's +clergies +clergy +clergy's +clergyman +clergyman's +clergymen +clergywoman +clergywoman's +clergywomen +cleric +cleric's +clerical +clerics +clerk +clerk's +clerked +clerking +clerks +clever +cleverer +cleverest +cleverly +cleverness +cleverness's +clew +clew's +clewed +clewing +clews +cliché +cliché's +clichéd +clichés +click +click's +clickable +clicked +clicking +clicks +client +client's +clients +clientèle +clientèle's +clientèles +cliff +cliff's +cliffhanger +cliffhanger's +cliffhangers +cliffs +climactic +climate +climate's +climates +climatic +climax +climax's +climaxed +climaxes +climaxing +climb +climb's +climbed +climber +climber's +climbers +climbing +climbs +clime +clime's +climes +clinch +clinch's +clinched +clincher +clincher's +clinchers +clinches +clinching +cling +cling's +clingier +clingiest +clinging +clings +clingy +clinic +clinic's +clinical +clinically +clinician +clinician's +clinicians +clinics +clink +clink's +clinked +clinker +clinker's +clinkers +clinking +clinks +clip +clip's +clipboard +clipboard's +clipboards +clipped +clipper +clipper's +clippers +clipping +clipping's +clippings +clips +clipt +clique +clique's +cliques +cliquish +clit +clit's +clitoral +clitoris +clitoris's +clitorises +clits +cloak +cloak's +cloaked +cloaking +cloakroom +cloakroom's +cloakrooms +cloaks +clobber +clobber's +clobbered +clobbering +clobbers +cloche +cloche's +cloches +clock +clock's +clocked +clocking +clocks +clockwise +clockwork +clockwork's +clockworks +clod +clod's +clodhopper +clodhopper's +clodhoppers +clods +clog +clog's +clogged +clogging +clogs +cloister +cloister's +cloistered +cloistering +cloisters +clomp +clomped +clomping +clomps +clone +clone's +cloned +clones +cloning +clop +clop's +clopped +clopping +clops +close +close's +closed +closefisted +closely +closemouthed +closeness +closeness's +closeout +closeout's +closeouts +closer +closes +closest +closet +closet's +closeted +closeting +closets +closing +closure +closure's +closures +clot +clot's +cloth +cloth's +clothe +clothed +clothes +clothesline +clothesline's +clotheslines +clothespin +clothespin's +clothespins +clothier +clothier's +clothiers +clothing +clothing's +cloths +clots +clotted +clotting +cloture +cloture's +clotures +cloud +cloud's +cloudburst +cloudburst's +cloudbursts +clouded +cloudier +cloudiest +cloudiness +cloudiness's +clouding +cloudless +clouds +cloudy +clout +clout's +clouted +clouting +clouts +clove +clove's +cloven +clover +clover's +cloverleaf +cloverleaf's +cloverleafs +cloverleaves +clovers +cloves +clown +clown's +clowned +clowning +clownish +clownishly +clownishness +clownishness's +clowns +cloy +cloyed +cloying +cloys +club +club's +clubbed +clubbing +clubfeet +clubfoot +clubfoot's +clubhouse +clubhouse's +clubhouses +clubs +cluck +cluck's +clucked +clucking +clucks +clue +clue's +clued +clueing +clueless +clues +cluing +clump +clump's +clumped +clumping +clumps +clumsier +clumsiest +clumsily +clumsiness +clumsiness's +clumsy +clung +clunk +clunk's +clunked +clunker +clunker's +clunkers +clunkier +clunkiest +clunking +clunks +clunky +cluster +cluster's +clustered +clustering +clusters +clutch +clutch's +clutched +clutches +clutching +clutter +clutter's +cluttered +cluttering +clutters +coach +coach's +coached +coaches +coaching +coachman +coachman's +coachmen +coagulant +coagulant's +coagulants +coagulate +coagulated +coagulates +coagulating +coagulation +coagulation's +coal +coal's +coaled +coalesce +coalesced +coalescence +coalescence's +coalesces +coalescing +coaling +coalition +coalition's +coalitions +coals +coarse +coarsely +coarsen +coarsened +coarseness +coarseness's +coarsening +coarsens +coarser +coarsest +coast +coast's +coastal +coasted +coaster +coaster's +coasters +coasting +coastline +coastline's +coastlines +coasts +coat +coat's +coated +coating +coating's +coatings +coats +coauthor +coauthor's +coauthored +coauthoring +coauthors +coax +coaxed +coaxes +coaxing +cob +cob's +cobalt +cobalt's +cobble +cobble's +cobbled +cobbler +cobbler's +cobblers +cobbles +cobblestone +cobblestone's +cobblestones +cobbling +cobra +cobra's +cobras +cobs +cobweb +cobweb's +cobwebs +cocaine +cocaine's +cocci +coccis +coccus +coccus's +coccyges +coccyx +coccyx's +coccyxes +cochlea +cochlea's +cochleae +cochleas +cock +cock's +cockade +cockade's +cockades +cockamamie +cockatoo +cockatoo's +cockatoos +cocked +cockerel +cockerel's +cockerels +cockeyed +cockfight +cockfight's +cockfights +cockier +cockiest +cockily +cockiness +cockiness's +cocking +cockle +cockle's +cockles +cockleshell +cockleshell's +cockleshells +cockney +cockney's +cockneys +cockpit +cockpit's +cockpits +cockroach +cockroach's +cockroaches +cocks +cockscomb +cockscomb's +cockscombs +cocksucker +cocksucker's +cocksuckers +cocksure +cocktail +cocktail's +cocktails +cocky +cocoa +cocoa's +cocoanut +cocoanut's +cocoanuts +cocoas +coconut +coconut's +coconuts +cocoon +cocoon's +cocooned +cocooning +cocoons +cod +cod's +coda +coda's +codas +codded +codding +coddle +coddled +coddles +coddling +code +code's +coded +codeine +codeine's +codependency +codependency's +codependent +codependent's +codependents +codes +codex +codex's +codfish +codfish's +codfishes +codger +codger's +codgers +codices +codicil +codicil's +codicils +codification +codification's +codifications +codified +codifies +codify +codifying +coding +cods +coed +coed's +coeds +coeducation +coeducation's +coeducational +coefficient +coefficient's +coefficients +coequal +coequal's +coequals +coerce +coerced +coerces +coercing +coercion +coercion's +coercive +coeval +coeval's +coevals +coexist +coexisted +coexistence +coexistence's +coexisting +coexists +coffee +coffee's +coffeecake +coffeecake's +coffeecakes +coffeehouse +coffeehouse's +coffeehouses +coffeepot +coffeepot's +coffeepots +coffees +coffer +coffer's +coffers +coffin +coffin's +coffined +coffining +coffins +cog +cog's +cogency +cogency's +cogent +cogently +cogitate +cogitated +cogitates +cogitating +cogitation +cogitation's +cognac +cognac's +cognacs +cognate +cognate's +cognates +cognition +cognition's +cognitive +cognizance +cognizance's +cognizant +cognomen +cognomen's +cognomens +cognomina +cogs +cogwheel +cogwheel's +cogwheels +cohabit +cohabitation +cohabitation's +cohabited +cohabiting +cohabits +cohere +cohered +coherence +coherence's +coherent +coherently +coheres +cohering +cohesion +cohesion's +cohesive +cohesively +cohesiveness +cohesiveness's +cohort +cohort's +cohorts +coif +coif's +coifed +coiffed +coiffing +coiffure +coiffure's +coiffured +coiffures +coiffuring +coifing +coifs +coil +coil's +coiled +coiling +coils +coin +coin's +coinage +coinage's +coinages +coincide +coincided +coincidence +coincidence's +coincidences +coincident +coincidental +coincidentally +coincides +coinciding +coined +coining +coins +coital +coitus +coitus's +coke +coke's +coked +cokes +coking +cola +cola's +colander +colander's +colanders +colas +cold +cold's +colder +coldest +coldly +coldness +coldness's +colds +coleslaw +coleslaw's +colic +colic's +colicky +coliseum +coliseum's +coliseums +colitis +colitis's +collaborate +collaborated +collaborates +collaborating +collaboration +collaboration's +collaborations +collaborative +collaborator +collaborator's +collaborators +collage +collage's +collages +collapse +collapse's +collapsed +collapses +collapsible +collapsing +collar +collar's +collarbone +collarbone's +collarbones +collared +collaring +collars +collate +collated +collateral +collateral's +collates +collating +collation +collation's +collations +colleague +colleague's +colleagues +collect +collect's +collectable +collectable's +collectables +collected +collectible +collectible's +collectibles +collecting +collection +collection's +collections +collective +collective's +collectively +collectives +collectivism +collectivism's +collectivist +collectivist's +collectivists +collectivize +collectivized +collectivizes +collectivizing +collector +collector's +collectors +collects +colleen +colleen's +colleens +college +college's +colleges +collegian +collegian's +collegians +collegiate +collide +collided +collides +colliding +collie +collie's +collier +collier's +collieries +colliers +colliery +colliery's +collies +collision +collision's +collisions +collocate +collocate's +collocated +collocates +collocating +collocation +collocation's +collocations +colloid +colloid's +colloids +colloquia +colloquial +colloquialism +colloquialism's +colloquialisms +colloquially +colloquies +colloquium +colloquium's +colloquiums +colloquy +colloquy's +collude +colluded +colludes +colluding +collusion +collusion's +collusive +cologne +cologne's +colognes +colon +colon's +colonel +colonel's +colonels +colones +colonial +colonial's +colonialism +colonialism's +colonialist +colonialist's +colonialists +colonials +colonies +colonist +colonist's +colonists +colonization +colonization's +colonize +colonized +colonizer +colonizer's +colonizers +colonizes +colonizing +colonnade +colonnade's +colonnades +colonoscopies +colonoscopy +colonoscopy's +colons +colony +colony's +color +color's +coloration +coloration's +coloratura +coloratura's +coloraturas +colorblind +colored +colored's +coloreds +colorfast +colorful +colorfully +coloring +coloring's +colorless +colors +colossal +colossally +colossi +colossus +colossus's +colossuses +cols +colt +colt's +coltish +colts +columbine +columbine's +columbines +column +column's +columned +columnist +columnist's +columnists +columns +coma +coma's +comas +comatose +comb +comb's +combat +combat's +combatant +combatant's +combatants +combated +combating +combative +combats +combatted +combatting +combed +combination +combination's +combinations +combine +combine's +combined +combines +combing +combining +combo +combo's +combos +combs +combustibility +combustibility's +combustible +combustible's +combustibles +combustion +combustion's +come +come's +comeback +comeback's +comebacks +comedian +comedian's +comedians +comedic +comedienne +comedienne's +comediennes +comedies +comedown +comedown's +comedowns +comedy +comedy's +comelier +comeliest +comeliness +comeliness's +comely +comer +comer's +comers +comes +comestible +comestible's +comestibles +comet +comet's +comets +comeuppance +comeuppance's +comeuppances +comfier +comfiest +comfort +comfort's +comfortable +comfortably +comforted +comforter +comforter's +comforters +comforting +comfortingly +comforts +comfy +comic +comic's +comical +comically +comics +coming +coming's +comings +comity +comity's +comma +comma's +command +command's +commandant +commandant's +commandants +commanded +commandeer +commandeered +commandeering +commandeers +commander +commander's +commanders +commanding +commandment +commandment's +commandments +commando +commando's +commandoes +commandos +commands +commas +commemorate +commemorated +commemorates +commemorating +commemoration +commemoration's +commemorations +commemorative +commence +commenced +commencement +commencement's +commencements +commences +commencing +commend +commendable +commendably +commendation +commendation's +commendations +commended +commending +commends +commensurable +commensurate +comment +comment's +commentaries +commentary +commentary's +commentate +commentated +commentates +commentating +commentator +commentator's +commentators +commented +commenting +comments +commerce +commerce's +commercial +commercial's +commercialism +commercialism's +commercialization +commercialization's +commercialize +commercialized +commercializes +commercializing +commercially +commercials +commingle +commingled +commingles +commingling +commiserate +commiserated +commiserates +commiserating +commiseration +commiseration's +commiserations +commissar +commissar's +commissariat +commissariat's +commissariats +commissaries +commissars +commissary +commissary's +commission +commission's +commissioned +commissioner +commissioner's +commissioners +commissioning +commissions +commit +commitment +commitment's +commitments +commits +committal +committal's +committals +committed +committee +committee's +committees +committing +commode +commode's +commodes +commodious +commodities +commodity +commodity's +commodore +commodore's +commodores +common +common's +commoner +commoner's +commoners +commonest +commonly +commonplace +commonplace's +commonplaces +commons +commonwealth +commonwealth's +commonwealths +commotion +commotion's +commotions +communal +communally +commune +commune's +communed +communes +communicable +communicant +communicant's +communicants +communicate +communicated +communicates +communicating +communication +communication's +communications +communicative +communicator +communicator's +communicators +communing +communion +communion's +communions +communique +communique's +communiques +communism +communism's +communist +communist's +communistic +communists +communities +community +community's +commutation +commutation's +commutations +commutative +commute +commute's +commuted +commuter +commuter's +commuters +commutes +commuting +compact +compact's +compacted +compacter +compactest +compacting +compaction +compactly +compactness +compactness's +compactor +compactor's +compactors +compacts +companies +companion +companion's +companionable +companions +companionship +companionship's +companionway +companionway's +companionways +company +company's +comparability +comparability's +comparable +comparably +comparative +comparative's +comparatively +comparatives +compare +compare's +compared +compares +comparing +comparison +comparison's +comparisons +compartment +compartment's +compartmentalize +compartmentalized +compartmentalizes +compartmentalizing +compartments +compass +compass's +compassed +compasses +compassing +compassion +compassion's +compassionate +compassionately +compatibility +compatibility's +compatible +compatible's +compatibles +compatibly +compatriot +compatriot's +compatriots +compel +compelled +compelling +compellingly +compels +compendia +compendium +compendium's +compendiums +compensate +compensated +compensates +compensating +compensation +compensation's +compensations +compensatory +compete +competed +competence +competence's +competences +competencies +competency +competency's +competent +competently +competes +competing +competition +competition's +competitions +competitive +competitively +competitiveness +competitiveness's +competitor +competitor's +competitors +compilation +compilation's +compilations +compile +compiled +compiler +compiler's +compilers +compiles +compiling +complacence +complacence's +complacency +complacency's +complacent +complacently +complain +complainant +complainant's +complainants +complained +complainer +complainer's +complainers +complaining +complains +complaint +complaint's +complaints +complaisance +complaisance's +complaisant +complaisantly +complected +complement +complement's +complementary +complemented +complementing +complements +complete +completed +completely +completeness +completeness's +completer +completes +completest +completing +completion +completion's +complex +complex's +complexes +complexion +complexion's +complexioned +complexions +complexities +complexity +complexity's +compliance +compliance's +compliant +complicate +complicated +complicates +complicating +complication +complication's +complications +complicity +complicity's +complied +complies +compliment +compliment's +complimentary +complimented +complimenting +compliments +comply +complying +component +component's +components +comport +comported +comporting +comportment +comportment's +comports +compose +composed +composer +composer's +composers +composes +composing +composite +composite's +composites +composition +composition's +compositions +compositor +compositor's +compositors +compost +compost's +composted +composting +composts +composure +composure's +compote +compote's +compotes +compound +compound's +compounded +compounding +compounds +comprehend +comprehended +comprehending +comprehends +comprehensibility +comprehensibility's +comprehensible +comprehension +comprehension's +comprehensions +comprehensive +comprehensive's +comprehensively +comprehensiveness +comprehensiveness's +comprehensives +compress +compress's +compressed +compresses +compressing +compression +compression's +compressor +compressor's +compressors +comprise +comprised +comprises +comprising +compromise +compromise's +compromised +compromises +compromising +comptroller +comptroller's +comptrollers +compulsion +compulsion's +compulsions +compulsive +compulsively +compulsiveness +compulsiveness's +compulsories +compulsorily +compulsory +compulsory's +compunction +compunction's +compunctions +computation +computation's +computational +computationally +computations +compute +computed +computer +computer's +computerization +computerization's +computerize +computerized +computerizes +computerizing +computers +computes +computing +computing's +comrade +comrade's +comrades +comradeship +comradeship's +con +con's +concatenate +concatenated +concatenates +concatenating +concatenation +concatenation's +concatenations +concave +concavities +concavity +concavity's +conceal +concealed +concealing +concealment +concealment's +conceals +concede +conceded +concedes +conceding +conceit +conceit's +conceited +conceits +conceivable +conceivably +conceive +conceived +conceives +conceiving +concentrate +concentrate's +concentrated +concentrates +concentrating +concentration +concentration's +concentrations +concentric +concentrically +concept +concept's +conception +conception's +conceptions +concepts +conceptual +conceptualization +conceptualization's +conceptualizations +conceptualize +conceptualized +conceptualizes +conceptualizing +conceptually +concern +concern's +concerned +concerning +concerns +concert +concert's +concerted +concerti +concertina +concertina's +concertinaed +concertinaing +concertinas +concerting +concertmaster +concertmaster's +concertmasters +concerto +concerto's +concertos +concerts +concession +concession's +concessionaire +concessionaire's +concessionaires +concessions +conch +conch's +conches +conchs +concierge +concierge's +concierges +conciliate +conciliated +conciliates +conciliating +conciliation +conciliation's +conciliator +conciliator's +conciliators +conciliatory +concise +concisely +conciseness +conciseness's +conciser +concisest +conclave +conclave's +conclaves +conclude +concluded +concludes +concluding +conclusion +conclusion's +conclusions +conclusive +conclusively +concoct +concocted +concocting +concoction +concoction's +concoctions +concocts +concomitant +concomitant's +concomitants +concord +concord's +concordance +concordance's +concordances +concordant +concourse +concourse's +concourses +concrete +concrete's +concreted +concretely +concretes +concreting +concubine +concubine's +concubines +concur +concurred +concurrence +concurrence's +concurrences +concurrency +concurrent +concurrently +concurring +concurs +concussion +concussion's +concussions +condemn +condemnation +condemnation's +condemnations +condemnatory +condemned +condemning +condemns +condensation +condensation's +condensations +condense +condensed +condenser +condenser's +condensers +condenses +condensing +condescend +condescended +condescending +condescendingly +condescends +condescension +condescension's +condiment +condiment's +condiments +condition +condition's +conditional +conditional's +conditionally +conditionals +conditioned +conditioner +conditioner's +conditioners +conditioning +conditions +condo +condo's +condoes +condole +condoled +condolence +condolence's +condolences +condoles +condoling +condom +condom's +condominium +condominium's +condominiums +condoms +condone +condoned +condones +condoning +condor +condor's +condors +condos +conduce +conduced +conduces +conducing +conducive +conduct +conduct's +conducted +conducting +conduction +conduction's +conductive +conductivity +conductivity's +conductor +conductor's +conductors +conducts +conduit +conduit's +conduits +cone +cone's +cones +confab +confab's +confabbed +confabbing +confabs +confection +confection's +confectioner +confectioner's +confectioneries +confectioners +confectionery +confectionery's +confections +confederacies +confederacy +confederacy's +confederate +confederate's +confederated +confederates +confederating +confederation +confederation's +confederations +confer +conference +conference's +conferences +conferencing +conferment +conferment's +conferments +conferred +conferrer +conferring +confers +confess +confessed +confessedly +confesses +confessing +confession +confession's +confessional +confessional's +confessionals +confessions +confessor +confessor's +confessors +confetti +confetti's +confidant +confidant's +confidante +confidante's +confidantes +confidants +confide +confided +confidence +confidence's +confidences +confident +confidential +confidentiality +confidentiality's +confidentially +confidently +confides +confiding +configurable +configuration +configuration's +configurations +configure +configured +configures +configuring +confine +confine's +confined +confinement +confinement's +confinements +confines +confining +confirm +confirmation +confirmation's +confirmations +confirmatory +confirmed +confirming +confirms +confiscate +confiscated +confiscates +confiscating +confiscation +confiscation's +confiscations +conflagration +conflagration's +conflagrations +conflict +conflict's +conflicted +conflicting +conflicts +confluence +confluence's +confluences +confluent +conform +conformance +conformation +conformation's +conformations +conformed +conforming +conformist +conformist's +conformists +conformity +conformity's +conforms +confound +confounded +confounding +confounds +confront +confrontation +confrontation's +confrontational +confrontations +confronted +confronting +confronts +confrère +confrère's +confrères +confuse +confused +confusedly +confuses +confusing +confusingly +confusion +confusion's +confusions +confute +confuted +confutes +confuting +conga +conga's +congaed +congaing +congas +congeal +congealed +congealing +congeals +congenial +congeniality +congeniality's +congenially +congenital +congenitally +congest +congested +congesting +congestion +congestion's +congestive +congests +conglomerate +conglomerate's +conglomerated +conglomerates +conglomerating +conglomeration +conglomeration's +conglomerations +congratulate +congratulated +congratulates +congratulating +congratulation +congratulation's +congratulations +congratulatory +congregate +congregated +congregates +congregating +congregation +congregation's +congregational +congregations +congress +congress's +congresses +congressional +congressman +congressman's +congressmen +congresswoman +congresswoman's +congresswomen +congruence +congruence's +congruent +congruities +congruity +congruity's +congruous +conic +conic's +conical +conics +conifer +conifer's +coniferous +conifers +conjectural +conjecture +conjecture's +conjectured +conjectures +conjecturing +conjoin +conjoined +conjoining +conjoins +conjoint +conjugal +conjugate +conjugated +conjugates +conjugating +conjugation +conjugation's +conjugations +conjunction +conjunction's +conjunctions +conjunctive +conjunctive's +conjunctives +conjunctivitis +conjunctivitis's +conjuncture +conjuncture's +conjunctures +conjure +conjured +conjurer +conjurer's +conjurers +conjures +conjuring +conjuror +conjuror's +conjurors +conk +conk's +conked +conking +conks +connect +connected +connecter +connecter's +connecters +connecting +connection +connection's +connections +connective +connective's +connectives +connectivity +connector +connector's +connectors +connects +conned +conning +connivance +connivance's +connive +connived +conniver +conniver's +connivers +connives +conniving +connoisseur +connoisseur's +connoisseurs +connotation +connotation's +connotations +connotative +connote +connoted +connotes +connoting +connubial +conquer +conquered +conquering +conqueror +conqueror's +conquerors +conquers +conquest +conquest's +conquests +conquistador +conquistador's +conquistadores +conquistadors +cons +consanguinity +consanguinity's +conscience +conscience's +consciences +conscientious +conscientiously +conscientiousness +conscientiousness's +conscious +consciously +consciousness +consciousness's +consciousnesses +conscript +conscript's +conscripted +conscripting +conscription +conscription's +conscripts +consecrate +consecrated +consecrates +consecrating +consecration +consecration's +consecrations +consecutive +consecutively +consensual +consensus +consensus's +consensuses +consent +consent's +consented +consenting +consents +consequence +consequence's +consequences +consequent +consequential +consequently +conservation +conservation's +conservationist +conservationist's +conservationists +conservatism +conservatism's +conservative +conservative's +conservatively +conservatives +conservator +conservator's +conservatories +conservators +conservatory +conservatory's +conserve +conserve's +conserved +conserves +conserving +consider +considerable +considerably +considerate +considerately +consideration +consideration's +considerations +considered +considering +considers +consign +consigned +consigning +consignment +consignment's +consignments +consigns +consist +consisted +consistencies +consistency +consistency's +consistent +consistently +consisting +consists +consolation +consolation's +consolations +console +console's +consoled +consoles +consolidate +consolidated +consolidates +consolidating +consolidation +consolidation's +consolidations +consoling +consommé +consommé's +consonance +consonance's +consonances +consonant +consonant's +consonants +consort +consort's +consorted +consortia +consorting +consortium +consortium's +consortiums +consorts +conspicuous +conspicuously +conspiracies +conspiracy +conspiracy's +conspirator +conspirator's +conspiratorial +conspirators +conspire +conspired +conspires +conspiring +constable +constable's +constables +constabularies +constabulary +constabulary's +constancy +constancy's +constant +constant's +constantly +constants +constellation +constellation's +constellations +consternation +consternation's +constipate +constipated +constipates +constipating +constipation +constipation's +constituencies +constituency +constituency's +constituent +constituent's +constituents +constitute +constituted +constitutes +constituting +constitution +constitution's +constitutional +constitutional's +constitutionality +constitutionality's +constitutionally +constitutionals +constitutions +constrain +constrained +constraining +constrains +constraint +constraint's +constraints +constrict +constricted +constricting +constriction +constriction's +constrictions +constrictive +constrictor +constrictor's +constrictors +constricts +construct +construct's +constructed +constructing +construction +construction's +constructions +constructive +constructively +constructor +constructor's +constructors +constructs +construe +construed +construes +construing +consul +consul's +consular +consulate +consulate's +consulates +consuls +consult +consultancies +consultancy +consultancy's +consultant +consultant's +consultants +consultation +consultation's +consultations +consultative +consulted +consulting +consults +consumable +consumable's +consumables +consume +consumed +consumer +consumer's +consumerism +consumerism's +consumers +consumes +consuming +consummate +consummated +consummates +consummating +consummation +consummation's +consummations +consumption +consumption's +consumptive +consumptive's +consumptives +contact +contact's +contactable +contacted +contacting +contacts +contagion +contagion's +contagions +contagious +contain +contained +container +container's +containers +containing +containment +containment's +contains +contaminant +contaminant's +contaminants +contaminate +contaminated +contaminates +contaminating +contamination +contamination's +contemplate +contemplated +contemplates +contemplating +contemplation +contemplation's +contemplative +contemplative's +contemplatives +contemporaneous +contemporaneously +contemporaries +contemporary +contemporary's +contempt +contempt's +contemptible +contemptibly +contemptuous +contemptuously +contend +contended +contender +contender's +contenders +contending +contends +content +content's +contented +contentedly +contentedness +contentedness's +contenting +contention +contention's +contentions +contentious +contentiously +contentment +contentment's +contents +contest +contest's +contestant +contestant's +contestants +contested +contesting +contests +context +context's +contexts +contextual +contiguity +contiguity's +contiguous +continence +continence's +continent +continent's +continental +continental's +continentals +continents +contingencies +contingency +contingency's +contingent +contingent's +contingents +continua +continual +continually +continuance +continuance's +continuances +continuation +continuation's +continuations +continue +continued +continues +continuing +continuity +continuity's +continuous +continuously +continuum +continuum's +continuums +contort +contorted +contorting +contortion +contortion's +contortionist +contortionist's +contortionists +contortions +contorts +contour +contour's +contoured +contouring +contours +contraband +contraband's +contraception +contraception's +contraceptive +contraceptive's +contraceptives +contract +contract's +contracted +contractile +contracting +contraction +contraction's +contractions +contractor +contractor's +contractors +contracts +contractual +contractually +contradict +contradicted +contradicting +contradiction +contradiction's +contradictions +contradictory +contradicts +contradistinction +contradistinction's +contradistinctions +contrail +contrail's +contrails +contralto +contralto's +contraltos +contraption +contraption's +contraptions +contrapuntal +contraries +contrarily +contrariness +contrariness's +contrariwise +contrary +contrary's +contrast +contrast's +contrasted +contrasting +contrasts +contravene +contravened +contravenes +contravening +contravention +contravention's +contraventions +contretemps +contretemps's +contribute +contributed +contributes +contributing +contribution +contribution's +contributions +contributor +contributor's +contributors +contributory +contrite +contritely +contrition +contrition's +contrivance +contrivance's +contrivances +contrive +contrived +contrives +contriving +control +control's +controllable +controlled +controller +controller's +controllers +controlling +controls +controversial +controversially +controversies +controversy +controversy's +controvert +controverted +controverting +controverts +contumacious +contumelies +contumely +contumely's +contuse +contused +contuses +contusing +contusion +contusion's +contusions +conundrum +conundrum's +conundrums +conurbation +conurbation's +conurbations +convalesce +convalesced +convalescence +convalescence's +convalescences +convalescent +convalescent's +convalescents +convalesces +convalescing +convection +convection's +convene +convened +convenes +convenience +convenience's +conveniences +convenient +conveniently +convening +convent +convent's +convention +convention's +conventional +conventionality +conventionality's +conventionally +conventions +convents +converge +converged +convergence +convergence's +convergences +convergent +converges +converging +conversant +conversation +conversation's +conversational +conversationalist +conversationalist's +conversationalists +conversationally +conversations +converse +converse's +conversed +conversely +converses +conversing +conversion +conversion's +conversions +convert +convert's +converted +converter +converter's +converters +convertible +convertible's +convertibles +converting +convertor +convertor's +convertors +converts +convex +convexity +convexity's +convey +conveyance +conveyance's +conveyances +conveyed +conveyer +conveyer's +conveyers +conveying +conveyor +conveyor's +conveyors +conveys +convict +convict's +convicted +convicting +conviction +conviction's +convictions +convicts +convince +convinced +convinces +convincing +convincingly +convivial +conviviality +conviviality's +convocation +convocation's +convocations +convoke +convoked +convokes +convoking +convoluted +convolution +convolution's +convolutions +convoy +convoy's +convoyed +convoying +convoys +convulse +convulsed +convulses +convulsing +convulsion +convulsion's +convulsions +convulsive +convulsively +coo +coo's +cooed +cooing +cook +cook's +cookbook +cookbook's +cookbooks +cooked +cooker +cooker's +cookeries +cookers +cookery +cookery's +cookie +cookie's +cookies +cooking +cooking's +cookout +cookout's +cookouts +cooks +cooky +cooky's +cool +cool's +coolant +coolant's +coolants +cooled +cooler +cooler's +coolers +coolest +coolie +coolie's +coolies +cooling +coolly +coolness +coolness's +cools +coon +coon's +coons +coop +coop's +cooped +cooper +cooper's +cooperate +cooperated +cooperates +cooperating +cooperation +cooperation's +cooperative +cooperative's +cooperatively +cooperatives +coopered +coopering +coopers +cooping +coops +coordinate +coordinate's +coordinated +coordinates +coordinating +coordination +coordination's +coordinator +coordinator's +coordinators +coos +coot +coot's +cootie +cootie's +cooties +coots +cop +cop's +cope +cope's +copeck +copeck's +copecks +coped +copes +copied +copier +copier's +copiers +copies +copilot +copilot's +copilots +coping +coping's +copings +copious +copiously +copped +copper +copper's +copperhead +copperhead's +copperheads +coppers +coppery +coppice +coppice's +coppices +copping +copra +copra's +cops +copse +copse's +copses +copter +copter's +copters +copula +copula's +copulae +copulas +copulate +copulated +copulates +copulating +copulation +copulation's +copy +copy's +copycat +copycat's +copycats +copycatted +copycatting +copying +copyright +copyright's +copyrighted +copyrighting +copyrights +copywriter +copywriter's +copywriters +coquette +coquette's +coquetted +coquettes +coquetting +coquettish +coral +coral's +corals +cord +cord's +corded +cordial +cordial's +cordiality +cordiality's +cordially +cordials +cording +cordite +cordite's +cordless +cordon +cordon's +cordoned +cordoning +cordons +cords +corduroy +corduroy's +corduroys +corduroys's +core +core's +cored +cores +corespondent +corespondent's +corespondents +coriander +coriander's +coring +cork +cork's +corked +corking +corks +corkscrew +corkscrew's +corkscrewed +corkscrewing +corkscrews +corm +corm's +cormorant +cormorant's +cormorants +corms +corn +corn's +cornball +cornball's +cornballs +cornbread +cornbread's +corncob +corncob's +corncobs +cornea +cornea's +corneal +corneas +corned +corner +corner's +cornered +cornering +corners +cornerstone +cornerstone's +cornerstones +cornet +cornet's +cornets +cornflakes +cornflakes's +cornflower +cornflower's +cornflowers +cornice +cornice's +cornices +cornier +corniest +corning +cornmeal +cornmeal's +cornrow +cornrow's +cornrowed +cornrowing +cornrows +corns +cornstalk +cornstalk's +cornstalks +cornstarch +cornstarch's +cornucopia +cornucopia's +cornucopias +corny +corolla +corolla's +corollaries +corollary +corollary's +corollas +corona +corona's +coronae +coronaries +coronary +coronary's +coronas +coronation +coronation's +coronations +coroner +coroner's +coroners +coronet +coronet's +coronets +corpora +corporal +corporal's +corporals +corporate +corporation +corporation's +corporations +corporeal +corps +corps's +corpse +corpse's +corpses +corpulence +corpulence's +corpulent +corpus +corpus's +corpuscle +corpuscle's +corpuscles +corpuses +corral +corral's +corralled +corralling +corrals +correct +correctable +corrected +correcter +correctest +correcting +correction +correction's +correctional +corrections +corrective +corrective's +correctives +correctly +correctness +correctness's +corrector +corrects +correlate +correlate's +correlated +correlates +correlating +correlation +correlation's +correlations +correlative +correlative's +correlatives +correspond +corresponded +correspondence +correspondence's +correspondences +correspondent +correspondent's +correspondents +corresponding +correspondingly +corresponds +corridor +corridor's +corridors +corroborate +corroborated +corroborates +corroborating +corroboration +corroboration's +corroborations +corroborative +corrode +corroded +corrodes +corroding +corrosion +corrosion's +corrosive +corrosive's +corrosives +corrugate +corrugated +corrugates +corrugating +corrugation +corrugation's +corrugations +corrupt +corrupted +corrupter +corruptest +corruptible +corrupting +corruption +corruption's +corruptions +corruptly +corruptness +corruptness's +corrupts +corsage +corsage's +corsages +corsair +corsair's +corsairs +corset +corset's +corseted +corseting +corsets +cortex +cortex's +cortexes +cortical +cortices +cortisone +cortisone's +cortège +cortège's +cortèges +coruscate +coruscated +coruscates +coruscating +cosier +cosies +cosiest +cosign +cosignatories +cosignatory +cosignatory's +cosigned +cosigner +cosigner's +cosigners +cosigning +cosigns +cosine +cosmetic +cosmetic's +cosmetically +cosmetics +cosmetologist +cosmetologist's +cosmetologists +cosmetology +cosmetology's +cosmic +cosmically +cosmogonies +cosmogony +cosmogony's +cosmological +cosmologies +cosmologist +cosmologist's +cosmologists +cosmology +cosmology's +cosmonaut +cosmonaut's +cosmonauts +cosmopolitan +cosmopolitan's +cosmopolitans +cosmos +cosmos's +cosmoses +cosplay +cosponsor +cosponsor's +cosponsored +cosponsoring +cosponsors +cost +cost's +costar +costar's +costarred +costarring +costars +costed +costing +costings +costlier +costliest +costliness +costliness's +costly +costs +costume +costume's +costumed +costumes +costuming +cosy +cosy's +cot +cot's +cote +cote's +coterie +coterie's +coteries +cotes +cotillion +cotillion's +cotillions +cots +cottage +cottage's +cottages +cotter +cotter's +cotters +cotton +cotton's +cottoned +cottoning +cottonmouth +cottonmouth's +cottonmouths +cottons +cottonseed +cottonseed's +cottonseeds +cottontail +cottontail's +cottontails +cottonwood +cottonwood's +cottonwoods +cotyledon +cotyledon's +cotyledons +couch +couch's +couched +couches +couching +cougar +cougar's +cougars +cough +cough's +coughed +coughing +coughs +could +could've +couldn't +council +council's +councillor +councillor's +councillors +councilman +councilman's +councilmen +councilor +councilor's +councilors +councils +councilwoman +councilwoman's +councilwomen +counsel +counsel's +counseled +counseling +counselings +counselled +counsellor +counsellor's +counsellors +counselor +counselor's +counselors +counsels +count +count's +countable +countably +countdown +countdown's +countdowns +counted +countenance +countenance's +countenanced +countenances +countenancing +counter +counter's +counteract +counteracted +counteracting +counteraction +counteraction's +counteractions +counteracts +counterattack +counterattack's +counterattacked +counterattacking +counterattacks +counterbalance +counterbalance's +counterbalanced +counterbalances +counterbalancing +counterclaim +counterclaim's +counterclaimed +counterclaiming +counterclaims +counterclockwise +counterculture +counterculture's +countered +counterespionage +counterespionage's +counterexample +counterexamples +counterfeit +counterfeit's +counterfeited +counterfeiter +counterfeiter's +counterfeiters +counterfeiting +counterfeits +countering +counterintelligence +counterintelligence's +countermand +countermand's +countermanded +countermanding +countermands +counteroffer +counteroffer's +counteroffers +counterpane +counterpane's +counterpanes +counterpart +counterpart's +counterparts +counterpoint +counterpoint's +counterpoints +counterproductive +counterrevolution +counterrevolution's +counterrevolutionaries +counterrevolutionary +counterrevolutionary's +counterrevolutions +counters +countersank +countersign +countersign's +countersigned +countersigning +countersigns +countersink +countersink's +countersinking +countersinks +countersunk +countertenor +countertenor's +countertenors +counterweight +counterweight's +counterweights +countess +countess's +countesses +counties +counting +countless +countries +countrified +country +country's +countryman +countryman's +countrymen +countryside +countryside's +countrysides +countrywoman +countrywoman's +countrywomen +counts +county +county's +coup +coup's +coupe +coupe's +coupes +couple +couple's +coupled +couples +couplet +couplet's +couplets +coupling +coupling's +couplings +coupon +coupon's +coupons +coups +courage +courage's +courageous +courageously +courier +courier's +couriers +course +course's +coursed +courser +courses +coursing +court +court's +courted +courteous +courteously +courteousness +courteousness's +courtesan +courtesan's +courtesans +courtesies +courtesy +courtesy's +courthouse +courthouse's +courthouses +courtier +courtier's +courtiers +courting +courtlier +courtliest +courtliness +courtliness's +courtly +courtroom +courtroom's +courtrooms +courts +courtship +courtship's +courtships +courtyard +courtyard's +courtyards +cousin +cousin's +cousins +cove +cove's +coven +coven's +covenant +covenant's +covenanted +covenanting +covenants +covens +cover +cover's +coverage +coverage's +coverall +coverall's +coveralls +covered +covering +covering's +coverings +coverlet +coverlet's +coverlets +covers +covert +covert's +covertly +coverts +coves +covet +coveted +coveting +covetous +covetously +covetousness +covetousness's +covets +covey +covey's +coveys +cow +cow's +coward +coward's +cowardice +cowardice's +cowardliness +cowardliness's +cowardly +cowards +cowbird +cowbird's +cowbirds +cowboy +cowboy's +cowboys +cowed +cower +cowered +cowering +cowers +cowgirl +cowgirl's +cowgirls +cowhand +cowhand's +cowhands +cowhide +cowhide's +cowhides +cowing +cowl +cowl's +cowlick +cowlick's +cowlicks +cowling +cowling's +cowlings +cowls +coworker +coworker's +coworkers +cowpoke +cowpoke's +cowpokes +cowpox +cowpox's +cowpuncher +cowpuncher's +cowpunchers +cows +cowslip +cowslip's +cowslips +cox +coxcomb +coxcomb's +coxcombs +coxswain +coxswain's +coxswains +coy +coyer +coyest +coyly +coyness +coyness's +coyote +coyote's +coyotes +cozen +cozened +cozening +cozens +cozier +cozies +coziest +cozily +coziness +coziness's +cozy +cozy's +crab +crab's +crabbed +crabbier +crabbiest +crabbily +crabbiness +crabbiness's +crabbing +crabby +crabs +crack +crack's +crackdown +crackdown's +crackdowns +cracked +cracker +cracker's +crackerjack +crackerjack's +crackerjacks +crackers +cracking +crackle +crackle's +crackled +crackles +crackling +crackly +crackpot +crackpot's +crackpots +cracks +crackup +crackup's +crackups +cradle +cradle's +cradled +cradles +cradling +craft +craft's +crafted +craftier +craftiest +craftily +craftiness +craftiness's +crafting +crafts +craftsman +craftsman's +craftsmanship +craftsmanship's +craftsmen +crafty +crag +crag's +craggier +craggiest +craggy +crags +cram +crammed +cramming +cramp +cramp's +cramped +cramping +cramps +crams +cranberries +cranberry +cranberry's +crane +crane's +craned +cranes +crania +cranial +craning +cranium +cranium's +craniums +crank +crank's +crankcase +crankcase's +crankcases +cranked +crankier +crankiest +crankiness +crankiness's +cranking +cranks +crankshaft +crankshaft's +crankshafts +cranky +crannies +cranny +cranny's +crap +crap's +crape +crape's +crapes +crapped +crappier +crappiest +crapping +crappy +craps +craps's +crash +crash's +crashed +crashes +crashing +crass +crasser +crassest +crassly +crassness +crassness's +crate +crate's +crated +crater +crater's +cratered +cratering +craters +crates +crating +cravat +cravat's +cravats +crave +craved +craven +craven's +cravenly +cravens +craves +craving +craving's +cravings +craw +craw's +crawfish +crawfish's +crawfishes +crawl +crawl's +crawled +crawling +crawls +crawlspace +crawlspace's +crawlspaces +craws +crayfish +crayfish's +crayfishes +crayon +crayon's +crayoned +crayoning +crayons +craze +craze's +crazed +crazes +crazier +crazies +craziest +crazily +craziness +craziness's +crazing +crazy +crazy's +creak +creak's +creaked +creakier +creakiest +creaking +creaks +creaky +cream +cream's +creamed +creamer +creamer's +creameries +creamers +creamery +creamery's +creamier +creamiest +creaminess +creaminess's +creaming +creams +creamy +crease +crease's +creased +creases +creasing +create +created +creates +creating +creation +creation's +creationism +creationism's +creations +creative +creative's +creatively +creativeness +creativeness's +creatives +creativity +creativity's +creator +creator's +creators +creature +creature's +creatures +credence +credence's +credential +credential's +credentials +credenza +credenza's +credenzas +credibility +credibility's +credible +credibly +credit +credit's +creditable +creditably +credited +crediting +creditor +creditor's +creditors +credits +credo +credo's +credos +credulity +credulity's +credulous +credulously +creed +creed's +creeds +creek +creek's +creeks +creel +creel's +creels +creep +creep's +creeper +creeper's +creepers +creepier +creepiest +creepily +creepiness +creepiness's +creeping +creeps +creepy +cremate +cremated +cremates +cremating +cremation +cremation's +cremations +crematoria +crematories +crematorium +crematorium's +crematoriums +crematory +crematory's +creole +creole's +creoles +creosote +creosote's +creosoted +creosotes +creosoting +crepe +crepe's +crepes +crept +crescendi +crescendo +crescendo's +crescendos +crescent +crescent's +crescents +cress +cress's +crest +crest's +crested +crestfallen +cresting +crests +cretin +cretin's +cretinous +cretins +crevasse +crevasse's +crevasses +crevice +crevice's +crevices +crew +crew's +crewed +crewing +crewman +crewman's +crewmen +crews +crib +crib's +cribbage +cribbage's +cribbed +cribbing +cribs +crick +crick's +cricked +cricket +cricket's +cricketer +cricketer's +cricketers +crickets +cricking +cricks +cried +crier +crier's +criers +cries +crime +crime's +crimes +criminal +criminal's +criminally +criminals +criminologist +criminologist's +criminologists +criminology +criminology's +crimp +crimp's +crimped +crimping +crimps +crimson +crimson's +crimsoned +crimsoning +crimsons +cringe +cringe's +cringed +cringes +cringing +crinkle +crinkle's +crinkled +crinkles +crinklier +crinkliest +crinkling +crinkly +crinoline +crinoline's +crinolines +cripple +cripple's +crippled +cripples +crippling +crises +crisis +crisis's +crisp +crisp's +crisped +crisper +crispest +crispier +crispiest +crisping +crisply +crispness +crispness's +crisps +crispy +crisscross +crisscross's +crisscrossed +crisscrosses +crisscrossing +criteria +criterion +criterion's +criterions +critic +critic's +critical +critically +criticism +criticism's +criticisms +criticize +criticized +criticizes +criticizing +critics +critique +critique's +critiqued +critiques +critiquing +critter +critter's +critters +croak +croak's +croaked +croaking +croaks +crochet +crochet's +crocheted +crocheting +crochets +croci +crock +crock's +crocked +crockery +crockery's +crocks +crocodile +crocodile's +crocodiles +crocus +crocus's +crocuses +crofts +croissant +croissant's +croissants +crone +crone's +crones +cronies +crony +crony's +crook +crook's +crooked +crookeder +crookedest +crookedly +crookedness +crookedness's +crooking +crooks +croon +croon's +crooned +crooner +crooner's +crooners +crooning +croons +crop +crop's +cropped +cropper +cropper's +croppers +cropping +crops +croquet +croquet's +croquette +croquette's +croquettes +crosier +crosier's +crosiers +cross +cross's +crossbar +crossbar's +crossbars +crossbeam +crossbeam's +crossbeams +crossbones +crossbones's +crossbow +crossbow's +crossbows +crossbred +crossbreed +crossbreed's +crossbreeding +crossbreeds +crosscheck +crosscheck's +crosschecked +crosschecking +crosschecks +crossed +crosser +crosses +crossest +crossfire +crossfire's +crossfires +crossing +crossing's +crossings +crossly +crossness +crossness's +crossover +crossover's +crossovers +crosspiece +crosspiece's +crosspieces +crossroad +crossroad's +crossroads +crossroads's +crosstown +crosswalk +crosswalk's +crosswalks +crossways +crosswise +crossword +crossword's +crosswords +crotch +crotch's +crotches +crotchet +crotchet's +crotchets +crotchety +crouch +crouch's +crouched +crouches +crouching +croup +croup's +croupier +croupier's +croupiers +croupiest +croupy +crow +crow's +crowbar +crowbar's +crowbars +crowd +crowd's +crowded +crowdfund +crowdfunded +crowdfunding +crowdfunds +crowding +crowds +crowed +crowing +crown +crown's +crowned +crowning +crowns +crows +crozier +crozier's +croziers +croûton +croûton's +croûtons +crucial +crucially +crucible +crucible's +crucibles +crucified +crucifies +crucifix +crucifix's +crucifixes +crucifixion +crucifixion's +crucifixions +cruciform +cruciform's +cruciforms +crucify +crucifying +crud +crud's +cruddier +cruddiest +cruddy +crude +crude's +crudely +crudeness +crudeness's +cruder +crudest +crudities +crudity +crudity's +crudités +crudités's +cruel +crueler +cruelest +crueller +cruellest +cruelly +cruelties +cruelty +cruelty's +cruet +cruet's +cruets +cruise +cruise's +cruised +cruiser +cruiser's +cruisers +cruises +cruising +cruller +cruller's +crullers +crumb +crumb's +crumbed +crumbier +crumbiest +crumbing +crumble +crumble's +crumbled +crumbles +crumblier +crumbliest +crumbling +crumbly +crumbs +crumby +crummier +crummiest +crummy +crumpet +crumpet's +crumpets +crumple +crumple's +crumpled +crumples +crumpling +crunch +crunch's +crunched +cruncher +crunches +crunchier +crunchiest +crunching +crunchy +crusade +crusade's +crusaded +crusader +crusader's +crusaders +crusades +crusading +crush +crush's +crushed +crushes +crushing +crust +crust's +crustacean +crustacean's +crustaceans +crusted +crustier +crustiest +crusting +crusts +crusty +crutch +crutch's +crutches +crux +crux's +cruxes +cry +cry's +crybabies +crybaby +crybaby's +crying +cryings +cryogenics +cryogenics's +crypt +crypt's +cryptic +cryptically +cryptogram +cryptogram's +cryptograms +cryptographer +cryptographer's +cryptographers +cryptography +cryptography's +crypts +crystal +crystal's +crystalize +crystalized +crystalizes +crystalizing +crystalline +crystallization +crystallization's +crystallize +crystallized +crystallizes +crystallizing +crystallographic +crystallography +crystals +crèche +crèche's +crèches +cs +cub +cub's +cubbyhole +cubbyhole's +cubbyholes +cube +cube's +cubed +cubes +cubic +cubical +cubicle +cubicle's +cubicles +cubing +cubism +cubism's +cubist +cubist's +cubists +cubit +cubit's +cubits +cubs +cuckold +cuckold's +cuckolded +cuckolding +cuckolds +cuckoo +cuckoo's +cuckoos +cucumber +cucumber's +cucumbers +cud +cud's +cuddle +cuddle's +cuddled +cuddles +cuddlier +cuddliest +cuddling +cuddly +cudgel +cudgel's +cudgeled +cudgeling +cudgelled +cudgelling +cudgels +cuds +cue +cue's +cued +cueing +cues +cuff +cuff's +cuffed +cuffing +cuffs +cuing +cuisine +cuisine's +cuisines +culinary +cull +cull's +culled +cullender +cullender's +cullenders +culling +culls +culminate +culminated +culminates +culminating +culmination +culmination's +culminations +culotte +culotte's +culottes +culpability +culpability's +culpable +culprit +culprit's +culprits +cult +cult's +cultivate +cultivated +cultivates +cultivating +cultivation +cultivation's +cultivator +cultivator's +cultivators +cults +cultural +culturally +culture +culture's +cultured +cultures +culturing +culvert +culvert's +culverts +cumbersome +cumin +cumin's +cummerbund +cummerbund's +cummerbunds +cumming +cumquat +cumquat's +cumquats +cums +cumulative +cumulatively +cumuli +cumulus +cumulus's +cuneiform +cuneiform's +cunnilingus +cunnilingus's +cunning +cunning's +cunninger +cunningest +cunningly +cunt +cunt's +cunts +cup +cup's +cupboard +cupboard's +cupboards +cupcake +cupcake's +cupcakes +cupful +cupful's +cupfuls +cupid +cupid's +cupidity +cupidity's +cupids +cupola +cupola's +cupolas +cupped +cupping +cups +cupsful +cur +cur's +curable +curacies +curacy +curacy's +curate +curate's +curates +curative +curative's +curatives +curator +curator's +curators +curb +curb's +curbed +curbing +curbs +curd +curd's +curdle +curdled +curdles +curdling +curds +cure +cure's +cured +curer +cures +curfew +curfew's +curfews +curie +curie's +curies +curing +curio +curio's +curios +curiosities +curiosity +curiosity's +curious +curiously +curl +curl's +curled +curler +curler's +curlers +curlew +curlew's +curlews +curlicue +curlicue's +curlicued +curlicues +curlicuing +curlier +curliest +curliness +curliness's +curling +curls +curly +curlycue +curlycue's +curlycues +curmudgeon +curmudgeon's +curmudgeons +currant +currant's +currants +currencies +currency +currency's +current +current's +currently +currents +curricula +curriculum +curriculum's +curriculums +curried +curries +curry +curry's +currycomb +currycomb's +currycombed +currycombing +currycombs +currying +curs +curse +curse's +cursed +curses +cursing +cursive +cursive's +cursor +cursor's +cursorily +cursors +cursory +curst +curt +curtail +curtailed +curtailing +curtailment +curtailment's +curtailments +curtails +curtain +curtain's +curtained +curtaining +curtains +curter +curtest +curtly +curtness +curtness's +curtsey +curtsey's +curtseyed +curtseying +curtseys +curtsied +curtsies +curtsy +curtsy's +curtsying +curvaceous +curvacious +curvature +curvature's +curvatures +curve +curve's +curved +curves +curvier +curviest +curving +curvy +cushier +cushiest +cushion +cushion's +cushioned +cushioning +cushions +cushy +cusp +cusp's +cuspid +cuspid's +cuspids +cusps +cuss +cuss's +cussed +cusses +cussing +custard +custard's +custards +custodial +custodian +custodian's +custodians +custody +custody's +custom +custom's +customarily +customary +customer +customer's +customers +customization +customize +customized +customizes +customizing +customs +cut +cut's +cutback +cutback's +cutbacks +cute +cutely +cuteness +cuteness's +cuter +cutesier +cutesiest +cutest +cutesy +cuticle +cuticle's +cuticles +cutlass +cutlass's +cutlasses +cutlery +cutlery's +cutlet +cutlet's +cutlets +cutoff +cutoff's +cutoffs +cutout +cutout's +cutouts +cuts +cutter +cutter's +cutters +cutthroat +cutthroat's +cutthroats +cutting +cutting's +cuttings +cuttlefish +cuttlefish's +cuttlefishes +cutup +cutup's +cutups +cyanide +cyanide's +cyberbullies +cyberbully +cyberbully's +cybernetic +cybernetics +cybernetics's +cyberpunk +cyberpunk's +cyberpunks +cybersex +cyberspace +cyberspace's +cyclamen +cyclamen's +cyclamens +cycle +cycle's +cycled +cycles +cyclic +cyclical +cyclically +cycling +cyclist +cyclist's +cyclists +cyclone +cyclone's +cyclones +cyclonic +cyclotron +cyclotron's +cyclotrons +cygnet +cygnet's +cygnets +cylinder +cylinder's +cylinders +cylindrical +cymbal +cymbal's +cymbals +cynic +cynic's +cynical +cynically +cynicism +cynicism's +cynics +cynosure +cynosure's +cynosures +cypher +cypher's +cypress +cypress's +cypresses +cyst +cyst's +cystic +cysts +cytology +cytology's +cytoplasm +cytoplasm's +czar +czar's +czarina +czarina's +czarinas +czars +d +d'Arezzo +d'Arezzo's +d'Estaing +d'Estaing's +dB +dab +dab's +dabbed +dabbing +dabble +dabbled +dabbler +dabbler's +dabblers +dabbles +dabbling +dabs +dacha +dacha's +dachas +dachshund +dachshund's +dachshunds +dactyl +dactyl's +dactylic +dactylic's +dactylics +dactyls +dad +dad's +daddies +daddy +daddy's +dado +dado's +dadoes +dados +dads +daemon +daemon's +daemons +daffier +daffiest +daffodil +daffodil's +daffodils +daffy +daft +dafter +daftest +dagger +dagger's +daggers +daguerreotype +daguerreotype's +daguerreotyped +daguerreotypes +daguerreotyping +dahlia +dahlia's +dahlias +dailies +daily +daily's +daintier +dainties +daintiest +daintily +daintiness +daintiness's +dainty +dainty's +daiquiri +daiquiri's +daiquiris +dairies +dairy +dairy's +dairying +dairying's +dairymaid +dairymaid's +dairymaids +dairyman +dairyman's +dairymen +dais +dais's +daises +daisies +daisy +daisy's +dale +dale's +dales +dalliance +dalliance's +dalliances +dallied +dallies +dally +dallying +dalmatian +dalmatian's +dalmatians +dam +dam's +damage +damage's +damaged +damages +damages's +damaging +damask +damask's +damasked +damasking +damasks +dame +dame's +dames +dammed +damming +damn +damn's +damnable +damnably +damnation +damnation's +damndest +damned +damnedest +damning +damns +damp +damp's +damped +dampen +dampened +dampening +dampens +damper +damper's +dampers +dampest +damping +damply +dampness +dampness's +damps +dams +damsel +damsel's +damsels +damson +damson's +damsons +dance +dance's +danced +dancer +dancer's +dancers +dances +dancing +dancing's +dandelion +dandelion's +dandelions +dander +dander's +dandier +dandies +dandiest +dandle +dandled +dandles +dandling +dandruff +dandruff's +dandy +dandy's +danger +danger's +dangerous +dangerously +dangers +dangle +dangled +dangles +dangling +dank +danker +dankest +dankly +dankness +dankness's +dapper +dapperer +dapperest +dapple +dapple's +dappled +dapples +dappling +dare +dare's +dared +daredevil +daredevil's +daredevils +dares +daring +daring's +daringly +dark +dark's +darken +darkened +darkening +darkens +darker +darkest +darkly +darkness +darkness's +darkroom +darkroom's +darkrooms +darling +darling's +darlings +darn +darn's +darned +darneder +darnedest +darning +darns +dart +dart's +dartboard +dartboard's +dartboards +darted +darting +darts +dash +dash's +dashboard +dashboard's +dashboards +dashed +dashes +dashiki +dashiki's +dashikis +dashing +dashingly +dastardly +data +database +database's +databases +datatype +date +date's +dated +dateline +dateline's +datelined +datelines +datelining +dates +dating +dative +dative's +datives +datum +datum's +daub +daub's +daubed +dauber +dauber's +daubers +daubing +daubs +daughter +daughter's +daughters +daunt +daunted +daunting +dauntless +dauntlessly +dauntlessness +dauntlessness's +daunts +dauphin +dauphin's +dauphins +davenport +davenport's +davenports +davit +davit's +davits +dawdle +dawdled +dawdler +dawdler's +dawdlers +dawdles +dawdling +dawn +dawn's +dawned +dawning +dawns +day +day's +daybed +daybed's +daybeds +daybreak +daybreak's +daydream +daydream's +daydreamed +daydreamer +daydreamer's +daydreamers +daydreaming +daydreams +daydreamt +daylight +daylight's +daylights +days +daytime +daytime's +daze +daze's +dazed +dazes +dazing +dazzle +dazzle's +dazzled +dazzles +dazzling +deacon +deacon's +deaconess +deaconess's +deaconesses +deacons +deactivate +deactivated +deactivates +deactivating +dead +dead's +deadbeat +deadbeat's +deadbeats +deadbolt +deadbolt's +deadbolts +deaden +deadened +deadening +deadens +deader +deadest +deadlier +deadliest +deadline +deadline's +deadlines +deadliness +deadliness's +deadlock +deadlock's +deadlocked +deadlocking +deadlocks +deadly +deadpan +deadpan's +deadpanned +deadpanning +deadpans +deadwood +deadwood's +deaf +deafen +deafened +deafening +deafens +deafer +deafest +deafness +deafness's +deal +deal's +dealer +dealer's +dealers +dealership +dealership's +dealerships +dealing +dealing's +dealings +deals +dealt +dean +dean's +deans +dear +dear's +dearer +dearest +dearly +dearness +dearness's +dears +dearth +dearth's +dearths +death +death's +deathbed +deathbed's +deathbeds +deathblow +deathblow's +deathblows +deathless +deathlike +deathly +deaths +deathtrap +deathtrap's +deathtraps +deaves +deb +deb's +debacle +debacle's +debacles +debar +debark +debarkation +debarkation's +debarked +debarking +debarks +debarment +debarment's +debarred +debarring +debars +debase +debased +debasement +debasement's +debasements +debases +debasing +debatable +debate +debate's +debated +debater +debater's +debaters +debates +debating +debauch +debauch's +debauched +debaucheries +debauchery +debauchery's +debauches +debauching +debenture +debenture's +debentures +debilitate +debilitated +debilitates +debilitating +debilitation +debilitation's +debilities +debility +debility's +debit +debit's +debited +debiting +debits +debonair +debonairly +debrief +debriefed +debriefing +debriefing's +debriefings +debriefs +debris +debris's +debs +debt +debt's +debtor +debtor's +debtors +debts +debug +debugged +debugger +debuggers +debugging +debugs +debunk +debunked +debunking +debunks +debut +debut's +debuted +debuting +debuts +decade +decade's +decadence +decadence's +decadent +decadent's +decadently +decadents +decades +decaf +decaf's +decaffeinate +decaffeinated +decaffeinates +decaffeinating +decal +decal's +decals +decamp +decamped +decamping +decamps +decant +decanted +decanter +decanter's +decanters +decanting +decants +decapitate +decapitated +decapitates +decapitating +decapitation +decapitation's +decapitations +decathlon +decathlon's +decathlons +decay +decay's +decayed +decaying +decays +decease +decease's +deceased +deceased's +deceases +deceasing +decedent +decedent's +decedents +deceit +deceit's +deceitful +deceitfully +deceitfulness +deceitfulness's +deceits +deceive +deceived +deceiver +deceiver's +deceivers +deceives +deceiving +decelerate +decelerated +decelerates +decelerating +deceleration +deceleration's +decencies +decency +decency's +decent +decently +decentralization +decentralization's +decentralize +decentralized +decentralizes +decentralizing +deception +deception's +deceptions +deceptive +deceptively +deceptiveness +deceptiveness's +decibel +decibel's +decibels +decide +decided +decidedly +decides +deciding +deciduous +decimal +decimal's +decimals +decimate +decimated +decimates +decimating +decimation +decimation's +decipher +decipherable +deciphered +deciphering +deciphers +decision +decision's +decisions +decisive +decisively +decisiveness +decisiveness's +deck +deck's +decked +deckhand +deckhand's +deckhands +decking +decks +declaim +declaimed +declaiming +declaims +declamation +declamation's +declamations +declamatory +declaration +declaration's +declarations +declarative +declare +declared +declares +declaring +declassified +declassifies +declassify +declassifying +declension +declension's +declensions +declination +declination's +decline +decline's +declined +declines +declining +declivities +declivity +declivity's +decode +decoded +decoder +decodes +decoding +decolonization +decolonization's +decolonize +decolonized +decolonizes +decolonizing +decommission +decommissioned +decommissioning +decommissions +decompose +decomposed +decomposes +decomposing +decomposition +decomposition's +decompress +decompressed +decompresses +decompressing +decompression +decompression's +decongestant +decongestant's +decongestants +deconstruction +deconstruction's +deconstructions +decontaminate +decontaminated +decontaminates +decontaminating +decontamination +decontamination's +decor +decor's +decorate +decorated +decorates +decorating +decoration +decoration's +decorations +decorative +decorator +decorator's +decorators +decorous +decorously +decors +decorum +decorum's +decoy +decoy's +decoyed +decoying +decoys +decrease +decrease's +decreased +decreases +decreasing +decree +decree's +decreed +decreeing +decrees +decremented +decrements +decrepit +decrepitude +decrepitude's +decrescendi +decrescendo +decrescendo's +decrescendos +decried +decries +decriminalization +decriminalization's +decriminalize +decriminalized +decriminalizes +decriminalizing +decry +decrying +decryption +dedicate +dedicated +dedicates +dedicating +dedication +dedication's +dedications +deduce +deduced +deduces +deducible +deducing +deduct +deducted +deductible +deductible's +deductibles +deducting +deduction +deduction's +deductions +deductive +deducts +deed +deed's +deeded +deeding +deeds +deejay +deejay's +deejays +deem +deemed +deeming +deems +deep +deep's +deepen +deepened +deepening +deepens +deeper +deepest +deeply +deepness +deepness's +deeps +deer +deer's +deers +deerskin +deerskin's +deescalate +deescalated +deescalates +deescalating +deface +defaced +defacement +defacement's +defaces +defacing +defamation +defamation's +defamatory +defame +defamed +defames +defaming +default +default's +defaulted +defaulter +defaulter's +defaulters +defaulting +defaults +defeat +defeat's +defeated +defeating +defeatism +defeatism's +defeatist +defeatist's +defeatists +defeats +defecate +defecated +defecates +defecating +defecation +defecation's +defect +defect's +defected +defecting +defection +defection's +defections +defective +defective's +defectives +defector +defector's +defectors +defects +defend +defendant +defendant's +defendants +defended +defender +defender's +defenders +defending +defends +defense +defense's +defensed +defenseless +defenses +defensible +defensing +defensive +defensive's +defensively +defensiveness +defensiveness's +defer +deference +deference's +deferential +deferentially +deferment +deferment's +deferments +deferred +deferring +defers +defiance +defiance's +defiant +defiantly +deficiencies +deficiency +deficiency's +deficient +deficit +deficit's +deficits +defied +defies +defile +defile's +defiled +defilement +defilement's +defiles +defiling +definable +define +defined +definer +definer's +definers +defines +defining +definite +definitely +definiteness +definiteness's +definition +definition's +definitions +definitive +definitively +deflate +deflated +deflates +deflating +deflation +deflation's +deflect +deflected +deflecting +deflection +deflection's +deflections +deflector +deflector's +deflectors +deflects +defogger +defogger's +defoggers +defoliant +defoliant's +defoliants +defoliate +defoliated +defoliates +defoliating +defoliation +defoliation's +deforest +deforestation +deforestation's +deforested +deforesting +deforests +deform +deformation +deformation's +deformations +deformed +deforming +deformities +deformity +deformity's +deforms +defraud +defrauded +defrauding +defrauds +defray +defrayal +defrayal's +defrayed +defraying +defrays +defrost +defrosted +defroster +defroster's +defrosters +defrosting +defrosts +deft +defter +deftest +deftly +deftness +deftness's +defunct +defuse +defused +defuses +defusing +defy +defying +degeneracy +degeneracy's +degenerate +degenerate's +degenerated +degenerates +degenerating +degeneration +degeneration's +degenerative +degradation +degradation's +degrade +degraded +degrades +degrading +degree +degree's +degrees +dehumanization +dehumanization's +dehumanize +dehumanized +dehumanizes +dehumanizing +dehumidified +dehumidifier +dehumidifier's +dehumidifiers +dehumidifies +dehumidify +dehumidifying +dehydrate +dehydrated +dehydrates +dehydrating +dehydration +dehydration's +deice +deiced +deicer +deicer's +deicers +deices +deicing +deification +deification's +deified +deifies +deify +deifying +deign +deigned +deigning +deigns +deism +deism's +deities +deity +deity's +deject +dejected +dejectedly +dejecting +dejection +dejection's +dejects +delay +delay's +delayed +delaying +delays +delectable +delectation +delectation's +delegate +delegate's +delegated +delegates +delegating +delegation +delegation's +delegations +delete +deleted +deleterious +deletes +deleting +deletion +deletion's +deletions +deleverage +deleveraged +deleverages +deleveraging +deli +deli's +deliberate +deliberated +deliberately +deliberates +deliberating +deliberation +deliberation's +deliberations +delicacies +delicacy +delicacy's +delicate +delicately +delicatessen +delicatessen's +delicatessens +delicious +deliciously +deliciousness +deliciousness's +delight +delight's +delighted +delightful +delightfully +delighting +delights +delimit +delimited +delimiter +delimiters +delimiting +delimits +delineate +delineated +delineates +delineating +delineation +delineation's +delineations +delinquencies +delinquency +delinquency's +delinquent +delinquent's +delinquently +delinquents +deliquescent +deliria +delirious +deliriously +delirium +delirium's +deliriums +delis +deliver +deliverance +deliverance's +delivered +deliverer +deliverer's +deliverers +deliveries +delivering +delivers +delivery +delivery's +dell +dell's +dells +delphinia +delphinium +delphinium's +delphiniums +delta +delta's +deltas +delude +deluded +deludes +deluding +deluge +deluge's +deluged +deluges +deluging +delusion +delusion's +delusions +delusive +deluxe +delve +delved +delves +delving +demagnetization +demagnetization's +demagnetize +demagnetized +demagnetizes +demagnetizing +demagog +demagog's +demagogic +demagogry +demagogs +demagogue +demagogue's +demagoguery +demagoguery's +demagogues +demagogy +demagogy's +demand +demand's +demanded +demanding +demands +demarcate +demarcated +demarcates +demarcating +demarcation +demarcation's +demean +demeaned +demeaning +demeanor +demeanor's +demeans +demented +dementedly +dementia +dementia's +demerit +demerit's +demerits +demesne +demesne's +demesnes +demigod +demigod's +demigods +demijohn +demijohn's +demijohns +demilitarization +demilitarization's +demilitarize +demilitarized +demilitarizes +demilitarizing +demise +demise's +demised +demises +demising +demitasse +demitasse's +demitasses +demo +demo's +demobilization +demobilization's +demobilize +demobilized +demobilizes +demobilizing +democracies +democracy +democracy's +democrat +democrat's +democratic +democratically +democratization +democratization's +democratize +democratized +democratizes +democratizing +democrats +demoed +demographer +demographer's +demographers +demographic +demographic's +demographically +demographics +demographics's +demography +demography's +demoing +demolish +demolished +demolishes +demolishing +demolition +demolition's +demolitions +demon +demon's +demoniac +demoniacal +demonic +demons +demonstrable +demonstrably +demonstrate +demonstrated +demonstrates +demonstrating +demonstration +demonstration's +demonstrations +demonstrative +demonstrative's +demonstratively +demonstratives +demonstrator +demonstrator's +demonstrators +demoralization +demoralization's +demoralize +demoralized +demoralizes +demoralizing +demos +demote +demoted +demotes +demoting +demotion +demotion's +demotions +demount +demur +demur's +demure +demurely +demurer +demurest +demurred +demurring +demurs +den +den's +denature +denatured +denatures +denaturing +dendrite +dendrite's +dendrites +deniability +denial +denial's +denials +denied +denier +denier's +deniers +denies +denigrate +denigrated +denigrates +denigrating +denigration +denigration's +denim +denim's +denims +denizen +denizen's +denizens +denominate +denominated +denominates +denominating +denomination +denomination's +denominational +denominations +denominator +denominator's +denominators +denotation +denotation's +denotations +denote +denoted +denotes +denoting +denouement +denouement's +denouements +denounce +denounced +denouncement +denouncement's +denouncements +denounces +denouncing +dens +dense +densely +denseness +denseness's +denser +densest +densities +density +density's +dent +dent's +dental +dented +dentifrice +dentifrice's +dentifrices +dentin +dentin's +dentine +dentine's +denting +dentist +dentist's +dentistry +dentistry's +dentists +dents +denture +denture's +dentures +denude +denuded +denudes +denuding +denunciation +denunciation's +denunciations +deny +denying +deodorant +deodorant's +deodorants +deodorize +deodorized +deodorizer +deodorizer's +deodorizers +deodorizes +deodorizing +depart +departed +departed's +departing +department +department's +departmental +departmentalize +departmentalized +departmentalizes +departmentalizing +departments +departs +departure +departure's +departures +depend +dependability +dependability's +dependable +dependably +dependance +dependance's +dependant +dependant's +dependants +depended +dependence +dependence's +dependencies +dependency +dependency's +dependent +dependent's +dependents +depending +depends +depict +depicted +depicting +depiction +depiction's +depictions +depicts +depilatories +depilatory +depilatory's +deplane +deplaned +deplanes +deplaning +deplete +depleted +depletes +depleting +depletion +depletion's +deplorable +deplorably +deplore +deplored +deplores +deploring +deploy +deployed +deploying +deployment +deployment's +deployments +deploys +depoliticize +depoliticized +depoliticizes +depoliticizing +depopulate +depopulated +depopulates +depopulating +depopulation +depopulation's +deport +deportation +deportation's +deportations +deported +deporting +deportment +deportment's +deports +depose +deposed +deposes +deposing +deposit +deposit's +deposited +depositing +deposition +deposition's +depositions +depositor +depositor's +depositories +depositors +depository +depository's +deposits +depot +depot's +depots +deprave +depraved +depraves +depraving +depravities +depravity +depravity's +deprecate +deprecated +deprecates +deprecating +deprecation +deprecation's +deprecatory +depreciate +depreciated +depreciates +depreciating +depreciation +depreciation's +depredation +depredation's +depredations +depress +depressant +depressant's +depressants +depressed +depresses +depressing +depressingly +depression +depression's +depressions +depressive +depressive's +depressives +deprivation +deprivation's +deprivations +deprive +deprived +deprives +depriving +deprogram +deprogramed +deprograming +deprogrammed +deprogramming +deprograms +depth +depth's +depths +deputation +deputation's +deputations +depute +deputed +deputes +deputies +deputing +deputize +deputized +deputizes +deputizing +deputy +deputy's +derail +derailed +derailing +derailment +derailment's +derailments +derails +derange +deranged +derangement +derangement's +deranges +deranging +derbies +derby +derby's +deregulate +deregulated +deregulates +deregulating +deregulation +deregulation's +derelict +derelict's +dereliction +dereliction's +derelicts +deride +derided +derides +deriding +derision +derision's +derisive +derisively +derisory +derivable +derivation +derivation's +derivations +derivative +derivative's +derivatives +derive +derived +derives +deriving +dermatitis +dermatitis's +dermatologist +dermatologist's +dermatologists +dermatology +dermatology's +dermis +dermis's +derogate +derogated +derogates +derogating +derogation +derogation's +derogatory +derrick +derrick's +derricks +derringer +derringer's +derringers +derrière +derrière's +derrières +dervish +dervish's +dervishes +desalinate +desalinated +desalinates +desalinating +desalination +desalination's +descant +descant's +descanted +descanting +descants +descend +descendant +descendant's +descendants +descended +descendent +descendent's +descendents +descender +descending +descends +descent +descent's +descents +describable +describe +described +describes +describing +descried +descries +description +description's +descriptions +descriptive +descriptively +descriptor +descriptors +descry +descrying +desecrate +desecrated +desecrates +desecrating +desecration +desecration's +desegregate +desegregated +desegregates +desegregating +desegregation +desegregation's +desensitization +desensitization's +desensitize +desensitized +desensitizes +desensitizing +desert +desert's +deserted +deserter +deserter's +deserters +deserting +desertion +desertion's +desertions +deserts +deserve +deserved +deservedly +deserves +deserving +desiccate +desiccated +desiccates +desiccating +desiccation +desiccation's +desiderata +desideratum +desideratum's +design +design's +designate +designated +designates +designating +designation +designation's +designations +designed +designer +designer's +designers +designing +designing's +designs +desirability +desirability's +desirable +desirably +desire +desire's +desired +desires +desiring +desirous +desist +desisted +desisting +desists +desk +desk's +desks +desktop +desktop's +desktops +desolate +desolated +desolately +desolateness +desolateness's +desolates +desolating +desolation +desolation's +despair +despair's +despaired +despairing +despairingly +despairs +despatch +despatch's +despatched +despatches +despatching +desperado +desperado's +desperadoes +desperados +desperate +desperately +desperation +desperation's +despicable +despicably +despise +despised +despises +despising +despite +despoil +despoiled +despoiling +despoils +despondency +despondency's +despondent +despondently +despot +despot's +despotic +despotism +despotism's +despots +dessert +dessert's +desserts +destabilize +destination +destination's +destinations +destine +destined +destines +destinies +destining +destiny +destiny's +destitute +destitution +destitution's +destroy +destroyed +destroyer +destroyer's +destroyers +destroying +destroys +destruct +destruct's +destructed +destructible +destructing +destruction +destruction's +destructive +destructively +destructiveness +destructiveness's +destructs +desultory +detach +detachable +detached +detaches +detaching +detachment +detachment's +detachments +detail +detail's +detailed +detailing +details +detain +detained +detainee +detainee's +detainees +detaining +detainment +detainment's +detains +detect +detectable +detected +detecting +detection +detection's +detective +detective's +detectives +detector +detector's +detectors +detects +detentes +detention +detention's +detentions +deter +detergent +detergent's +detergents +deteriorate +deteriorated +deteriorates +deteriorating +deterioration +deterioration's +determinable +determinant +determinant's +determinants +determinate +determination +determination's +determinations +determine +determined +determiner +determiner's +determiners +determines +determining +determinism +deterministic +deterred +deterrence +deterrence's +deterrent +deterrent's +deterrents +deterring +deters +detest +detestable +detestation +detestation's +detested +detesting +detests +dethrone +dethroned +dethronement +dethronement's +dethrones +dethroning +detonate +detonated +detonates +detonating +detonation +detonation's +detonations +detonator +detonator's +detonators +detour +detour's +detoured +detouring +detours +detox +detox's +detoxed +detoxes +detoxification +detoxification's +detoxified +detoxifies +detoxify +detoxifying +detoxing +detract +detracted +detracting +detraction +detraction's +detractor +detractor's +detractors +detracts +detriment +detriment's +detrimental +detriments +detritus +detritus's +deuce +deuce's +deuces +deuterium +deuterium's +devaluation +devaluation's +devaluations +devalue +devalued +devalues +devaluing +devastate +devastated +devastates +devastating +devastation +devastation's +develop +developed +developer +developer's +developers +developing +development +development's +developmental +developments +develops +deviance +deviance's +deviant +deviant's +deviants +deviate +deviate's +deviated +deviates +deviating +deviation +deviation's +deviations +device +device's +devices +devil +devil's +deviled +deviling +devilish +devilishly +devilled +devilling +devilment +devilment's +devilries +devilry +devilry's +devils +deviltries +deviltry +deviltry's +devious +deviously +deviousness +deviousness's +devise +devise's +devised +devises +devising +devoid +devolution +devolve +devolved +devolves +devolving +devote +devoted +devotedly +devotee +devotee's +devotees +devotes +devoting +devotion +devotion's +devotional +devotional's +devotionals +devotions +devour +devoured +devouring +devours +devout +devouter +devoutest +devoutly +devoutness +devoutness's +dew +dew's +dewberries +dewberry +dewberry's +dewdrop +dewdrop's +dewdrops +dewier +dewiest +dewlap +dewlap's +dewlaps +dewy +dexterity +dexterity's +dexterous +dexterously +dextrose +dextrose's +dextrous +dextrously +dharma +dhoti +dhoti's +dhotis +diabetes +diabetes's +diabetic +diabetic's +diabetics +diabolic +diabolical +diabolically +diacritic +diacritic's +diacritical +diacritics +diadem +diadem's +diadems +diagnose +diagnosed +diagnoses +diagnosing +diagnosis +diagnosis's +diagnostic +diagnostician +diagnostician's +diagnosticians +diagnostics +diagonal +diagonal's +diagonally +diagonals +diagram +diagram's +diagramed +diagraming +diagrammatic +diagrammed +diagramming +diagrams +dial +dial's +dialect +dialect's +dialectal +dialectic +dialectic's +dialects +dialed +dialing +dialings +dialog +dialog's +dialogs +dialogue +dialogue's +dialogues +dials +dialyses +dialysis +dialysis's +dialyzes +diameter +diameter's +diameters +diametrical +diametrically +diamond +diamond's +diamonds +diaper +diaper's +diapered +diapering +diapers +diaphanous +diaphragm +diaphragm's +diaphragms +diaries +diarist +diarist's +diarists +diarrhea +diarrhea's +diarrhoea +diarrhoea's +diary +diary's +diastolic +diatom +diatom's +diatoms +diatribe +diatribe's +diatribes +dibble +dibble's +dibbled +dibbles +dibbling +dice +diced +dices +dicey +dichotomies +dichotomy +dichotomy's +dicier +diciest +dicing +dick +dick's +dicker +dickered +dickering +dickers +dickey +dickey's +dickeys +dickie +dickie's +dickies +dicks +dicky +dicky's +dicta +dictate +dictate's +dictated +dictates +dictating +dictation +dictation's +dictations +dictator +dictator's +dictatorial +dictators +dictatorship +dictatorship's +dictatorships +diction +diction's +dictionaries +dictionary +dictionary's +dictum +dictum's +dictums +did +didactic +diddle +diddled +diddles +diddling +didn't +die +die's +died +diehard +diehard's +diehards +diereses +dieresis +dieresis's +dies +diesel +diesel's +dieseled +dieseling +diesels +diet +diet's +dietaries +dietary +dietary's +dieted +dieter +dieter's +dieters +dietetic +dietetics +dietetics's +dietician +dietician's +dieticians +dieting +dietitian +dietitian's +dietitians +diets +differ +differed +difference +difference's +differences +different +differential +differential's +differentials +differentiate +differentiated +differentiates +differentiating +differentiation +differentiation's +differently +differing +differs +difficult +difficulties +difficulty +difficulty's +diffidence +diffidence's +diffident +diffidently +diffraction +diffraction's +diffuse +diffused +diffusely +diffuseness +diffuseness's +diffuses +diffusing +diffusion +diffusion's +dig +dig's +digest +digest's +digested +digestible +digesting +digestion +digestion's +digestions +digestive +digests +digger +digger's +diggers +digging +digit +digit's +digital +digitalis +digitalis's +digitally +digitization +digitize +digitized +digitizes +digitizing +digits +dignified +dignifies +dignify +dignifying +dignitaries +dignitary +dignitary's +dignities +dignity +dignity's +digraph +digraph's +digraphs +digress +digressed +digresses +digressing +digression +digression's +digressions +digressive +digs +dike +dike's +diked +dikes +diking +dilapidated +dilapidation +dilapidation's +dilate +dilated +dilates +dilating +dilation +dilation's +dilatory +dilemma +dilemma's +dilemmas +dilettante +dilettante's +dilettantes +dilettanti +dilettantism +dilettantism's +diligence +diligence's +diligent +diligently +dill +dill's +dillies +dills +dilly +dilly's +dillydallied +dillydallies +dillydally +dillydallying +dilute +diluted +dilutes +diluting +dilution +dilution's +dim +dime +dime's +dimension +dimension's +dimensional +dimensionless +dimensions +dimer +dimes +diminish +diminished +diminishes +diminishing +diminuendo +diminuendo's +diminuendoes +diminuendos +diminution +diminution's +diminutions +diminutive +diminutive's +diminutives +dimly +dimmed +dimmer +dimmer's +dimmers +dimmest +dimming +dimness +dimness's +dimple +dimple's +dimpled +dimples +dimpling +dims +dimwit +dimwit's +dimwits +dimwitted +din +din's +dine +dined +diner +diner's +diners +dines +dinette +dinette's +dinettes +ding +ding's +dinged +dinghies +dinghy +dinghy's +dingier +dingiest +dinginess +dinginess's +dinging +dingo +dingo's +dingoes +dings +dingy +dining +dinkier +dinkies +dinkiest +dinky +dinky's +dinned +dinner +dinner's +dinnered +dinnering +dinners +dinning +dinosaur +dinosaur's +dinosaurs +dins +dint +dint's +diocesan +diocesan's +diocesans +diocese +diocese's +dioceses +diode +diode's +diodes +diorama +diorama's +dioramas +dioxide +dioxin +dioxin's +dioxins +dip +dip's +diphtheria +diphtheria's +diphthong +diphthong's +diphthongs +diploma +diploma's +diplomacy +diplomacy's +diplomas +diplomat +diplomat's +diplomata +diplomatic +diplomatically +diplomats +dipole +dipped +dipper +dipper's +dippers +dipping +dips +dipsomania +dipsomania's +dipsomaniac +dipsomaniac's +dipsomaniacs +dipstick +dipstick's +dipsticks +dire +direct +directed +directer +directest +directing +direction +direction's +directional +directions +directive +directive's +directives +directly +directness +directness's +director +director's +directorate +directorate's +directorates +directorial +directories +directors +directorship +directorship's +directorships +directory +directory's +directs +direr +direst +dirge +dirge's +dirges +dirigible +dirigible's +dirigibles +dirk +dirk's +dirks +dirt +dirt's +dirtied +dirtier +dirties +dirtiest +dirtiness +dirtiness's +dirty +dirtying +dis +dis's +disabilities +disability +disability's +disable +disabled +disablement +disablement's +disables +disabling +disabuse +disabused +disabuses +disabusing +disadvantage +disadvantage's +disadvantaged +disadvantageous +disadvantageously +disadvantages +disadvantaging +disaffect +disaffected +disaffecting +disaffection +disaffection's +disaffects +disagree +disagreeable +disagreeably +disagreed +disagreeing +disagreement +disagreement's +disagreements +disagrees +disallow +disallowed +disallowing +disallows +disambiguate +disambiguation +disappear +disappearance +disappearance's +disappearances +disappeared +disappearing +disappears +disappoint +disappointed +disappointing +disappointingly +disappointment +disappointment's +disappointments +disappoints +disapprobation +disapprobation's +disapproval +disapproval's +disapprove +disapproved +disapproves +disapproving +disapprovingly +disarm +disarmament +disarmament's +disarmed +disarming +disarms +disarrange +disarranged +disarrangement +disarrangement's +disarranges +disarranging +disarray +disarray's +disarrayed +disarraying +disarrays +disassemble +disassembled +disassembles +disassembling +disassociate +disassociated +disassociates +disassociating +disaster +disaster's +disasters +disastrous +disastrously +disavow +disavowal +disavowal's +disavowals +disavowed +disavowing +disavows +disband +disbanded +disbanding +disbands +disbar +disbarment +disbarment's +disbarred +disbarring +disbars +disbelief +disbelief's +disbelieve +disbelieved +disbelieves +disbelieving +disburse +disbursed +disbursement +disbursement's +disbursements +disburses +disbursing +disc +disc's +discard +discard's +discarded +discarding +discards +discern +discerned +discernible +discerning +discernment +discernment's +discerns +discharge +discharge's +discharged +discharges +discharging +disciple +disciple's +disciples +disciplinarian +disciplinarian's +disciplinarians +disciplinary +discipline +discipline's +disciplined +disciplines +disciplining +disclaim +disclaimed +disclaimer +disclaimer's +disclaimers +disclaiming +disclaims +disclose +disclosed +discloses +disclosing +disclosure +disclosure's +disclosures +disco +disco's +discoed +discoing +discolor +discoloration +discoloration's +discolorations +discolored +discoloring +discolors +discombobulate +discombobulated +discombobulates +discombobulating +discomfit +discomfited +discomfiting +discomfits +discomfiture +discomfiture's +discomfort +discomfort's +discomforted +discomforting +discomforts +discommode +discommoded +discommodes +discommoding +discompose +discomposed +discomposes +discomposing +discomposure +discomposure's +disconcert +disconcerted +disconcerting +disconcerts +disconnect +disconnected +disconnectedly +disconnecting +disconnection +disconnection's +disconnections +disconnects +disconsolate +disconsolately +discontent +discontent's +discontented +discontentedly +discontenting +discontentment +discontentment's +discontents +discontinuance +discontinuance's +discontinuances +discontinuation +discontinuation's +discontinuations +discontinue +discontinued +discontinues +discontinuing +discontinuities +discontinuity +discontinuity's +discontinuous +discord +discord's +discordant +discorded +discording +discords +discos +discotheque +discotheque's +discotheques +discount +discount's +discounted +discountenance +discountenanced +discountenances +discountenancing +discounting +discounts +discourage +discouraged +discouragement +discouragement's +discouragements +discourages +discouraging +discouragingly +discourse +discourse's +discoursed +discourses +discoursing +discourteous +discourteously +discourtesies +discourtesy +discourtesy's +discover +discovered +discoverer +discoverer's +discoverers +discoveries +discovering +discovers +discovery +discovery's +discredit +discredit's +discreditable +discredited +discrediting +discredits +discreet +discreeter +discreetest +discreetly +discrepancies +discrepancy +discrepancy's +discrete +discretion +discretion's +discretionary +discriminant +discriminate +discriminated +discriminates +discriminating +discrimination +discrimination's +discriminatory +discs +discursive +discus +discus's +discuses +discuss +discussant +discussant's +discussants +discussed +discusses +discussing +discussion +discussion's +discussions +disdain +disdain's +disdained +disdainful +disdainfully +disdaining +disdains +disease +disease's +diseased +diseases +disembark +disembarkation +disembarkation's +disembarked +disembarking +disembarks +disembodied +disembodies +disembody +disembodying +disembowel +disemboweled +disemboweling +disembowelled +disembowelling +disembowels +disenchant +disenchanted +disenchanting +disenchantment +disenchantment's +disenchants +disencumber +disencumbered +disencumbering +disencumbers +disenfranchise +disenfranchised +disenfranchisement +disenfranchisement's +disenfranchises +disenfranchising +disengage +disengaged +disengagement +disengagement's +disengagements +disengages +disengaging +disentangle +disentangled +disentanglement +disentanglement's +disentangles +disentangling +disestablish +disestablished +disestablishes +disestablishing +disfavor +disfavor's +disfavored +disfavoring +disfavors +disfigure +disfigured +disfigurement +disfigurement's +disfigurements +disfigures +disfiguring +disfranchise +disfranchised +disfranchisement +disfranchisement's +disfranchises +disfranchising +disgorge +disgorged +disgorges +disgorging +disgrace +disgrace's +disgraced +disgraceful +disgracefully +disgraces +disgracing +disgruntle +disgruntled +disgruntles +disgruntling +disguise +disguise's +disguised +disguises +disguising +disgust +disgust's +disgusted +disgustedly +disgusting +disgustingly +disgusts +dish +dish's +disharmonious +disharmony +disharmony's +dishcloth +dishcloth's +dishcloths +dishearten +disheartened +disheartening +disheartens +dished +dishes +dishevel +disheveled +disheveling +dishevelled +dishevelling +dishevels +dishing +dishonest +dishonestly +dishonesty +dishonesty's +dishonor +dishonor's +dishonorable +dishonorably +dishonored +dishonoring +dishonors +dishpan +dishpan's +dishpans +dishrag +dishrag's +dishrags +dishtowel +dishtowel's +dishtowels +dishwasher +dishwasher's +dishwashers +dishwater +dishwater's +disillusion +disillusion's +disillusioned +disillusioning +disillusionment +disillusionment's +disillusions +disincentive +disinclination +disinclination's +disincline +disinclined +disinclines +disinclining +disinfect +disinfectant +disinfectant's +disinfectants +disinfected +disinfecting +disinfects +disinformation +disinformation's +disingenuous +disinherit +disinherited +disinheriting +disinherits +disintegrate +disintegrated +disintegrates +disintegrating +disintegration +disintegration's +disinter +disinterest +disinterest's +disinterested +disinterestedly +disinterests +disinterment +disinterment's +disinterred +disinterring +disinters +disjoint +disjointed +disjointedly +disjointing +disjoints +disk +disk's +diskette +diskette's +diskettes +disks +dislike +dislike's +disliked +dislikes +disliking +dislocate +dislocated +dislocates +dislocating +dislocation +dislocation's +dislocations +dislodge +dislodged +dislodges +dislodging +disloyal +disloyally +disloyalty +disloyalty's +dismal +dismally +dismantle +dismantled +dismantles +dismantling +dismay +dismay's +dismayed +dismaying +dismays +dismember +dismembered +dismembering +dismemberment +dismemberment's +dismembers +dismiss +dismissal +dismissal's +dismissals +dismissed +dismisses +dismissing +dismissive +dismount +dismount's +dismounted +dismounting +dismounts +disobedience +disobedience's +disobedient +disobediently +disobey +disobeyed +disobeying +disobeys +disoblige +disobliged +disobliges +disobliging +disorder +disorder's +disordered +disordering +disorderliness +disorderliness's +disorderly +disorders +disorganization +disorganization's +disorganize +disorganized +disorganizes +disorganizing +disorient +disorientation +disorientation's +disoriented +disorienting +disorients +disown +disowned +disowning +disowns +disparage +disparaged +disparagement +disparagement's +disparages +disparaging +disparate +disparities +disparity +disparity's +dispassionate +dispassionately +dispatch +dispatch's +dispatched +dispatcher +dispatcher's +dispatchers +dispatches +dispatching +dispel +dispelled +dispelling +dispels +dispensable +dispensaries +dispensary +dispensary's +dispensation +dispensation's +dispensations +dispense +dispensed +dispenser +dispenser's +dispensers +dispenses +dispensing +dispersal +dispersal's +disperse +dispersed +disperses +dispersing +dispersion +dispersion's +dispirit +dispirited +dispiriting +dispirits +displace +displaced +displacement +displacement's +displacements +displaces +displacing +display +display's +displayable +displayed +displaying +displays +displease +displeased +displeases +displeasing +displeasure +displeasure's +disport +disported +disporting +disports +disposable +disposable's +disposables +disposal +disposal's +disposals +dispose +disposed +disposes +disposing +disposition +disposition's +dispositions +dispossess +dispossessed +dispossesses +dispossessing +dispossession +dispossession's +disproof +disproportion +disproportion's +disproportionate +disproportionately +disproportions +disprove +disproved +disproven +disproves +disproving +disputable +disputant +disputant's +disputants +disputation +disputation's +disputations +disputatious +dispute +dispute's +disputed +disputes +disputing +disqualification +disqualification's +disqualifications +disqualified +disqualifies +disqualify +disqualifying +disquiet +disquiet's +disquieted +disquieting +disquiets +disquisition +disquisition's +disquisitions +disregard +disregard's +disregarded +disregarding +disregards +disrepair +disrepair's +disreputable +disreputably +disrepute +disrepute's +disrespect +disrespect's +disrespected +disrespectful +disrespectfully +disrespecting +disrespects +disrobe +disrobed +disrobes +disrobing +disrupt +disrupted +disrupting +disruption +disruption's +disruptions +disruptive +disrupts +diss +diss's +dissatisfaction +dissatisfaction's +dissatisfied +dissatisfies +dissatisfy +dissatisfying +dissect +dissected +dissecting +dissection +dissection's +dissections +dissects +dissed +dissemble +dissembled +dissembles +dissembling +disseminate +disseminated +disseminates +disseminating +dissemination +dissemination's +dissension +dissension's +dissensions +dissent +dissent's +dissented +dissenter +dissenter's +dissenters +dissenting +dissents +dissertation +dissertation's +dissertations +disservice +disservice's +disservices +disses +dissidence +dissidence's +dissident +dissident's +dissidents +dissimilar +dissimilarities +dissimilarity +dissimilarity's +dissimulate +dissimulated +dissimulates +dissimulating +dissimulation +dissimulation's +dissing +dissipate +dissipated +dissipates +dissipating +dissipation +dissipation's +dissociate +dissociated +dissociates +dissociating +dissociation +dissociation's +dissolute +dissolutely +dissoluteness +dissoluteness's +dissolution +dissolution's +dissolve +dissolved +dissolves +dissolving +dissonance +dissonance's +dissonances +dissonant +dissuade +dissuaded +dissuades +dissuading +dissuasion +dissuasion's +distaff +distaff's +distaffs +distance +distance's +distanced +distances +distancing +distant +distantly +distaste +distaste's +distasteful +distastefully +distastes +distemper +distemper's +distend +distended +distending +distends +distension +distension's +distensions +distention +distention's +distentions +distil +distill +distillate +distillate's +distillates +distillation +distillation's +distillations +distilled +distiller +distiller's +distilleries +distillers +distillery +distillery's +distilling +distills +distils +distinct +distincter +distinctest +distinction +distinction's +distinctions +distinctive +distinctively +distinctiveness +distinctiveness's +distinctly +distinguish +distinguishable +distinguished +distinguishes +distinguishing +distort +distorted +distorter +distorting +distortion +distortion's +distortions +distorts +distract +distracted +distracting +distraction +distraction's +distractions +distracts +distrait +distraught +distress +distress's +distressed +distresses +distressful +distressing +distressingly +distribute +distributed +distributes +distributing +distribution +distribution's +distributions +distributive +distributor +distributor's +distributors +district +district's +districts +distrust +distrust's +distrusted +distrustful +distrustfully +distrusting +distrusts +disturb +disturbance +disturbance's +disturbances +disturbed +disturbing +disturbingly +disturbs +disunite +disunited +disunites +disuniting +disunity +disunity's +disuse +disuse's +disused +disuses +disusing +ditch +ditch's +ditched +ditches +ditching +dither +dither's +dithered +dithering +dithers +ditties +ditto +ditto's +dittoed +dittoes +dittoing +dittos +ditty +ditty's +diuretic +diuretic's +diuretics +diurnal +diurnally +diva +diva's +divan +divan's +divans +divas +dive +dive's +dived +diver +diver's +diverge +diverged +divergence +divergence's +divergences +divergent +diverges +diverging +divers +diverse +diversely +diversification +diversification's +diversified +diversifies +diversify +diversifying +diversion +diversion's +diversionary +diversions +diversities +diversity +diversity's +divert +diverted +diverting +diverts +dives +divest +divested +divesting +divests +divide +divide's +divided +dividend +dividend's +dividends +divider +divider's +dividers +divides +dividing +divination +divination's +divine +divine's +divined +divinely +diviner +diviner's +diviners +divines +divinest +diving +diving's +divining +divinities +divinity +divinity's +divisibility +divisibility's +divisible +division +division's +divisional +divisions +divisive +divisively +divisiveness +divisiveness's +divisor +divisor's +divisors +divorce +divorce's +divorced +divorces +divorcing +divorcée +divorcée's +divorcées +divot +divot's +divots +divulge +divulged +divulges +divulging +divvied +divvies +divvy +divvy's +divvying +dizzied +dizzier +dizzies +dizziest +dizzily +dizziness +dizziness's +dizzy +dizzying +djinn +djinn's +djinni +djinni's +djinns +do +do's +doable +doc +doc's +docent +docent's +docents +docile +docilely +docility +docility's +dock +dock's +docked +docket +docket's +docketed +docketing +dockets +docking +docks +dockyard +dockyard's +dockyards +docs +doctor +doctor's +doctoral +doctorate +doctorate's +doctorates +doctored +doctoring +doctors +doctrinaire +doctrinaire's +doctrinaires +doctrinal +doctrine +doctrine's +doctrines +docudrama +docudrama's +docudramas +document +document's +documentaries +documentary +documentary's +documentation +documentation's +documented +documenting +documents +dodder +dodder's +doddered +doddering +dodders +dodge +dodge's +dodged +dodger +dodger's +dodgers +dodges +dodging +dodo +dodo's +dodoes +dodos +doe +doe's +doer +doer's +doers +does +doesn't +doff +doffed +doffing +doffs +dog +dog's +dogcatcher +dogcatcher's +dogcatchers +dogfight +dogfight's +dogfights +dogfish +dogfish's +dogfishes +dogged +doggedly +doggedness +doggedness's +doggerel +doggerel's +doggie +doggie's +doggier +doggies +doggiest +dogging +doggone +doggoned +doggoneder +doggonedest +doggoner +doggones +doggonest +doggoning +doggy +doggy's +doghouse +doghouse's +doghouses +dogie +dogie's +dogies +dogma +dogma's +dogmas +dogmata +dogmatic +dogmatically +dogmatism +dogmatism's +dogmatist +dogmatist's +dogmatists +dogs +dogtrot +dogtrot's +dogtrots +dogtrotted +dogtrotting +dogwood +dogwood's +dogwoods +doilies +doily +doily's +doing +doing's +doings +doldrums +doldrums's +dole +dole's +doled +doleful +dolefully +doles +doling +doll +doll's +dollar +dollar's +dollars +dolled +dollhouse +dollhouse's +dollhouses +dollies +dolling +dollop +dollop's +dolloped +dolloping +dollops +dolls +dolly +dolly's +dolmen +dolmen's +dolmens +dolorous +dolphin +dolphin's +dolphins +dolt +dolt's +doltish +dolts +domain +domain's +domains +dome +dome's +domed +domes +domestic +domestic's +domestically +domesticate +domesticated +domesticates +domesticating +domestication +domestication's +domesticity +domesticity's +domestics +domicile +domicile's +domiciled +domiciles +domiciling +dominance +dominance's +dominant +dominant's +dominantly +dominants +dominate +dominated +dominates +dominating +domination +domination's +domineer +domineered +domineering +domineers +doming +dominion +dominion's +dominions +domino +domino's +dominoes +dominos +don +don's +don't +donate +donated +donates +donating +donation +donation's +donations +done +donkey +donkey's +donkeys +donned +donning +donor +donor's +donors +dons +donut +donut's +donuts +doodad +doodad's +doodads +doodle +doodle's +doodled +doodler +doodler's +doodlers +doodles +doodling +doohickey +doohickey's +doohickeys +doom +doom's +doomed +dooming +dooms +doomsday +doomsday's +door +door's +doorbell +doorbell's +doorbells +doorknob +doorknob's +doorknobs +doorman +doorman's +doormat +doormat's +doormats +doormen +doors +doorstep +doorstep's +doorsteps +doorway +doorway's +doorways +dope +dope's +doped +dopes +dopey +dopier +dopiest +doping +dopy +dories +dork +dork's +dorkier +dorkiest +dorks +dorky +dorm +dorm's +dormancy +dormancy's +dormant +dormer +dormer's +dormers +dormice +dormitories +dormitory +dormitory's +dormouse +dormouse's +dorms +dorsal +dory +dory's +dos +dosage +dosage's +dosages +dose +dose's +dosed +doses +dosing +dossier +dossier's +dossiers +dot +dot's +dotage +dotage's +dotcom +dotcom's +dotcoms +dote +doted +dotes +doth +doting +dotingly +dots +dotted +dotting +dotty +double +double's +doubled +doubles +doublet +doublet's +doublets +doubling +doubloon +doubloon's +doubloons +doubly +doubt +doubt's +doubted +doubter +doubter's +doubters +doubtful +doubtfully +doubting +doubtless +doubtlessly +doubts +douche +douche's +douched +douches +douching +dough +dough's +doughier +doughiest +doughnut +doughnut's +doughnuts +doughtier +doughtiest +doughty +doughy +dour +dourer +dourest +dourly +douse +doused +douses +dousing +dove +dove's +doves +dovetail +dovetail's +dovetailed +dovetailing +dovetails +dowager +dowager's +dowagers +dowdier +dowdies +dowdiest +dowdily +dowdiness +dowdiness's +dowdy +dowel +dowel's +doweled +doweling +dowelled +dowelling +dowels +down +down's +downbeat +downbeat's +downbeats +downcast +downed +downer +downer's +downers +downfall +downfall's +downfalls +downgrade +downgrade's +downgraded +downgrades +downgrading +downhearted +downhill +downhill's +downhills +downier +downiest +downing +download +download's +downloadable +downloaded +downloading +downloads +downplay +downplayed +downplaying +downplays +downpour +downpour's +downpours +downright +downs +downscale +downsize +downsized +downsizes +downsizing +downsizing's +downstage +downstairs +downstairs's +downstate +downstate's +downstream +downswing +downswing's +downswings +downtime +downtime's +downtown +downtown's +downtrodden +downturn +downturn's +downturns +downward +downwards +downwind +downy +dowries +dowry +dowry's +dowse +dowsed +dowses +dowsing +doxologies +doxology +doxology's +doyen +doyen's +doyens +doze +doze's +dozed +dozen +dozen's +dozens +dozes +dozing +drab +drab's +drabber +drabbest +drably +drabness +drabness's +drabs +drachma +drachma's +drachmae +drachmai +drachmas +draconian +draft +draft's +drafted +draftee +draftee's +draftees +draftier +draftiest +draftiness +draftiness's +drafting +drafts +draftsman +draftsman's +draftsmanship +draftsmanship's +draftsmen +drafty +drag +drag's +dragged +dragging +dragnet +dragnet's +dragnets +dragon +dragon's +dragonflies +dragonfly +dragonfly's +dragons +dragoon +dragoon's +dragooned +dragooning +dragoons +drags +drain +drain's +drainage +drainage's +drained +drainer +drainer's +drainers +draining +drainpipe +drainpipe's +drainpipes +drains +drake +drake's +drakes +dram +dram's +drama +drama's +dramas +dramatic +dramatically +dramatics +dramatics's +dramatist +dramatist's +dramatists +dramatization +dramatization's +dramatizations +dramatize +dramatized +dramatizes +dramatizing +drams +drank +drape +drape's +draped +draperies +drapery +drapery's +drapes +draping +drastic +drastically +draw +draw's +drawback +drawback's +drawbacks +drawbridge +drawbridge's +drawbridges +drawer +drawer's +drawers +drawing +drawing's +drawings +drawl +drawl's +drawled +drawling +drawls +drawn +draws +drawstring +drawstring's +drawstrings +dray +dray's +drays +dread +dread's +dreaded +dreadful +dreadfully +dreading +dreadlocks +dreadlocks's +dreadnought +dreadnought's +dreadnoughts +dreads +dream +dream's +dreamed +dreamer +dreamer's +dreamers +dreamier +dreamiest +dreamily +dreaming +dreamland +dreamland's +dreamless +dreamlike +dreams +dreamy +drearier +dreariest +drearily +dreariness +dreariness's +dreary +dredge +dredge's +dredged +dredger +dredger's +dredgers +dredges +dredging +dregs +dregs's +drench +drenched +drenches +drenching +dress +dress's +dressage +dressage's +dressed +dresser +dresser's +dressers +dresses +dressier +dressiest +dressiness +dressiness's +dressing +dressing's +dressings +dressmaker +dressmaker's +dressmakers +dressmaking +dressmaking's +dressy +drew +dribble +dribble's +dribbled +dribbler +dribbler's +dribblers +dribbles +dribbling +driblet +driblet's +driblets +dried +drier +drier's +driers +dries +driest +drift +drift's +drifted +drifter +drifter's +drifters +drifting +drifts +driftwood +driftwood's +drill +drill's +drilled +drilling +drills +drily +drink +drink's +drinkable +drinker +drinker's +drinkers +drinking +drinkings +drinks +drip +drip's +dripped +dripping +dripping's +drippings +drips +drive +drive's +drivel +drivel's +driveled +driveling +drivelled +drivelling +drivels +driven +driver +driver's +drivers +drives +driveway +driveway's +driveways +driving +drivings +drizzle +drizzle's +drizzled +drizzles +drizzling +drizzly +droll +droller +drolleries +drollery +drollery's +drollest +drollness +drollness's +drolly +dromedaries +dromedary +dromedary's +drone +drone's +droned +drones +droning +drool +drool's +drooled +drooling +drools +droop +droop's +drooped +droopier +droopiest +drooping +droops +droopy +drop +drop's +droplet +droplet's +droplets +dropout +dropout's +dropouts +dropped +dropper +dropper's +droppers +dropping +droppings +droppings's +drops +dropsy +dropsy's +dross +dross's +drought +drought's +droughts +drouth +drouth's +drouthes +drouths +drove +drove's +drover +drover's +drovers +droves +drown +drowned +drowning +drowning's +drownings +drowns +drowse +drowse's +drowsed +drowses +drowsier +drowsiest +drowsily +drowsiness +drowsiness's +drowsing +drowsy +drub +drubbed +drubbing +drubbing's +drubbings +drubs +drudge +drudge's +drudged +drudgery +drudgery's +drudges +drudging +drug +drug's +drugged +drugging +druggist +druggist's +druggists +drugs +drugstore +drugstore's +drugstores +druid +druid's +druids +drum +drum's +drummed +drummer +drummer's +drummers +drumming +drums +drumstick +drumstick's +drumsticks +drunk +drunk's +drunkard +drunkard's +drunkards +drunken +drunkenly +drunkenness +drunkenness's +drunker +drunkest +drunks +dry +dry's +dryad +dryad's +dryads +dryer +dryer's +dryers +dryest +drying +dryly +dryness +dryness's +drys +drywall +drywall's +dual +dualism +duality +duality's +dub +dub's +dubbed +dubbing +dubiety +dubiety's +dubious +dubiously +dubiousness +dubiousness's +dubs +ducal +ducat +ducat's +ducats +duchess +duchess's +duchesses +duchies +duchy +duchy's +duck +duck's +duckbill +duckbill's +duckbills +ducked +ducking +duckling +duckling's +ducklings +ducks +duct +duct's +ductile +ductility +ductility's +ducting +ductless +ducts +dud +dud's +dude +dude's +duded +dudes +dudgeon +dudgeon's +duding +duds +due +due's +duel +duel's +dueled +dueling +duelist +duelist's +duelists +duelled +duelling +duellist +duellist's +duellists +duels +dues +duet +duet's +duets +duff +duffer +duffer's +duffers +dug +dugout +dugout's +dugouts +duh +duke +duke's +dukedom +dukedom's +dukedoms +dukes +dulcet +dulcimer +dulcimer's +dulcimers +dull +dullard +dullard's +dullards +dulled +duller +dullest +dulling +dullness +dullness's +dulls +dully +dulness +dulness's +duly +dumb +dumbbell +dumbbell's +dumbbells +dumber +dumbest +dumbfound +dumbfounded +dumbfounding +dumbfounds +dumbly +dumbness +dumbness's +dumbwaiter +dumbwaiter's +dumbwaiters +dumfound +dumfounded +dumfounding +dumfounds +dummies +dummy +dummy's +dump +dump's +dumped +dumpier +dumpiest +dumping +dumpling +dumpling's +dumplings +dumps +dumpster +dumpy +dun +dun's +dunce +dunce's +dunces +dune +dune's +dunes +dung +dung's +dungaree +dungaree's +dungarees +dunged +dungeon +dungeon's +dungeons +dunging +dungs +dunk +dunk's +dunked +dunking +dunks +dunned +dunner +dunnest +dunning +dunno +duns +duo +duo's +duodena +duodenal +duodenum +duodenum's +duodenums +duos +dupe +dupe's +duped +dupes +duping +duplex +duplex's +duplexes +duplicate +duplicate's +duplicated +duplicates +duplicating +duplication +duplication's +duplicator +duplicator's +duplicators +duplicity +duplicity's +durability +durability's +durable +durably +duration +duration's +duress +duress's +during +dusk +dusk's +duskier +duskiest +dusky +dust +dust's +dustbin +dustbin's +dustbins +dusted +duster +duster's +dusters +dustier +dustiest +dustiness +dustiness's +dusting +dustless +dustman +dustmen +dustpan +dustpan's +dustpans +dusts +dusty +duteous +dutiable +duties +dutiful +dutifully +duty +duty's +duvet +dwarf +dwarf's +dwarfed +dwarfing +dwarfish +dwarfism +dwarfism's +dwarfs +dwarves +dweeb +dweeb's +dweebs +dwell +dwelled +dweller +dweller's +dwellers +dwelling +dwelling's +dwellings +dwells +dwelt +dwindle +dwindled +dwindles +dwindling +dyadic +dye +dye's +dyed +dyeing +dyer +dyer's +dyers +dyes +dyestuff +dyestuff's +dying +dying's +dyke +dyke's +dykes +dynamic +dynamic's +dynamical +dynamically +dynamics +dynamics's +dynamism +dynamism's +dynamite +dynamite's +dynamited +dynamites +dynamiting +dynamo +dynamo's +dynamos +dynastic +dynasties +dynasty +dynasty's +dysentery +dysentery's +dysfunction +dysfunction's +dysfunctional +dysfunctions +dyslexia +dyslexia's +dyslexic +dyslexic's +dyslexics +dyspepsia +dyspepsia's +dyspeptic +dyspeptic's +dyspeptics +débutante +débutante's +débutantes +décolleté +dérailleur +dérailleur's +dérailleurs +détente +détente's +e +e'er +eBay +eBay's +eMusic +eMusic's +each +eager +eagerer +eagerest +eagerly +eagerness +eagerness's +eagle +eagle's +eagles +eaglet +eaglet's +eaglets +ear +ear's +earache +earache's +earaches +earbud +earbud's +earbuds +eardrum +eardrum's +eardrums +earful +earful's +earfuls +earl +earl's +earldom +earldom's +earldoms +earlier +earliest +earliness +earliness's +earlobe +earlobe's +earlobes +earls +early +earmark +earmark's +earmarked +earmarking +earmarks +earmuff +earmuff's +earmuffs +earn +earned +earner +earner's +earners +earnest +earnest's +earnestly +earnestness +earnestness's +earnests +earning +earnings +earnings's +earns +earphone +earphone's +earphones +earplug +earplug's +earplugs +earring +earring's +earrings +ears +earshot +earshot's +earsplitting +earth +earth's +earthed +earthen +earthenware +earthenware's +earthier +earthiest +earthiness +earthiness's +earthing +earthlier +earthliest +earthling +earthling's +earthlings +earthly +earthquake +earthquake's +earthquakes +earths +earthshaking +earthward +earthwork +earthwork's +earthworks +earthworm +earthworm's +earthworms +earthy +earwax +earwax's +earwig +earwig's +earwigs +ease +ease's +eased +easel +easel's +easels +eases +easier +easiest +easily +easiness +easiness's +easing +east +east's +eastbound +easterlies +easterly +easterly's +eastern +easterner +easterner's +easterners +easternmost +eastward +eastwards +easy +easygoing +eat +eatable +eatable's +eatables +eaten +eater +eater's +eateries +eaters +eatery +eatery's +eating +eats +eave +eave's +eaves +eavesdrop +eavesdropped +eavesdropper +eavesdropper's +eavesdroppers +eavesdropping +eavesdrops +ebb +ebb's +ebbed +ebbing +ebbs +ebonies +ebony +ebony's +ebullience +ebullience's +ebullient +eccentric +eccentric's +eccentrically +eccentricities +eccentricity +eccentricity's +eccentrics +ecclesiastic +ecclesiastic's +ecclesiastical +ecclesiastics +echelon +echelon's +echelons +echo +echo's +echoed +echoes +echoing +echos +eclectic +eclectic's +eclectically +eclecticism +eclecticism's +eclectics +eclipse +eclipse's +eclipsed +eclipses +eclipsing +ecliptic +ecliptic's +ecological +ecologically +ecologist +ecologist's +ecologists +ecology +ecology's +econometric +economic +economical +economically +economics +economics's +economies +economist +economist's +economists +economize +economized +economizes +economizing +economy +economy's +ecosystem +ecosystem's +ecosystems +ecotourism +ecotourism's +ecru +ecru's +ecstasies +ecstasy +ecstasy's +ecstatic +ecstatically +ecumenical +ecumenically +eczema +eczema's +edamame +eddied +eddies +eddy +eddy's +eddying +edelweiss +edelweiss's +edema +edema's +edge +edge's +edged +edger +edges +edgeways +edgewise +edgier +edgiest +edginess +edginess's +edging +edging's +edgings +edgy +edibility +edibility's +edible +edible's +edibles +edict +edict's +edicts +edification +edification's +edifice +edifice's +edifices +edified +edifies +edify +edifying +edit +edit's +editable +edited +editing +edition +edition's +editions +editor +editor's +editorial +editorial's +editorialize +editorialized +editorializes +editorializing +editorially +editorials +editors +editorship +edits +educable +educate +educated +educates +educating +education +education's +educational +educationally +educations +educator +educator's +educators +eel +eel's +eels +eerie +eerier +eeriest +eerily +eeriness +eeriness's +eery +efface +effaced +effacement +effacement's +effaces +effacing +effect +effect's +effected +effecting +effective +effectively +effectiveness +effectiveness's +effects +effectual +effectually +effectuate +effectuated +effectuates +effectuating +effeminacy +effeminacy's +effeminate +effervesce +effervesced +effervescence +effervescence's +effervescent +effervesces +effervescing +effete +efficacious +efficaciously +efficacy +efficacy's +efficiencies +efficiency +efficiency's +efficient +efficiently +effigies +effigy +effigy's +effluent +effluent's +effluents +effort +effort's +effortless +effortlessly +efforts +effrontery +effrontery's +effulgence +effulgence's +effulgent +effusion +effusion's +effusions +effusive +effusively +effusiveness +effusiveness's +egalitarian +egalitarian's +egalitarianism +egalitarianism's +egalitarians +egg +egg's +eggbeater +eggbeater's +eggbeaters +egged +egghead +egghead's +eggheads +egging +eggnog +eggnog's +eggplant +eggplant's +eggplants +eggs +eggshell +eggshell's +eggshells +egis +egis's +eglantine +eglantine's +eglantines +ego +ego's +egocentric +egocentric's +egocentrics +egoism +egoism's +egoist +egoist's +egoistic +egoists +egos +egotism +egotism's +egotist +egotist's +egotistic +egotistical +egotistically +egotists +egregious +egregiously +egress +egress's +egresses +egret +egret's +egrets +eh +eider +eider's +eiderdown +eiderdown's +eiderdowns +eiders +eigenvalue +eigenvalues +eight +eight's +eighteen +eighteen's +eighteens +eighteenth +eighteenth's +eighteenths +eighth +eighth's +eighths +eighties +eightieth +eightieth's +eightieths +eights +eighty +eighty's +either +ejaculate +ejaculated +ejaculates +ejaculating +ejaculation +ejaculation's +ejaculations +eject +ejected +ejecting +ejection +ejection's +ejections +ejects +eke +eked +ekes +eking +elaborate +elaborated +elaborately +elaborateness +elaborateness's +elaborates +elaborating +elaboration +elaboration's +elaborations +elapse +elapsed +elapses +elapsing +elastic +elastic's +elasticity +elasticity's +elastics +elate +elated +elates +elating +elation +elation's +elbow +elbow's +elbowed +elbowing +elbowroom +elbowroom's +elbows +elder +elder's +elderberries +elderberry +elderberry's +eldercare +eldercare's +elderly +elders +eldest +elect +elect's +elected +electing +election +election's +electioneer +electioneered +electioneering +electioneers +elections +elective +elective's +electives +elector +elector's +electoral +electorate +electorate's +electorates +electors +electric +electrical +electrically +electrician +electrician's +electricians +electricity +electricity's +electrification +electrification's +electrified +electrifies +electrify +electrifying +electrocardiogram +electrocardiogram's +electrocardiograms +electrocardiograph +electrocardiograph's +electrocardiographs +electrocute +electrocuted +electrocutes +electrocuting +electrocution +electrocution's +electrocutions +electrode +electrode's +electrodes +electrodynamics +electroencephalogram +electroencephalogram's +electroencephalograms +electroencephalograph +electroencephalograph's +electroencephalographs +electrolysis +electrolysis's +electrolyte +electrolyte's +electrolytes +electrolytic +electromagnet +electromagnet's +electromagnetic +electromagnetism +electromagnetism's +electromagnets +electron +electron's +electronic +electronica +electronica's +electronically +electronics +electronics's +electrons +electroplate +electroplated +electroplates +electroplating +electrostatic +elects +elegance +elegance's +elegant +elegantly +elegiac +elegiac's +elegiacs +elegies +elegy +elegy's +element +element's +elemental +elementary +elements +elephant +elephant's +elephantine +elephants +elevate +elevated +elevates +elevating +elevation +elevation's +elevations +elevator +elevator's +elevators +eleven +eleven's +elevens +eleventh +eleventh's +elevenths +elf +elf's +elfin +elfish +elicit +elicited +eliciting +elicits +elide +elided +elides +eliding +eligibility +eligibility's +eligible +eliminate +eliminated +eliminates +eliminating +elimination +elimination's +eliminations +elision +elision's +elisions +elite +elite's +elites +elitism +elitism's +elitist +elitist's +elitists +elixir +elixir's +elixirs +elk +elk's +elks +ell +ell's +ellipse +ellipse's +ellipses +ellipsis +ellipsis's +elliptic +elliptical +elliptically +ells +elm +elm's +elms +elocution +elocution's +elocutionist +elocutionist's +elocutionists +elongate +elongated +elongates +elongating +elongation +elongation's +elongations +elope +eloped +elopement +elopement's +elopements +elopes +eloping +eloquence +eloquence's +eloquent +eloquently +else +elsewhere +elucidate +elucidated +elucidates +elucidating +elucidation +elucidation's +elucidations +elude +eluded +eludes +eluding +elusive +elusively +elusiveness +elusiveness's +elves +em +em's +emaciate +emaciated +emaciates +emaciating +emaciation +emaciation's +email +email's +emailed +emailing +emails +emanate +emanated +emanates +emanating +emanation +emanation's +emanations +emancipate +emancipated +emancipates +emancipating +emancipation +emancipation's +emancipator +emancipator's +emancipators +emasculate +emasculated +emasculates +emasculating +emasculation +emasculation's +embalm +embalmed +embalmer +embalmer's +embalmers +embalming +embalms +embankment +embankment's +embankments +embargo +embargo's +embargoed +embargoes +embargoing +embark +embarkation +embarkation's +embarkations +embarked +embarking +embarks +embarrass +embarrassed +embarrasses +embarrassing +embarrassingly +embarrassment +embarrassment's +embarrassments +embassies +embassy +embassy's +embattled +embed +embedded +embedding +embeds +embellish +embellished +embellishes +embellishing +embellishment +embellishment's +embellishments +ember +ember's +embers +embezzle +embezzled +embezzlement +embezzlement's +embezzler +embezzler's +embezzlers +embezzles +embezzling +embitter +embittered +embittering +embitters +emblazon +emblazoned +emblazoning +emblazons +emblem +emblem's +emblematic +emblems +embodied +embodies +embodiment +embodiment's +embody +embodying +embolden +emboldened +emboldening +emboldens +embolism +embolism's +embolisms +emboss +embossed +embosses +embossing +embrace +embrace's +embraced +embraces +embracing +embroider +embroidered +embroideries +embroidering +embroiders +embroidery +embroidery's +embroil +embroiled +embroiling +embroils +embryo +embryo's +embryologist +embryologist's +embryologists +embryology +embryology's +embryonic +embryos +emcee +emcee's +emceed +emceeing +emcees +emend +emendation +emendation's +emendations +emended +emending +emends +emerald +emerald's +emeralds +emerge +emerged +emergence +emergence's +emergencies +emergency +emergency's +emergent +emerges +emerging +emeritus +emery +emery's +emetic +emetic's +emetics +emigrant +emigrant's +emigrants +emigrate +emigrated +emigrates +emigrating +emigration +emigration's +emigrations +eminence +eminence's +eminences +eminent +eminently +emir +emir's +emirate +emirate's +emirates +emirs +emissaries +emissary +emissary's +emission +emission's +emissions +emit +emits +emitted +emitting +emo +emo's +emoji +emoji's +emojis +emollient +emollient's +emollients +emolument +emolument's +emoluments +emos +emote +emoted +emotes +emoting +emotion +emotion's +emotional +emotionalism +emotionalism's +emotionally +emotions +emotive +empanel +empaneled +empaneling +empanels +empathetic +empathize +empathized +empathizes +empathizing +empathy +empathy's +emperor +emperor's +emperors +emphases +emphasis +emphasis's +emphasize +emphasized +emphasizes +emphasizing +emphatic +emphatically +emphysema +emphysema's +empire +empire's +empires +empirical +empirically +empiricism +empiricism's +emplacement +emplacement's +emplacements +employ +employ's +employable +employe +employe's +employed +employee +employee's +employees +employer +employer's +employers +employes +employing +employment +employment's +employments +employs +emporia +emporium +emporium's +emporiums +empower +empowered +empowering +empowerment +empowerment's +empowers +empress +empress's +empresses +emptied +emptier +empties +emptiest +emptily +emptiness +emptiness's +empty +empty's +emptying +ems +emu +emu's +emulate +emulated +emulates +emulating +emulation +emulation's +emulations +emulator +emulator's +emulators +emulsification +emulsification's +emulsified +emulsifies +emulsify +emulsifying +emulsion +emulsion's +emulsions +emus +enable +enabled +enables +enabling +enact +enacted +enacting +enactment +enactment's +enactments +enacts +enamel +enamel's +enameled +enameling +enamelled +enamelling +enamels +enamor +enamored +enamoring +enamors +encamp +encamped +encamping +encampment +encampment's +encampments +encamps +encapsulate +encapsulated +encapsulates +encapsulating +encapsulation +encapsulation's +encapsulations +encase +encased +encases +encasing +encephalitis +encephalitis's +enchant +enchanted +enchanter +enchanter's +enchanters +enchanting +enchantingly +enchantment +enchantment's +enchantments +enchantress +enchantress's +enchantresses +enchants +enchilada +enchilada's +enchiladas +encircle +encircled +encirclement +encirclement's +encircles +encircling +enclave +enclave's +enclaves +enclose +enclosed +encloses +enclosing +enclosure +enclosure's +enclosures +encode +encoded +encoder +encoder's +encoders +encodes +encoding +encompass +encompassed +encompasses +encompassing +encore +encore's +encored +encores +encoring +encounter +encounter's +encountered +encountering +encounters +encourage +encouraged +encouragement +encouragement's +encouragements +encourages +encouraging +encouragingly +encroach +encroached +encroaches +encroaching +encroachment +encroachment's +encroachments +encrust +encrustation +encrustation's +encrustations +encrusted +encrusting +encrusts +encrypt +encrypted +encryption +encrypts +encumber +encumbered +encumbering +encumbers +encumbrance +encumbrance's +encumbrances +encyclical +encyclical's +encyclicals +encyclopaedia +encyclopaedia's +encyclopaedias +encyclopaedic +encyclopedia +encyclopedia's +encyclopedias +encyclopedic +end +end's +endanger +endangered +endangering +endangers +endear +endeared +endearing +endearingly +endearment +endearment's +endearments +endears +endeavor +endeavor's +endeavored +endeavoring +endeavors +ended +endemic +endemic's +endemics +ending +ending's +endings +endive +endive's +endives +endless +endlessly +endlessness +endlessness's +endocrine +endocrine's +endocrines +endorse +endorsed +endorsement +endorsement's +endorsements +endorser +endorser's +endorsers +endorses +endorsing +endow +endowed +endowing +endowment +endowment's +endowments +endows +ends +endue +endued +endues +enduing +endurable +endurance +endurance's +endure +endured +endures +enduring +endways +endwise +enema +enema's +enemas +enemata +enemies +enemy +enemy's +energetic +energetically +energies +energize +energized +energizer +energizer's +energizers +energizes +energizing +energy +energy's +enervate +enervated +enervates +enervating +enervation +enervation's +enfeeble +enfeebled +enfeebles +enfeebling +enfold +enfolded +enfolding +enfolds +enforce +enforceable +enforced +enforcement +enforcement's +enforcer +enforcer's +enforcers +enforces +enforcing +enfranchise +enfranchised +enfranchisement +enfranchisement's +enfranchises +enfranchising +engage +engaged +engagement +engagement's +engagements +engages +engaging +engagingly +engender +engendered +engendering +engenders +engine +engine's +engineer +engineer's +engineered +engineering +engineering's +engineers +engines +engorge +engorged +engorges +engorging +engrave +engraved +engraver +engraver's +engravers +engraves +engraving +engraving's +engravings +engross +engrossed +engrosses +engrossing +engulf +engulfed +engulfing +engulfs +enhance +enhanced +enhancement +enhancement's +enhancements +enhancer +enhances +enhancing +enigma +enigma's +enigmas +enigmatic +enigmatically +enjoin +enjoined +enjoining +enjoins +enjoy +enjoyable +enjoyed +enjoying +enjoyment +enjoyment's +enjoyments +enjoys +enlarge +enlarged +enlargement +enlargement's +enlargements +enlarger +enlarger's +enlargers +enlarges +enlarging +enlighten +enlightened +enlightening +enlightenment +enlightenment's +enlightens +enlist +enlisted +enlistee +enlistee's +enlistees +enlisting +enlistment +enlistment's +enlistments +enlists +enliven +enlivened +enlivening +enlivens +enmesh +enmeshed +enmeshes +enmeshing +enmities +enmity +enmity's +ennoble +ennobled +ennoblement +ennoblement's +ennobles +ennobling +ennui +ennui's +enormities +enormity +enormity's +enormous +enormously +enormousness +enormousness's +enough +enough's +enquire +enquired +enquires +enquiries +enquiring +enquiry +enquiry's +enrage +enraged +enrages +enraging +enrapture +enraptured +enraptures +enrapturing +enrich +enriched +enriches +enriching +enrichment +enrichment's +enrol +enroll +enrolled +enrolling +enrollment +enrollment's +enrollments +enrolls +enrolment +enrolment's +enrolments +enrols +ensconce +ensconced +ensconces +ensconcing +ensemble +ensemble's +ensembles +enshrine +enshrined +enshrines +enshrining +enshroud +enshrouded +enshrouding +enshrouds +ensign +ensign's +ensigns +enslave +enslaved +enslavement +enslavement's +enslaves +enslaving +ensnare +ensnared +ensnares +ensnaring +ensue +ensued +ensues +ensuing +ensure +ensured +ensures +ensuring +entail +entailed +entailing +entails +entangle +entangled +entanglement +entanglement's +entanglements +entangles +entangling +entente +entente's +ententes +enter +entered +entering +enterprise +enterprise's +enterprises +enterprising +enters +entertain +entertained +entertainer +entertainer's +entertainers +entertaining +entertaining's +entertainingly +entertainment +entertainment's +entertainments +entertains +enthral +enthrall +enthralled +enthralling +enthralls +enthrals +enthrone +enthroned +enthronement +enthronement's +enthronements +enthrones +enthroning +enthuse +enthused +enthuses +enthusiasm +enthusiasm's +enthusiasms +enthusiast +enthusiast's +enthusiastic +enthusiastically +enthusiasts +enthusing +entice +enticed +enticement +enticement's +enticements +entices +enticing +entire +entirely +entirety +entirety's +entities +entitle +entitled +entitlement +entitlement's +entitlements +entitles +entitling +entity +entity's +entomb +entombed +entombing +entombment +entombment's +entombs +entomological +entomologist +entomologist's +entomologists +entomology +entomology's +entourage +entourage's +entourages +entrails +entrails's +entrance +entrance's +entranced +entrances +entrancing +entrant +entrant's +entrants +entrap +entrapment +entrapment's +entrapped +entrapping +entraps +entreat +entreated +entreaties +entreating +entreats +entreaty +entreaty's +entrench +entrenched +entrenches +entrenching +entrenchment +entrenchment's +entrenchments +entrepreneur +entrepreneur's +entrepreneurial +entrepreneurs +entries +entropy +entropy's +entrust +entrusted +entrusting +entrusts +entry +entry's +entryway +entryway's +entryways +entrée +entrée's +entrées +entwine +entwined +entwines +entwining +enumerable +enumerate +enumerated +enumerates +enumerating +enumeration +enumeration's +enumerations +enunciate +enunciated +enunciates +enunciating +enunciation +enunciation's +enure +enured +enures +enuring +envelop +envelope +envelope's +enveloped +envelopes +enveloping +envelopment +envelopment's +envelops +enviable +enviably +envied +envies +envious +enviously +enviousness +enviousness's +environment +environment's +environmental +environmentalism +environmentalism's +environmentalist +environmentalist's +environmentalists +environmentally +environments +environs +environs's +envisage +envisaged +envisages +envisaging +envision +envisioned +envisioning +envisions +envoy +envoy's +envoys +envy +envy's +envying +enzyme +enzyme's +enzymes +eon +eon's +eons +epaulet +epaulet's +epaulets +epaulette +epaulette's +epaulettes +ephemeral +epic +epic's +epicenter +epicenter's +epicenters +epics +epicure +epicure's +epicurean +epicurean's +epicureans +epicures +epidemic +epidemic's +epidemics +epidemiology +epidemiology's +epidermal +epidermis +epidermis's +epidermises +epiglottides +epiglottis +epiglottis's +epiglottises +epigram +epigram's +epigrammatic +epigrams +epilepsy +epilepsy's +epileptic +epileptic's +epileptics +epilog +epilog's +epilogs +epilogue +epilogue's +epilogues +episcopacy +episcopacy's +episcopal +episcopate +episcopate's +episode +episode's +episodes +episodic +epistemology +epistle +epistle's +epistles +epistolary +epitaph +epitaph's +epitaphs +epithet +epithet's +epithets +epitome +epitome's +epitomes +epitomize +epitomized +epitomizes +epitomizing +epoch +epoch's +epochal +epochs +epoxied +epoxies +epoxy +epoxy's +epoxyed +epoxying +epsilon +equability +equability's +equable +equably +equal +equal's +equaled +equaling +equality +equality's +equalization +equalization's +equalize +equalized +equalizer +equalizer's +equalizers +equalizes +equalizing +equalled +equalling +equally +equals +equanimity +equanimity's +equate +equated +equates +equating +equation +equation's +equations +equator +equator's +equatorial +equators +equestrian +equestrian's +equestrians +equestrienne +equestrienne's +equestriennes +equidistant +equilateral +equilateral's +equilaterals +equilibrium +equilibrium's +equine +equine's +equines +equinoctial +equinox +equinox's +equinoxes +equip +equipage +equipage's +equipages +equipment +equipment's +equipoise +equipoise's +equipped +equipping +equips +equitable +equitably +equities +equity +equity's +equivalence +equivalence's +equivalences +equivalent +equivalent's +equivalently +equivalents +equivocal +equivocally +equivocate +equivocated +equivocates +equivocating +equivocation +equivocation's +equivocations +era +era's +eradicate +eradicated +eradicates +eradicating +eradication +eradication's +eras +erase +erased +eraser +eraser's +erasers +erases +erasing +erasure +erasure's +erasures +ere +erect +erected +erectile +erecting +erection +erection's +erections +erectly +erectness +erectness's +erects +erg +erg's +ergo +ergonomic +ergonomics +ergonomics's +ergs +ermine +ermine's +ermines +erode +eroded +erodes +eroding +erogenous +erosion +erosion's +erosive +erotic +erotica +erotica's +erotically +eroticism +eroticism's +err +errand +errand's +errands +errant +errata +errata's +erratas +erratic +erratically +erratum +erratum's +erred +erring +erroneous +erroneously +error +error's +errors +errs +ersatz +ersatz's +ersatzes +erstwhile +erudite +eruditely +erudition +erudition's +erupt +erupted +erupting +eruption +eruption's +eruptions +erupts +erythrocyte +erythrocyte's +erythrocytes +es +escalate +escalated +escalates +escalating +escalation +escalation's +escalations +escalator +escalator's +escalators +escapade +escapade's +escapades +escape +escape's +escaped +escapee +escapee's +escapees +escapes +escaping +escapism +escapism's +escapist +escapist's +escapists +escarole +escarole's +escaroles +escarpment +escarpment's +escarpments +eschatology +eschew +eschewed +eschewing +eschews +escort +escort's +escorted +escorting +escorts +escrow +escrow's +escrows +escutcheon +escutcheon's +escutcheons +esophagi +esophagus +esophagus's +esophaguses +esoteric +esoterically +espadrille +espadrille's +espadrilles +especial +especially +espied +espies +espionage +espionage's +esplanade +esplanade's +esplanades +espousal +espousal's +espouse +espoused +espouses +espousing +espresso +espresso's +espressos +espy +espying +esquire +esquire's +esquires +essay +essay's +essayed +essaying +essayist +essayist's +essayists +essays +essence +essence's +essences +essential +essential's +essentially +essentials +establish +established +establishes +establishing +establishment +establishment's +establishments +estate +estate's +estates +esteem +esteem's +esteemed +esteeming +esteems +ester +ester's +esters +esthete +esthete's +esthetes +esthetic +esthetics +estimable +estimate +estimate's +estimated +estimates +estimating +estimation +estimation's +estimations +estimator +estimator's +estimators +estrange +estranged +estrangement +estrangement's +estrangements +estranges +estranging +estrogen +estrogen's +estuaries +estuary +estuary's +eta +etch +etched +etcher +etcher's +etchers +etches +etching +etching's +etchings +eternal +eternally +eternities +eternity +eternity's +ether +ether's +ethereal +ethereally +ethic +ethic's +ethical +ethically +ethics +ethics's +ethnic +ethnic's +ethnically +ethnicity +ethnicity's +ethnics +ethnological +ethnologist +ethnologist's +ethnologists +ethnology +ethnology's +ethos +ethos's +etiologies +etiology +etiology's +etiquette +etiquette's +etymological +etymologies +etymologist +etymologist's +etymologists +etymology +etymology's +eucalypti +eucalyptus +eucalyptus's +eucalyptuses +eugenics +eugenics's +eulogies +eulogistic +eulogize +eulogized +eulogizes +eulogizing +eulogy +eulogy's +eunuch +eunuch's +eunuchs +euphemism +euphemism's +euphemisms +euphemistic +euphemistically +euphony +euphony's +euphoria +euphoria's +euphoric +eureka +euro +euro's +euros +eutectic +euthanasia +euthanasia's +evacuate +evacuated +evacuates +evacuating +evacuation +evacuation's +evacuations +evacuee +evacuee's +evacuees +evade +evaded +evades +evading +evaluate +evaluated +evaluates +evaluating +evaluation +evaluation's +evaluations +evanescent +evangelical +evangelical's +evangelicals +evangelism +evangelism's +evangelist +evangelist's +evangelistic +evangelists +evangelize +evangelized +evangelizes +evangelizing +evaporate +evaporated +evaporates +evaporating +evaporation +evaporation's +evasion +evasion's +evasions +evasive +evasively +evasiveness +evasiveness's +eve +eve's +even +even's +evened +evener +evenest +evenhanded +evening +evening's +evenings +evenly +evenness +evenness's +evens +event +event's +eventful +eventfully +eventfulness +eventfulness's +eventide +eventide's +events +eventual +eventualities +eventuality +eventuality's +eventually +eventuate +eventuated +eventuates +eventuating +ever +everglade +everglade's +everglades +evergreen +evergreen's +evergreens +everlasting +everlasting's +everlastings +evermore +every +everybody +everybody's +everyday +everyone +everyone's +everyplace +everything +everything's +everywhere +eves +evict +evicted +evicting +eviction +eviction's +evictions +evicts +evidence +evidence's +evidenced +evidences +evidencing +evident +evidently +evil +evil's +evildoer +evildoer's +evildoers +eviler +evilest +eviller +evillest +evilly +evils +evince +evinced +evinces +evincing +eviscerate +eviscerated +eviscerates +eviscerating +evisceration +evisceration's +evocation +evocation's +evocations +evocative +evoke +evoked +evokes +evoking +evolution +evolution's +evolutionary +evolve +evolved +evolves +evolving +ewe +ewe's +ewer +ewer's +ewers +ewes +ex +ex's +exacerbate +exacerbated +exacerbates +exacerbating +exacerbation +exacerbation's +exact +exacted +exacter +exactest +exacting +exactingly +exactitude +exactitude's +exactly +exactness +exactness's +exacts +exaggerate +exaggerated +exaggerates +exaggerating +exaggeration +exaggeration's +exaggerations +exalt +exaltation +exaltation's +exalted +exalting +exalts +exam +exam's +examination +examination's +examinations +examine +examined +examiner +examiner's +examiners +examines +examining +example +example's +exampled +examples +exampling +exams +exasperate +exasperated +exasperates +exasperating +exasperation +exasperation's +excavate +excavated +excavates +excavating +excavation +excavation's +excavations +excavator +excavator's +excavators +exceed +exceeded +exceeding +exceedingly +exceeds +excel +excelled +excellence +excellence's +excellent +excellently +excelling +excels +except +excepted +excepting +exception +exception's +exceptionable +exceptional +exceptionally +exceptions +excepts +excerpt +excerpt's +excerpted +excerpting +excerpts +excess +excess's +excesses +excessive +excessively +exchange +exchange's +exchangeable +exchanged +exchanges +exchanging +exchequer +exchequer's +exchequers +excise +excise's +excised +excises +excising +excision +excision's +excisions +excitability +excitability's +excitable +excitation +excitation's +excite +excited +excitedly +excitement +excitement's +excitements +excites +exciting +excitingly +exclaim +exclaimed +exclaiming +exclaims +exclamation +exclamation's +exclamations +exclamatory +exclude +excluded +excludes +excluding +exclusion +exclusion's +exclusive +exclusive's +exclusively +exclusiveness +exclusiveness's +exclusives +exclusivity +exclusivity's +excommunicate +excommunicated +excommunicates +excommunicating +excommunication +excommunication's +excommunications +excoriate +excoriated +excoriates +excoriating +excoriation +excoriation's +excoriations +excrement +excrement's +excrescence +excrescence's +excrescences +excreta +excreta's +excrete +excreted +excretes +excreting +excretion +excretion's +excretions +excretory +excruciating +excruciatingly +exculpate +exculpated +exculpates +exculpating +excursion +excursion's +excursions +excusable +excuse +excuse's +excused +excuses +excusing +exec +exec's +execrable +execrate +execrated +execrates +execrating +execs +executable +execute +executed +executes +executing +execution +execution's +executioner +executioner's +executioners +executions +executive +executive's +executives +executor +executor's +executors +executrices +executrix +executrix's +executrixes +exegeses +exegesis +exegesis's +exemplar +exemplar's +exemplars +exemplary +exemplification +exemplification's +exemplifications +exemplified +exemplifies +exemplify +exemplifying +exempt +exempted +exempting +exemption +exemption's +exemptions +exempts +exercise +exercise's +exercised +exercises +exercising +exert +exerted +exerting +exertion +exertion's +exertions +exerts +exes +exhalation +exhalation's +exhalations +exhale +exhaled +exhales +exhaling +exhaust +exhaust's +exhausted +exhaustible +exhausting +exhaustion +exhaustion's +exhaustive +exhaustively +exhausts +exhibit +exhibit's +exhibited +exhibiting +exhibition +exhibition's +exhibitionism +exhibitionism's +exhibitionist +exhibitionist's +exhibitionists +exhibitions +exhibitor +exhibitor's +exhibitors +exhibits +exhilarate +exhilarated +exhilarates +exhilarating +exhilaration +exhilaration's +exhort +exhortation +exhortation's +exhortations +exhorted +exhorting +exhorts +exhumation +exhumation's +exhumations +exhume +exhumed +exhumes +exhuming +exigencies +exigency +exigency's +exigent +exiguous +exile +exile's +exiled +exiles +exiling +exist +existed +existence +existence's +existences +existent +existential +existentialism +existentialism's +existentialist +existentialist's +existentialists +existentially +existing +exists +exit +exit's +exited +exiting +exits +exodus +exodus's +exoduses +exonerate +exonerated +exonerates +exonerating +exoneration +exoneration's +exoplanet +exoplanet's +exoplanets +exorbitance +exorbitance's +exorbitant +exorbitantly +exorcise +exorcised +exorcises +exorcising +exorcism +exorcism's +exorcisms +exorcist +exorcist's +exorcists +exorcize +exorcized +exorcizes +exorcizing +exotic +exotic's +exotically +exotics +expand +expandable +expanded +expanding +expands +expanse +expanse's +expanses +expansion +expansion's +expansionist +expansionist's +expansionists +expansions +expansive +expansively +expansiveness +expansiveness's +expatiate +expatiated +expatiates +expatiating +expatriate +expatriate's +expatriated +expatriates +expatriating +expatriation +expatriation's +expect +expectancy +expectancy's +expectant +expectantly +expectation +expectation's +expectations +expected +expecting +expectorant +expectorant's +expectorants +expectorate +expectorated +expectorates +expectorating +expectoration +expectoration's +expects +expedience +expedience's +expediences +expediencies +expediency +expediency's +expedient +expedient's +expediently +expedients +expedite +expedited +expediter +expediter's +expediters +expedites +expediting +expedition +expedition's +expeditionary +expeditions +expeditious +expeditiously +expeditor +expeditor's +expeditors +expel +expelled +expelling +expels +expend +expendable +expendable's +expendables +expended +expending +expenditure +expenditure's +expenditures +expends +expense +expense's +expenses +expensive +expensively +experience +experience's +experienced +experiences +experiencing +experiment +experiment's +experimental +experimentally +experimentation +experimentation's +experimented +experimenter +experimenter's +experimenters +experimenting +experiments +expert +expert's +expertise +expertise's +expertly +expertness +expertness's +experts +expiate +expiated +expiates +expiating +expiation +expiation's +expiration +expiration's +expire +expired +expires +expiring +expiry +explain +explained +explaining +explains +explanation +explanation's +explanations +explanatory +expletive +expletive's +expletives +explicable +explicate +explicated +explicates +explicating +explication +explication's +explications +explicit +explicitly +explicitness +explicitness's +explode +exploded +explodes +exploding +exploit +exploit's +exploitation +exploitation's +exploitative +exploited +exploiter +exploiter's +exploiters +exploiting +exploits +exploration +exploration's +explorations +exploratory +explore +explored +explorer +explorer's +explorers +explores +exploring +explosion +explosion's +explosions +explosive +explosive's +explosively +explosiveness +explosiveness's +explosives +expo +expo's +exponent +exponent's +exponential +exponentially +exponentiation +exponents +export +export's +exportation +exportation's +exported +exporter +exporter's +exporters +exporting +exports +expos +expose +expose's +exposed +exposes +exposing +exposition +exposition's +expositions +expository +expostulate +expostulated +expostulates +expostulating +expostulation +expostulation's +expostulations +exposure +exposure's +exposures +expound +expounded +expounding +expounds +express +express's +expressed +expresses +expressible +expressing +expression +expression's +expressionism +expressionism's +expressionist +expressionist's +expressionists +expressionless +expressions +expressive +expressively +expressiveness +expressiveness's +expressly +expressway +expressway's +expressways +expropriate +expropriated +expropriates +expropriating +expropriation +expropriation's +expropriations +expulsion +expulsion's +expulsions +expunge +expunged +expunges +expunging +expurgate +expurgated +expurgates +expurgating +expurgation +expurgation's +expurgations +exquisite +exquisitely +extant +extemporaneous +extemporaneously +extempore +extemporize +extemporized +extemporizes +extemporizing +extend +extendable +extended +extendible +extending +extends +extension +extension's +extensional +extensions +extensive +extensively +extensiveness +extensiveness's +extent +extent's +extents +extenuate +extenuated +extenuates +extenuating +extenuation +extenuation's +exterior +exterior's +exteriors +exterminate +exterminated +exterminates +exterminating +extermination +extermination's +exterminations +exterminator +exterminator's +exterminators +external +external's +externally +externals +extinct +extincted +extincting +extinction +extinction's +extinctions +extincts +extinguish +extinguishable +extinguished +extinguisher +extinguisher's +extinguishers +extinguishes +extinguishing +extirpate +extirpated +extirpates +extirpating +extirpation +extirpation's +extol +extoll +extolled +extolling +extolls +extols +extort +extorted +extorting +extortion +extortion's +extortionate +extortionist +extortionist's +extortionists +extorts +extra +extra's +extract +extract's +extracted +extracting +extraction +extraction's +extractions +extractor +extractor's +extractors +extracts +extracurricular +extradite +extradited +extradites +extraditing +extradition +extradition's +extraditions +extramarital +extraneous +extraneously +extraordinarily +extraordinary +extrapolate +extrapolated +extrapolates +extrapolating +extrapolation +extrapolation's +extrapolations +extras +extrasensory +extraterrestrial +extraterrestrial's +extraterrestrials +extravagance +extravagance's +extravagances +extravagant +extravagantly +extravaganza +extravaganza's +extravaganzas +extravert +extravert's +extraverted +extraverts +extreme +extreme's +extremely +extremer +extremes +extremest +extremism +extremism's +extremist +extremist's +extremists +extremities +extremity +extremity's +extricate +extricated +extricates +extricating +extrication +extrication's +extrinsic +extrinsically +extroversion +extroversion's +extrovert +extrovert's +extroverted +extroverts +extrude +extruded +extrudes +extruding +extrusion +extrusion's +extrusions +exuberance +exuberance's +exuberant +exuberantly +exude +exuded +exudes +exuding +exult +exultant +exultantly +exultation +exultation's +exulted +exulting +exults +eye +eye's +eyeball +eyeball's +eyeballed +eyeballing +eyeballs +eyebrow +eyebrow's +eyebrows +eyed +eyeful +eyeful's +eyefuls +eyeglass +eyeglass's +eyeglasses +eyeing +eyelash +eyelash's +eyelashes +eyelet +eyelet's +eyelets +eyelid +eyelid's +eyelids +eyeliner +eyeliner's +eyeliners +eyepiece +eyepiece's +eyepieces +eyes +eyesight +eyesight's +eyesore +eyesore's +eyesores +eyestrain +eyestrain's +eyeteeth +eyetooth +eyetooth's +eyewitness +eyewitness's +eyewitnesses +eying +eyrie +eyrie's +f +fa +fa's +fable +fable's +fabled +fables +fabric +fabric's +fabricate +fabricated +fabricates +fabricating +fabrication +fabrication's +fabrications +fabrics +fabulous +fabulously +facade +facade's +facades +face +face's +faced +faceless +facelift +facelift's +facelifts +faces +facet +facet's +faceted +faceting +facetious +facetiously +facetiousness +facetiousness's +facets +facetted +facetting +facial +facial's +facially +facials +facile +facilitate +facilitated +facilitates +facilitating +facilitation +facilitation's +facilities +facility +facility's +facing +facing's +facings +facsimile +facsimile's +facsimiled +facsimileing +facsimiles +fact +fact's +faction +faction's +factional +factionalism +factionalism's +factions +factitious +factor +factor's +factored +factorial +factories +factoring +factorization +factorize +factorizing +factors +factory +factory's +factotum +factotum's +factotums +facts +factual +factually +faculties +faculty +faculty's +fad +fad's +faddish +fade +fade's +faded +fades +fading +fads +faecal +faeces +faeces's +fag +fag's +fagged +fagging +faggot +faggot's +faggots +fagot +fagot's +fagots +fags +fail +fail's +failed +failing +failing's +failings +fails +failure +failure's +failures +fain +fainer +fainest +faint +faint's +fainted +fainter +faintest +fainthearted +fainting +faintly +faintness +faintness's +faints +fair +fair's +fairer +fairest +fairground +fairground's +fairgrounds +fairies +fairly +fairness +fairness's +fairs +fairway +fairway's +fairways +fairy +fairy's +fairyland +fairyland's +fairylands +faith +faith's +faithful +faithful's +faithfully +faithfulness +faithfulness's +faithfuls +faithless +faithlessly +faithlessness +faithlessness's +faiths +fake +fake's +faked +faker +faker's +fakers +fakes +faking +fakir +fakir's +fakirs +falcon +falcon's +falconer +falconer's +falconers +falconry +falconry's +falcons +fall +fall's +fallacies +fallacious +fallaciously +fallacy +fallacy's +fallen +fallibility +fallibility's +fallible +fallibly +falling +falloff +falloff's +falloffs +fallout +fallout's +fallow +fallow's +fallowed +fallowing +fallows +falls +false +falsehood +falsehood's +falsehoods +falsely +falseness +falseness's +falser +falsest +falsetto +falsetto's +falsettos +falsifiable +falsification +falsification's +falsifications +falsified +falsifies +falsify +falsifying +falsities +falsity +falsity's +falter +falter's +faltered +faltering +falteringly +falterings +falters +fame +fame's +famed +familial +familiar +familiar's +familiarity +familiarity's +familiarization +familiarization's +familiarize +familiarized +familiarizes +familiarizing +familiarly +familiars +families +family +family's +famine +famine's +famines +famish +famished +famishes +famishing +famous +famously +fan +fan's +fanatic +fanatic's +fanatical +fanatically +fanaticism +fanaticism's +fanatics +fanboy +fanboy's +fanboys +fancied +fancier +fancier's +fanciers +fancies +fanciest +fanciful +fancifully +fancily +fanciness +fanciness's +fancy +fancy's +fancying +fandom +fanfare +fanfare's +fanfares +fang +fang's +fangs +fanned +fannies +fanning +fanny +fanny's +fans +fantasied +fantasies +fantasize +fantasized +fantasizes +fantasizing +fantastic +fantastically +fantasy +fantasy's +fantasying +fanzine +far +faraway +farce +farce's +farces +farcical +fare +fare's +fared +fares +farewell +farewell's +farewells +farina +farina's +farinaceous +faring +farm +farm's +farmed +farmer +farmer's +farmers +farmhand +farmhand's +farmhands +farmhouse +farmhouse's +farmhouses +farming +farming's +farmland +farmland's +farms +farmyard +farmyard's +farmyards +farrow +farrow's +farrowed +farrowing +farrows +farsighted +farsightedness +farsightedness's +fart +fart's +farted +farther +farthest +farthing +farthing's +farthings +farting +farts +fascinate +fascinated +fascinates +fascinating +fascination +fascination's +fascinations +fascism +fascism's +fascist +fascist's +fascists +fashion +fashion's +fashionable +fashionably +fashioned +fashioning +fashionista +fashionista's +fashionistas +fashions +fast +fast's +fasted +fasten +fastened +fastener +fastener's +fasteners +fastening +fastening's +fastenings +fastens +faster +fastest +fastidious +fastidiously +fastidiousness +fastidiousness's +fasting +fastness +fastness's +fastnesses +fasts +fat +fat's +fatal +fatalism +fatalism's +fatalist +fatalist's +fatalistic +fatalists +fatalities +fatality +fatality's +fatally +fate +fate's +fated +fateful +fatefully +fates +fathead +fathead's +fatheads +father +father's +fathered +fatherhood +fatherhood's +fathering +fatherland +fatherland's +fatherlands +fatherless +fatherly +fathers +fathom +fathom's +fathomable +fathomed +fathoming +fathomless +fathoms +fatigue +fatigue's +fatigued +fatigues +fatigues's +fatiguing +fating +fatness +fatness's +fats +fatten +fattened +fattening +fattens +fatter +fattest +fattier +fatties +fattiest +fatty +fatty's +fatuous +fatuously +fatuousness +fatuousness's +faucet +faucet's +faucets +fault +fault's +faulted +faultfinding +faultfinding's +faultier +faultiest +faultily +faultiness +faultiness's +faulting +faultless +faultlessly +faults +faulty +faun +faun's +fauna +fauna's +faunae +faunas +fauns +favor +favor's +favorable +favorably +favored +favoring +favorite +favorite's +favorites +favoritism +favoritism's +favors +fawn +fawn's +fawned +fawning +fawns +fax +fax's +faxed +faxes +faxing +faze +fazed +fazes +fazing +fealty +fealty's +fear +fear's +feared +fearful +fearfully +fearfulness +fearfulness's +fearing +fearless +fearlessly +fearlessness +fearlessness's +fears +fearsome +feasibility +feasibility's +feasible +feasibly +feast +feast's +feasted +feasting +feasts +feat +feat's +feather +feather's +featherbedding +featherbedding's +feathered +featherier +featheriest +feathering +feathers +featherweight +featherweight's +featherweights +feathery +feats +feature +feature's +featured +featureless +features +featuring +febrile +fecal +feces +feces's +feckless +fecund +fecundity +fecundity's +fed +fed's +federal +federal's +federalism +federalism's +federalist +federalist's +federalists +federally +federals +federate +federated +federates +federating +federation +federation's +federations +fedora +fedora's +fedoras +feds +fee +fee's +feeble +feebleness +feebleness's +feebler +feeblest +feebly +feed +feed's +feedback +feedback's +feedbag +feedbag's +feedbags +feeder +feeder's +feeders +feeding +feeding's +feedings +feeds +feel +feel's +feeler +feeler's +feelers +feeling +feeling's +feelingly +feelings +feels +fees +feet +feign +feigned +feigning +feigns +feint +feint's +feinted +feinting +feints +feistier +feistiest +feisty +feldspar +feldspar's +felicities +felicitous +felicity +felicity's +feline +feline's +felines +fell +fell's +fellatio +fellatio's +felled +feller +fellest +felling +fellow +fellow's +fellows +fellowship +fellowship's +fellowships +fells +felon +felon's +felonies +felonious +felons +felony +felony's +felt +felt's +felted +felting +felts +female +female's +females +feminine +feminine's +feminines +femininity +femininity's +feminism +feminism's +feminist +feminist's +feminists +femora +femoral +femur +femur's +femurs +fen +fen's +fence +fence's +fenced +fencer +fencer's +fencers +fences +fencing +fencing's +fend +fended +fender +fender's +fenders +fending +fends +fennel +fennel's +fens +fer +feral +ferment +ferment's +fermentation +fermentation's +fermented +fermenting +ferments +fern +fern's +ferns +ferocious +ferociously +ferociousness +ferociousness's +ferocity +ferocity's +ferret +ferret's +ferreted +ferreting +ferrets +ferric +ferried +ferries +ferrous +ferrule +ferrule's +ferrules +ferry +ferry's +ferryboat +ferryboat's +ferryboats +ferrying +fertile +fertility +fertility's +fertilization +fertilization's +fertilize +fertilized +fertilizer +fertilizer's +fertilizers +fertilizes +fertilizing +fervency +fervency's +fervent +fervently +fervid +fervidly +fervor +fervor's +fest +fest's +festal +fester +fester's +festered +festering +festers +festival +festival's +festivals +festive +festively +festivities +festivity +festivity's +festoon +festoon's +festooned +festooning +festoons +fests +feta +feta's +fetal +fetch +fetched +fetches +fetching +fetchingly +feted +fetich +fetich's +fetiches +fetid +feting +fetish +fetish's +fetishes +fetishism +fetishism's +fetishist +fetishist's +fetishistic +fetishists +fetlock +fetlock's +fetlocks +fetter +fetter's +fettered +fettering +fetters +fettle +fettle's +fetus +fetus's +fetuses +feud +feud's +feudal +feudalism +feudalism's +feudalistic +feuded +feuding +feuds +fever +fever's +fevered +feverish +feverishly +fevers +few +few's +fewer +fewest +fey +fez +fez's +fezes +fezzes +fiancé +fiancé's +fiancée +fiancée's +fiancées +fiancés +fiasco +fiasco's +fiascoes +fiascos +fiat +fiat's +fiats +fib +fib's +fibbed +fibber +fibber's +fibbers +fibbing +fiber +fiber's +fiberboard +fiberboard's +fiberglass +fiberglass's +fibers +fibroid +fibrous +fibs +fibula +fibula's +fibulae +fibulas +fiche +fiche's +fiches +fickle +fickleness +fickleness's +fickler +ficklest +fiction +fiction's +fictional +fictionalize +fictionalized +fictionalizes +fictionalizing +fictions +fictitious +fiddle +fiddle's +fiddled +fiddler +fiddler's +fiddlers +fiddles +fiddlesticks +fiddling +fiddly +fidelity +fidelity's +fidget +fidget's +fidgeted +fidgeting +fidgets +fidgety +fiduciaries +fiduciary +fiduciary's +fie +fief +fief's +fiefs +field +field's +fielded +fielder +fielder's +fielders +fielding +fields +fieldwork +fieldwork's +fiend +fiend's +fiendish +fiendishly +fiends +fierce +fiercely +fierceness +fierceness's +fiercer +fiercest +fierier +fieriest +fieriness +fieriness's +fiery +fiesta +fiesta's +fiestas +fife +fife's +fifes +fifteen +fifteen's +fifteens +fifteenth +fifteenth's +fifteenths +fifth +fifth's +fifths +fifties +fiftieth +fiftieth's +fiftieths +fifty +fifty's +fig +fig's +fight +fight's +fighter +fighter's +fighters +fighting +fighting's +fights +figment +figment's +figments +figs +figurative +figuratively +figure +figure's +figured +figurehead +figurehead's +figureheads +figures +figurine +figurine's +figurines +figuring +filament +filament's +filamentous +filaments +filbert +filbert's +filberts +filch +filched +filches +filching +file +file's +filed +files +filet +filet's +filets +filial +filibuster +filibuster's +filibustered +filibustering +filibusters +filigree +filigree's +filigreed +filigreeing +filigrees +filing +filing's +filings +fill +fill's +filled +filler +filler's +fillers +fillet +fillet's +filleted +filleting +fillets +fillies +filling +filling's +fillings +fillip +fillip's +filliped +filliping +fillips +fills +filly +filly's +film +film's +filmed +filmier +filmiest +filming +filmmaker +filmmaker's +filmmakers +films +filmstrip +filmstrip's +filmstrips +filmy +filter +filter's +filterable +filtered +filtering +filters +filth +filth's +filthier +filthiest +filthiness +filthiness's +filthy +filtrable +filtrate +filtrate's +filtrated +filtrates +filtrating +filtration +filtration's +fin +fin's +finagle +finagled +finagler +finagler's +finaglers +finagles +finagling +final +final's +finale +finale's +finales +finalist +finalist's +finalists +finality +finality's +finalize +finalized +finalizes +finalizing +finally +finals +finance +finance's +financed +finances +financial +financially +financier +financier's +financiers +financing +financing's +finch +finch's +finches +find +find's +finder +finder's +finders +finding +finding's +findings +finds +fine +fine's +fined +finely +fineness +fineness's +finer +finery +finery's +fines +finesse +finesse's +finessed +finesses +finessing +finest +finger +finger's +fingerboard +fingerboard's +fingerboards +fingered +fingering +fingering's +fingerings +fingernail +fingernail's +fingernails +fingerprint +fingerprint's +fingerprinted +fingerprinting +fingerprints +fingers +fingertip +fingertip's +fingertips +finickier +finickiest +finicky +fining +finis +finis's +finises +finish +finish's +finished +finisher +finisher's +finishers +finishes +finishing +finite +finitely +fink +fink's +finked +finking +finks +finny +fins +fiord +fiord's +fiords +fir +fir's +fire +fire's +firearm +firearm's +firearms +fireball +fireball's +fireballs +firebomb +firebomb's +firebombed +firebombing +firebombs +firebrand +firebrand's +firebrands +firebreak +firebreak's +firebreaks +firebug +firebug's +firebugs +firecracker +firecracker's +firecrackers +fired +firefight +firefight's +firefighter +firefighter's +firefighters +firefighting +firefighting's +firefights +fireflies +firefly +firefly's +firehouse +firehouse's +firehouses +fireman +fireman's +firemen +fireplace +fireplace's +fireplaces +fireplug +fireplug's +fireplugs +firepower +firepower's +fireproof +fireproofed +fireproofing +fireproofs +fires +fireside +fireside's +firesides +firestorm +firestorm's +firestorms +firetrap +firetrap's +firetraps +firewall +firewall's +firewalls +firewater +firewater's +firewood +firewood's +firework +firework's +fireworks +firing +firm +firm's +firmament +firmament's +firmaments +firmed +firmer +firmest +firming +firmly +firmness +firmness's +firms +firmware +firs +first +first's +firstborn +firstborn's +firstborns +firsthand +firstly +firsts +firth +firth's +firths +fiscal +fiscal's +fiscally +fiscals +fish +fish's +fishbowl +fishbowl's +fishbowls +fished +fisher +fisher's +fisheries +fisherman +fisherman's +fishermen +fishers +fishery +fishery's +fishes +fishhook +fishhook's +fishhooks +fishier +fishiest +fishing +fishing's +fishnet +fishnet's +fishnets +fishtail +fishtailed +fishtailing +fishtails +fishwife +fishwife's +fishwives +fishy +fission +fission's +fissure +fissure's +fissures +fist +fist's +fistful +fistful's +fistfuls +fisticuffs +fisticuffs's +fists +fit +fit's +fitful +fitfully +fitly +fitness +fitness's +fits +fitted +fitter +fitter's +fitters +fittest +fitting +fitting's +fittingly +fittings +five +five's +fiver +fives +fix +fix's +fixable +fixate +fixated +fixates +fixating +fixation +fixation's +fixations +fixative +fixative's +fixatives +fixed +fixedly +fixer +fixer's +fixers +fixes +fixing +fixings +fixings's +fixity +fixity's +fixture +fixture's +fixtures +fizz +fizz's +fizzed +fizzes +fizzier +fizziest +fizzing +fizzle +fizzle's +fizzled +fizzles +fizzling +fizzy +fjord +fjord's +fjords +flab +flab's +flabbergast +flabbergasted +flabbergasting +flabbergasts +flabbier +flabbiest +flabbiness +flabbiness's +flabby +flaccid +flack +flack's +flacks +flag +flag's +flagella +flagellate +flagellated +flagellates +flagellating +flagellation +flagellation's +flagellum +flagellum's +flagellums +flagged +flagging +flagon +flagon's +flagons +flagpole +flagpole's +flagpoles +flagrant +flagrantly +flags +flagship +flagship's +flagships +flagstaff +flagstaff's +flagstaffs +flagstone +flagstone's +flagstones +flail +flail's +flailed +flailing +flails +flair +flair's +flairs +flak +flak's +flake +flake's +flaked +flakes +flakier +flakiest +flakiness +flakiness's +flaking +flaky +flambeing +flambes +flamboyance +flamboyance's +flamboyant +flamboyantly +flambé +flambé's +flambéed +flame +flame's +flamed +flamenco +flamenco's +flamencos +flames +flamethrower +flamethrower's +flamethrowers +flaming +flamingo +flamingo's +flamingoes +flamingos +flamings +flammability +flammability's +flammable +flammable's +flammables +flan +flange +flange's +flanges +flank +flank's +flanked +flanking +flanks +flannel +flannel's +flanneled +flannelet +flannelet's +flannelette +flannelette's +flanneling +flannelled +flannelling +flannels +flap +flap's +flapjack +flapjack's +flapjacks +flapped +flapper +flapper's +flappers +flapping +flaps +flare +flare's +flared +flares +flaring +flash +flash's +flashback +flashback's +flashbacks +flashbulb +flashbulb's +flashbulbs +flashed +flasher +flasher's +flashers +flashes +flashest +flashgun +flashgun's +flashguns +flashier +flashiest +flashily +flashiness +flashiness's +flashing +flashing's +flashlight +flashlight's +flashlights +flashy +flask +flask's +flasks +flat +flat's +flatbed +flatbed's +flatbeds +flatboat +flatboat's +flatboats +flatcar +flatcar's +flatcars +flatfeet +flatfish +flatfish's +flatfishes +flatfoot +flatfoot's +flatfooted +flatfoots +flatiron +flatiron's +flatirons +flatly +flatness +flatness's +flats +flatted +flatten +flattened +flattening +flattens +flatter +flattered +flatterer +flatterer's +flatterers +flattering +flatteringly +flatters +flattery +flattery's +flattest +flatting +flattop +flattop's +flattops +flatulence +flatulence's +flatulent +flatware +flatware's +flaunt +flaunt's +flaunted +flaunting +flaunts +flavor +flavor's +flavored +flavorful +flavoring +flavoring's +flavorings +flavorless +flavors +flaw +flaw's +flawed +flawing +flawless +flawlessly +flaws +flax +flax's +flaxen +flay +flayed +flaying +flays +flea +flea's +fleas +fleck +fleck's +flecked +flecking +flecks +fled +fledged +fledgeling +fledgeling's +fledgelings +fledgling +fledgling's +fledglings +flee +fleece +fleece's +fleeced +fleeces +fleecier +fleeciest +fleecing +fleecy +fleeing +flees +fleet +fleet's +fleeted +fleeter +fleetest +fleeting +fleetingly +fleetingly's +fleetness +fleetness's +fleets +flesh +flesh's +fleshed +fleshes +fleshier +fleshiest +fleshing +fleshlier +fleshliest +fleshly +fleshy +flew +flex +flex's +flexed +flexes +flexibility +flexibility's +flexible +flexibly +flexing +flexitime +flexitime's +flextime +flextime's +flibbertigibbet +flibbertigibbet's +flibbertigibbets +flick +flick's +flicked +flicker +flicker's +flickered +flickering +flickers +flicking +flicks +flied +flier +flier's +fliers +flies +fliest +flight +flight's +flightier +flightiest +flightiness +flightiness's +flightless +flights +flighty +flimflam +flimflam's +flimflammed +flimflamming +flimflams +flimsier +flimsiest +flimsily +flimsiness +flimsiness's +flimsy +flinch +flinch's +flinched +flinches +flinching +fling +fling's +flinging +flings +flint +flint's +flintier +flintiest +flintlock +flintlock's +flintlocks +flints +flinty +flip +flip's +flippancy +flippancy's +flippant +flippantly +flipped +flipper +flipper's +flippers +flippest +flipping +flips +flirt +flirt's +flirtation +flirtation's +flirtations +flirtatious +flirtatiously +flirted +flirting +flirts +flit +flit's +flits +flitted +flitting +float +float's +floatation +floatation's +floatations +floated +floater +floater's +floaters +floating +floats +flock +flock's +flocked +flocking +flocks +floe +floe's +floes +flog +flogged +flogging +flogging's +floggings +flogs +flood +flood's +flooded +flooder +floodgate +floodgate's +floodgates +flooding +floodlight +floodlight's +floodlighted +floodlighting +floodlights +floodlit +floods +floor +floor's +floorboard +floorboard's +floorboards +floored +flooring +flooring's +floors +floozie +floozie's +floozies +floozy +floozy's +flop +flop's +flophouse +flophouse's +flophouses +flopped +floppier +floppies +floppiest +floppiness +floppiness's +flopping +floppy +floppy's +flops +flora +flora's +florae +floral +floras +florid +floridly +florin +florin's +florins +florist +florist's +florists +floss +floss's +flossed +flosses +flossing +flotation +flotation's +flotations +flotilla +flotilla's +flotillas +flotsam +flotsam's +flounce +flounce's +flounced +flounces +flouncing +flounder +flounder's +floundered +floundering +flounders +flour +flour's +floured +flouring +flourish +flourish's +flourished +flourishes +flourishing +flours +floury +flout +flout's +flouted +flouting +flouts +flow +flow's +flowed +flower +flower's +flowerbed +flowerbed's +flowerbeds +flowered +flowerier +floweriest +floweriness +floweriness's +flowering +flowerpot +flowerpot's +flowerpots +flowers +flowery +flowing +flown +flows +flu +flu's +flub +flub's +flubbed +flubbing +flubs +fluctuate +fluctuated +fluctuates +fluctuating +fluctuation +fluctuation's +fluctuations +flue +flue's +fluency +fluency's +fluent +fluently +flues +fluff +fluff's +fluffed +fluffier +fluffiest +fluffiness +fluffiness's +fluffing +fluffs +fluffy +fluid +fluid's +fluidity +fluidity's +fluidly +fluids +fluke +fluke's +flukes +flukey +flukier +flukiest +fluky +flume +flume's +flumes +flummox +flummoxed +flummoxes +flummoxing +flung +flunk +flunk's +flunked +flunkey +flunkey's +flunkeys +flunkie +flunkie's +flunkies +flunking +flunks +flunky +flunky's +fluoresce +fluoresced +fluorescence +fluorescence's +fluorescent +fluoresces +fluorescing +fluoridate +fluoridated +fluoridates +fluoridating +fluoridation +fluoridation's +fluoride +fluoride's +fluorides +fluorine +fluorine's +fluorite +fluorite's +fluorocarbon +fluorocarbon's +fluorocarbons +fluoroscope +fluoroscope's +fluoroscopes +flurried +flurries +flurry +flurry's +flurrying +flush +flush's +flushed +flusher +flushes +flushest +flushing +fluster +fluster's +flustered +flustering +flusters +flute +flute's +fluted +flutes +fluting +fluting's +flutist +flutist's +flutists +flutter +flutter's +fluttered +fluttering +flutters +fluttery +flux +flux's +fluxed +fluxes +fluxing +fly +fly's +flyby +flyby's +flybys +flycatcher +flycatcher's +flycatchers +flyer +flyer's +flyers +flying +flying's +flyleaf +flyleaf's +flyleaves +flyover +flyover's +flyovers +flypaper +flypaper's +flypapers +flysheet +flyspeck +flyspeck's +flyspecked +flyspecking +flyspecks +flyswatter +flyswatter's +flyswatters +flyweight +flyweight's +flyweights +flywheel +flywheel's +flywheels +fo'c's'le +fo'c's'le's +fo'c's'les +fo'c'sle +fo'c'sle's +fo'c'sles +foal +foal's +foaled +foaling +foals +foam +foam's +foamed +foamier +foamiest +foaming +foams +foamy +fob +fob's +fobbed +fobbing +fobs +focal +foci +focus +focus's +focused +focuses +focusing +focussed +focusses +focussing +fodder +fodder's +fodders +foe +foe's +foes +foetal +foetus +foetus's +foetuses +fog +fog's +fogbound +fogey +fogey's +fogeys +fogged +foggier +foggiest +fogginess +fogginess's +fogging +foggy +foghorn +foghorn's +foghorns +fogies +fogs +fogy +fogy's +foible +foible's +foibles +foil +foil's +foiled +foiling +foils +foist +foisted +foisting +foists +fold +fold's +foldaway +folded +folder +folder's +folders +folding +folds +foliage +foliage's +folio +folio's +folios +folk +folk's +folklore +folklore's +folks +folksier +folksiest +folksy +follicle +follicle's +follicles +follies +follow +followed +follower +follower's +followers +following +following's +followings +follows +folly +folly's +foment +fomentation +fomentation's +fomented +fomenting +foments +fond +fondant +fondant's +fondants +fonder +fondest +fondle +fondled +fondles +fondling +fondly +fondness +fondness's +fondu +fondu's +fondue +fondue's +fondues +fondus +font +font's +fonts +food +food's +foods +foodstuff +foodstuff's +foodstuffs +fool +fool's +fooled +fooleries +foolery +foolery's +foolhardier +foolhardiest +foolhardiness +foolhardiness's +foolhardy +fooling +foolish +foolishly +foolishness +foolishness's +foolproof +fools +foolscap +foolscap's +foot +foot's +footage +footage's +football +football's +footballer +footballer's +footballers +footballs +footbridge +footbridge's +footbridges +footed +footfall +footfall's +footfalls +foothill +foothill's +foothills +foothold +foothold's +footholds +footing +footing's +footings +footlights +footlights's +footlocker +footlocker's +footlockers +footloose +footman +footman's +footmen +footnote +footnote's +footnoted +footnotes +footnoting +footpath +footpath's +footpaths +footprint +footprint's +footprints +footrest +footrest's +footrests +foots +footsie +footsie's +footsies +footsore +footstep +footstep's +footsteps +footstool +footstool's +footstools +footwear +footwear's +footwork +footwork's +fop +fop's +foppish +fops +for +fora +forage +forage's +foraged +forager +forager's +foragers +forages +foraging +foray +foray's +forayed +foraying +forays +forbad +forbade +forbear +forbear's +forbearance +forbearance's +forbearing +forbears +forbid +forbidden +forbidding +forbiddingly +forbiddings +forbids +forbore +forborne +force +force's +forced +forceful +forcefully +forcefulness +forcefulness's +forceps +forceps's +forces +forcible +forcibly +forcing +ford +ford's +forded +fording +fords +fore +fore's +forearm +forearm's +forearmed +forearming +forearms +forebear +forebear's +forebears +forebode +foreboded +forebodes +foreboding +foreboding's +forebodings +forecast +forecast's +forecasted +forecaster +forecaster's +forecasters +forecasting +forecastle +forecastle's +forecastles +forecasts +foreclose +foreclosed +forecloses +foreclosing +foreclosure +foreclosure's +foreclosures +forefather +forefather's +forefathers +forefeet +forefinger +forefinger's +forefingers +forefoot +forefoot's +forefront +forefront's +forefronts +foregather +foregathered +foregathering +foregathers +forego +foregoes +foregoing +foregone +foreground +foreground's +foregrounded +foregrounding +foregrounds +forehand +forehand's +forehands +forehead +forehead's +foreheads +foreign +foreigner +foreigner's +foreigners +foreknowledge +foreknowledge's +foreleg +foreleg's +forelegs +forelock +forelock's +forelocks +foreman +foreman's +foremast +foremast's +foremasts +foremen +foremost +forename +forename's +forenames +forenoon +forenoon's +forenoons +forensic +forensic's +forensics +foreordain +foreordained +foreordaining +foreordains +foreplay +foreplay's +forerunner +forerunner's +forerunners +fores +foresail +foresail's +foresails +foresaw +foresee +foreseeable +foreseeing +foreseen +foresees +foreshadow +foreshadowed +foreshadowing +foreshadows +foreshorten +foreshortened +foreshortening +foreshortens +foresight +foresight's +foreskin +foreskin's +foreskins +forest +forest's +forestall +forestalled +forestalling +forestalls +forestation +forestation's +forested +forester +forester's +foresters +foresting +forestry +forestry's +forests +foreswear +foreswearing +foreswears +foreswore +foresworn +foretaste +foretaste's +foretasted +foretastes +foretasting +foretell +foretelling +foretells +forethought +forethought's +foretold +forever +forever's +forevermore +forewarn +forewarned +forewarning +forewarns +forewent +forewoman +forewoman's +forewomen +foreword +foreword's +forewords +forfeit +forfeit's +forfeited +forfeiting +forfeits +forfeiture +forfeiture's +forgather +forgathered +forgathering +forgathers +forgave +forge +forge's +forged +forger +forger's +forgeries +forgers +forgery +forgery's +forges +forget +forgetful +forgetfully +forgetfulness +forgetfulness's +forgets +forgettable +forgetting +forging +forgivable +forgive +forgiven +forgiveness +forgiveness's +forgives +forgiving +forgo +forgoes +forgoing +forgone +forgot +forgotten +fork +fork's +forked +forking +forklift +forklift's +forklifts +forks +forlorn +forlornly +form +form's +formal +formal's +formaldehyde +formaldehyde's +formalism +formalism's +formalities +formality +formality's +formalization +formalization's +formalize +formalized +formalizes +formalizing +formally +formals +format +format's +formation +formation's +formations +formative +formats +formatted +formatting +formed +former +former's +formerly +formidable +formidably +forming +formless +formlessly +formlessness +formlessness's +forms +formula +formula's +formulae +formulaic +formulas +formulate +formulated +formulates +formulating +formulation +formulation's +formulations +fornicate +fornicated +fornicates +fornicating +fornication +fornication's +forsake +forsaken +forsakes +forsaking +forsook +forsooth +forswear +forswearing +forswears +forswore +forsworn +forsythia +forsythia's +forsythias +fort +fort's +forte +forte's +fortes +forth +forthcoming +forthcoming's +forthright +forthrightly +forthrightness +forthrightness's +forthwith +forties +fortieth +fortieth's +fortieths +fortification +fortification's +fortifications +fortified +fortifies +fortify +fortifying +fortissimo +fortitude +fortitude's +fortnight +fortnight's +fortnightly +fortnights +fortress +fortress's +fortresses +forts +fortuitous +fortuitously +fortunate +fortunately +fortune +fortune's +fortunes +forty +forty's +forum +forum's +forums +forward +forward's +forwarded +forwarder +forwardest +forwarding +forwardness +forwardness's +forwards +forwent +fossil +fossil's +fossilization +fossilization's +fossilize +fossilized +fossilizes +fossilizing +fossils +foster +fostered +fostering +fosters +fought +foul +foul's +fouled +fouler +foulest +fouling +foully +foulness +foulness's +fouls +found +foundation +foundation's +foundations +founded +founder +founder's +foundered +foundering +founders +founding +foundling +foundling's +foundlings +foundries +foundry +foundry's +founds +fount +fount's +fountain +fountain's +fountainhead +fountainhead's +fountainheads +fountains +founts +four +four's +fourfold +fours +fourscore +fourscore's +foursome +foursome's +foursomes +foursquare +fourteen +fourteen's +fourteens +fourteenth +fourteenth's +fourteenths +fourth +fourth's +fourthly +fourths +fowl +fowl's +fowled +fowling +fowls +fox +fox's +foxed +foxes +foxglove +foxglove's +foxgloves +foxhole +foxhole's +foxholes +foxhound +foxhound's +foxhounds +foxier +foxiest +foxing +foxtrot +foxtrot's +foxtrots +foxtrotted +foxtrotting +foxy +foyer +foyer's +foyers +fracas +fracas's +fracases +frack +fracked +fracking +fracks +fractal +fractal's +fractals +fraction +fraction's +fractional +fractionally +fractions +fractious +fractiously +fracture +fracture's +fractured +fractures +fracturing +fragile +fragility +fragility's +fragment +fragment's +fragmentary +fragmentary's +fragmentation +fragmentation's +fragmented +fragmenting +fragments +fragrance +fragrance's +fragrances +fragrant +fragrantly +frail +frailer +frailest +frailties +frailty +frailty's +frame +frame's +framed +framer +framer's +framers +frames +framework +framework's +frameworks +framing +franc +franc's +franchise +franchise's +franchised +franchisee +franchisee's +franchisees +franchiser +franchiser's +franchisers +franchises +franchising +francs +frank +frank's +franked +franker +frankest +frankfurter +frankfurter's +frankfurters +frankincense +frankincense's +franking +frankly +frankness +frankness's +franks +frantic +frantically +frappes +frappé +frappé's +frat +frat's +fraternal +fraternally +fraternities +fraternity +fraternity's +fraternization +fraternization's +fraternize +fraternized +fraternizes +fraternizing +fratricide +fratricide's +fratricides +frats +fraud +fraud's +frauds +fraudulence +fraudulence's +fraudulent +fraudulently +fraught +fray +fray's +frayed +fraying +frays +frazzle +frazzle's +frazzled +frazzles +frazzling +freak +freak's +freaked +freakier +freakiest +freaking +freakish +freaks +freaky +freckle +freckle's +freckled +freckles +freckling +free +freebase +freebase's +freebased +freebases +freebasing +freebee +freebee's +freebees +freebie +freebie's +freebies +freebooter +freebooter's +freebooters +freed +freedman +freedman's +freedmen +freedom +freedom's +freedoms +freehand +freehold +freehold's +freeholder +freeholder's +freeholders +freeholds +freeing +freelance +freelance's +freelanced +freelancer +freelancer's +freelancers +freelances +freelancing +freeload +freeloaded +freeloader +freeloader's +freeloaders +freeloading +freeloads +freely +freeman +freeman's +freemen +freer +frees +freest +freestanding +freestyle +freestyle's +freestyles +freethinker +freethinker's +freethinkers +freethinking +freethinking's +freeway +freeway's +freeways +freewheel +freewheeled +freewheeling +freewheels +freewill +freeze +freeze's +freezer +freezer's +freezers +freezes +freezing +freezing's +freight +freight's +freighted +freighter +freighter's +freighters +freighting +freights +french +frenetic +frenetically +frenzied +frenziedly +frenzies +frenzy +frenzy's +frequencies +frequency +frequency's +frequent +frequented +frequenter +frequentest +frequenting +frequently +frequents +fresco +fresco's +frescoes +frescos +fresh +freshen +freshened +freshening +freshens +fresher +freshest +freshet +freshet's +freshets +freshly +freshman +freshman's +freshmen +freshness +freshness's +freshwater +freshwater's +fret +fret's +fretful +fretfully +fretfulness +fretfulness's +frets +fretted +fretting +fretwork +fretwork's +friable +friar +friar's +friars +fricassee +fricassee's +fricasseed +fricasseeing +fricassees +friction +friction's +fridge +fridge's +fridges +fried +friend +friend's +friended +friending +friendless +friendlier +friendlies +friendliest +friendliness +friendliness's +friendly +friendly's +friends +friendship +friendship's +friendships +frier +frier's +friers +fries +frieze +frieze's +friezes +frigate +frigate's +frigates +fright +fright's +frighted +frighten +frightened +frightening +frighteningly +frightens +frightful +frightfully +frighting +frights +frigid +frigidity +frigidity's +frigidly +frill +frill's +frillier +frilliest +frills +frilly +fringe +fringe's +fringed +fringes +fringing +fripperies +frippery +frippery's +frisk +frisked +friskier +friskiest +friskily +friskiness +friskiness's +frisking +frisks +frisky +fritter +fritter's +frittered +frittering +fritters +frivolities +frivolity +frivolity's +frivolous +frivolously +frizz +frizz's +frizzed +frizzes +frizzier +frizziest +frizzing +frizzle +frizzle's +frizzled +frizzles +frizzling +frizzy +fro +frock +frock's +frocks +frog +frog's +frogman +frogman's +frogmen +frogs +frolic +frolic's +frolicked +frolicking +frolics +frolicsome +from +frond +frond's +fronds +front +front's +frontage +frontage's +frontages +frontal +frontally +fronted +frontier +frontier's +frontiers +frontiersman +frontiersman's +frontiersmen +fronting +frontispiece +frontispiece's +frontispieces +frontrunner +frontrunner's +frontrunners +fronts +frost +frost's +frostbit +frostbite +frostbite's +frostbites +frostbiting +frostbitten +frosted +frostier +frostiest +frostily +frostiness +frostiness's +frosting +frosting's +frostings +frosts +frosty +froth +froth's +frothed +frothier +frothiest +frothing +froths +frothy +frown +frown's +frowned +frowning +frowns +frowsier +frowsiest +frowsy +frowzier +frowziest +frowzy +froze +frozen +fructified +fructifies +fructify +fructifying +fructose +fructose's +frugal +frugality +frugality's +frugally +fruit +fruit's +fruitcake +fruitcake's +fruitcakes +fruited +fruitful +fruitfully +fruitfulness +fruitfulness's +fruitier +fruitiest +fruiting +fruition +fruition's +fruitless +fruitlessly +fruitlessness +fruitlessness's +fruits +fruity +frump +frump's +frumpier +frumpiest +frumps +frumpy +frustrate +frustrated +frustrates +frustrating +frustration +frustration's +frustrations +fry +fry's +fryer +fryer's +fryers +frying +fuchsia +fuchsia's +fuchsias +fuck +fuck's +fucked +fucker +fucker's +fuckers +fucking +fucks +fuddle +fuddle's +fuddled +fuddles +fuddling +fudge +fudge's +fudged +fudges +fudging +fuel +fuel's +fueled +fueling +fuelled +fuelling +fuels +fugitive +fugitive's +fugitives +fugue +fugue's +fugues +fulcra +fulcrum +fulcrum's +fulcrums +fulfil +fulfill +fulfilled +fulfilling +fulfillment +fulfillment's +fulfills +fulfilment +fulfilment's +fulfils +full +full's +fullback +fullback's +fullbacks +fulled +fuller +fullest +fulling +fullness +fullness's +fulls +fully +fulminate +fulminated +fulminates +fulminating +fulmination +fulmination's +fulminations +fulness +fulness's +fulsome +fumble +fumble's +fumbled +fumbler +fumbler's +fumblers +fumbles +fumbling +fume +fume's +fumed +fumes +fumigate +fumigated +fumigates +fumigating +fumigation +fumigation's +fumigator +fumigator's +fumigators +fuming +fun +fun's +function +function's +functional +functionality +functionally +functionaries +functionary +functionary's +functioned +functioning +functions +fund +fund's +fundamental +fundamental's +fundamentalism +fundamentalism's +fundamentalist +fundamentalist's +fundamentalists +fundamentally +fundamentals +funded +funding +funding's +funds +funeral +funeral's +funerals +funereal +funereally +fungal +fungi +fungicidal +fungicide +fungicide's +fungicides +fungous +fungus +fungus's +funguses +funicular +funicular's +funiculars +funk +funk's +funked +funkier +funkiest +funking +funks +funky +funnel +funnel's +funneled +funneling +funnelled +funnelling +funnels +funner +funnest +funnier +funnies +funniest +funnily +funniness +funniness's +funny +funny's +fur +fur's +furbelow +furbelow's +furbish +furbished +furbishes +furbishing +furies +furious +furiously +furl +furl's +furled +furling +furlong +furlong's +furlongs +furlough +furlough's +furloughed +furloughing +furloughs +furls +furnace +furnace's +furnaces +furnish +furnished +furnishes +furnishing +furnishings +furnishings's +furniture +furniture's +furor +furor's +furors +furred +furrier +furrier's +furriers +furriest +furring +furrow +furrow's +furrowed +furrowing +furrows +furry +furs +further +furtherance +furtherance's +furthered +furthering +furthermore +furthermost +furthers +furthest +furtive +furtively +furtiveness +furtiveness's +fury +fury's +furze +furze's +fuse +fuse's +fused +fuselage +fuselage's +fuselages +fuses +fusible +fusillade +fusillade's +fusillades +fusing +fusion +fusion's +fusions +fuss +fuss's +fussbudget +fussbudget's +fussbudgets +fussed +fusses +fussier +fussiest +fussily +fussiness +fussiness's +fussing +fussy +fustian +fustian's +fustier +fustiest +fusty +futile +futilely +futility +futility's +futon +futon's +futons +future +future's +futures +futuristic +futurities +futurity +futurity's +futz +futzed +futzes +futzing +fuze +fuze's +fuzed +fuzes +fuzing +fuzz +fuzz's +fuzzed +fuzzes +fuzzier +fuzziest +fuzzily +fuzziness +fuzziness's +fuzzing +fuzzy +fête +fête's +fêtes +g +gab +gab's +gabardine +gabardine's +gabardines +gabbed +gabbier +gabbiest +gabbing +gabble +gabble's +gabbled +gabbles +gabbling +gabby +gaberdine +gaberdine's +gaberdines +gable +gable's +gabled +gables +gabs +gad +gadabout +gadabout's +gadabouts +gadded +gadding +gadflies +gadfly +gadfly's +gadget +gadget's +gadgetry +gadgetry's +gadgets +gads +gaff +gaff's +gaffe +gaffe's +gaffed +gaffes +gaffing +gaffs +gag +gag's +gage +gage's +gaged +gages +gagged +gagging +gaggle +gaggle's +gaggles +gaging +gags +gaiety +gaiety's +gaily +gain +gain's +gained +gainful +gainfully +gaining +gains +gainsaid +gainsay +gainsaying +gainsays +gait +gait's +gaiter +gaiter's +gaiters +gaits +gal +gal's +gala +gala's +galactic +galas +galaxies +galaxy +galaxy's +gale +gale's +galena +galena's +gales +gall +gall's +gallant +gallant's +gallantly +gallantry +gallantry's +gallants +gallbladder +gallbladder's +gallbladders +galled +galleon +galleon's +galleons +galleries +gallery +gallery's +galley +galley's +galleys +galling +gallium +gallium's +gallivant +gallivanted +gallivanting +gallivants +gallon +gallon's +gallons +gallop +gallop's +galloped +galloping +gallops +gallows +gallows's +gallowses +galls +gallstone +gallstone's +gallstones +galore +galosh +galosh's +galoshes +gals +galvanic +galvanize +galvanized +galvanizes +galvanizing +galvanometer +galvanometer's +galvanometers +gambit +gambit's +gambits +gamble +gamble's +gambled +gambler +gambler's +gamblers +gambles +gambling +gambling's +gambol +gambol's +gamboled +gamboling +gambolled +gambolling +gambols +game +game's +gamecock +gamecock's +gamecocks +gamed +gamekeeper +gamekeeper's +gamekeepers +gamely +gameness +gameness's +gamer +games +gamesmanship +gamesmanship's +gamest +gamete +gamete's +gametes +gamey +gamier +gamiest +gamin +gamin's +gamine +gamine's +gamines +gaming +gamins +gamma +gamma's +gammas +gamut +gamut's +gamuts +gamy +gander +gander's +ganders +gang +gang's +ganged +ganging +gangland +gangland's +ganglia +ganglier +gangliest +gangling +ganglion +ganglion's +ganglions +gangly +gangplank +gangplank's +gangplanks +gangrene +gangrene's +gangrened +gangrenes +gangrening +gangrenous +gangs +gangster +gangster's +gangsters +gangway +gangway's +gangways +gannet +gannet's +gannets +gantlet +gantlet's +gantlets +gantries +gantry +gantry's +gap +gap's +gape +gape's +gaped +gapes +gaping +gaps +garage +garage's +garaged +garages +garaging +garb +garb's +garbage +garbage's +garbageman +garbanzo +garbanzo's +garbanzos +garbed +garbing +garble +garbled +garbles +garbling +garbs +garden +garden's +gardened +gardener +gardener's +gardeners +gardenia +gardenia's +gardenias +gardening +gardening's +gardens +gargantuan +gargle +gargle's +gargled +gargles +gargling +gargoyle +gargoyle's +gargoyles +garish +garishly +garishness +garishness's +garland +garland's +garlanded +garlanding +garlands +garlic +garlic's +garlicky +garment +garment's +garments +garner +garnered +garnering +garners +garnet +garnet's +garnets +garnish +garnish's +garnished +garnishee +garnishee's +garnisheed +garnisheeing +garnishees +garnishes +garnishing +garote +garote's +garoted +garotes +garoting +garotte +garotte's +garotted +garottes +garotting +garret +garret's +garrets +garrison +garrison's +garrisoned +garrisoning +garrisons +garrote +garrote's +garroted +garrotes +garroting +garrotte +garrotte's +garrotted +garrottes +garrotting +garrulity +garrulity's +garrulous +garrulously +garrulousness +garrulousness's +garter +garter's +garters +gas +gas's +gaseous +gases +gash +gash's +gashed +gashes +gashing +gasket +gasket's +gaskets +gaslight +gaslight's +gaslights +gasohol +gasohol's +gasolene +gasolene's +gasoline +gasoline's +gasp +gasp's +gasped +gasping +gasps +gassed +gasses +gassier +gassiest +gassing +gassy +gastric +gastritis +gastritis's +gastrointestinal +gastronomic +gastronomical +gastronomy +gastronomy's +gasworks +gasworks's +gate +gate's +gatecrasher +gatecrasher's +gatecrashers +gated +gatepost +gatepost's +gateposts +gates +gateway +gateway's +gateways +gather +gather's +gathered +gatherer +gatherer's +gatherers +gathering +gathering's +gatherings +gathers +gating +gauche +gaucher +gauchest +gaucho +gaucho's +gauchos +gaudier +gaudiest +gaudily +gaudiness +gaudiness's +gaudy +gauge +gauge's +gauged +gauges +gauging +gaunt +gaunter +gauntest +gauntlet +gauntlet's +gauntlets +gauntness +gauntness's +gauze +gauze's +gauzier +gauziest +gauzy +gave +gavel +gavel's +gavels +gavotte +gavotte's +gavottes +gawk +gawked +gawkier +gawkiest +gawkily +gawkiness +gawkiness's +gawking +gawks +gawky +gay +gay's +gayer +gayest +gayety +gayety's +gayly +gayness +gayness's +gays +gaze +gaze's +gazebo +gazebo's +gazeboes +gazebos +gazed +gazelle +gazelle's +gazelles +gazer +gazer's +gazers +gazes +gazette +gazette's +gazetted +gazetteer +gazetteer's +gazetteers +gazettes +gazetting +gazillion +gazillions +gazing +gazpacho +gazpacho's +gear +gear's +gearbox +gearbox's +gearboxes +geared +gearing +gears +gearshift +gearshift's +gearshifts +gearwheel +gearwheel's +gearwheels +gecko +gecko's +geckoes +geckos +gee +geed +geegaw +geegaw's +geegaws +geeing +geek +geek's +geekier +geekiest +geeks +geeky +gees +geese +geez +geezer +geezer's +geezers +geisha +geisha's +geishas +gel +gel's +gelatin +gelatin's +gelatine +gelatine's +gelatinous +geld +gelded +gelding +gelding's +geldings +gelds +gelid +gelled +gelling +gels +gelt +gem +gem's +gems +gemstone +gemstone's +gemstones +gendarme +gendarme's +gendarmes +gender +gender's +genders +gene +gene's +genealogical +genealogies +genealogist +genealogist's +genealogists +genealogy +genealogy's +genera +general +general's +generalissimo +generalissimo's +generalissimos +generalities +generality +generality's +generalization +generalization's +generalizations +generalize +generalized +generalizes +generalizing +generally +generals +generate +generated +generates +generating +generation +generation's +generations +generative +generator +generator's +generators +generic +generic's +generically +generics +generosities +generosity +generosity's +generous +generously +genes +geneses +genesis +genesis's +genetic +genetically +geneticist +geneticist's +geneticists +genetics +genetics's +genial +geniality +geniality's +genially +genie +genie's +genies +genii +genital +genitalia +genitalia's +genitals +genitals's +genitive +genitive's +genitives +genius +genius's +geniuses +genocide +genocide's +genome +genome's +genomes +genre +genre's +genres +gent +gent's +genteel +gentian +gentian's +gentians +gentile +gentile's +gentiles +gentility +gentility's +gentle +gentled +gentlefolk +gentlefolk's +gentleman +gentleman's +gentlemanly +gentlemen +gentleness +gentleness's +gentler +gentles +gentlest +gentlewoman +gentlewoman's +gentlewomen +gentling +gently +gentries +gentrification +gentrification's +gentrified +gentrifies +gentrify +gentrifying +gentry +gentry's +gents +genuflect +genuflected +genuflecting +genuflection +genuflection's +genuflections +genuflects +genuine +genuinely +genuineness +genuineness's +genus +genus's +genuses +geocache +geocached +geocaches +geocaching +geocentric +geode +geode's +geodes +geodesic +geodesic's +geodesics +geoengineering +geographer +geographer's +geographers +geographic +geographical +geographically +geographies +geography +geography's +geologic +geological +geologically +geologies +geologist +geologist's +geologists +geology +geology's +geometer +geometric +geometrical +geometrically +geometries +geometry +geometry's +geophysical +geophysics +geophysics's +geopolitical +geopolitics +geopolitics's +geostationary +geothermal +geranium +geranium's +geraniums +gerbil +gerbil's +gerbils +geriatric +geriatrics +geriatrics's +germ +germ's +germane +germanium +germanium's +germicidal +germicide +germicide's +germicides +germinal +germinal's +germinate +germinated +germinates +germinating +germination +germination's +germs +gerontologist +gerontologist's +gerontologists +gerontology +gerontology's +gerrymander +gerrymander's +gerrymandered +gerrymandering +gerrymandering's +gerrymanders +gerund +gerund's +gerunds +gestate +gestated +gestates +gestating +gestation +gestation's +gesticulate +gesticulated +gesticulates +gesticulating +gesticulation +gesticulation's +gesticulations +gesture +gesture's +gestured +gestures +gesturing +gesundheit +get +getaway +getaway's +getaways +gets +getting +getup +getup's +gewgaw +gewgaw's +gewgaws +geyser +geyser's +geysers +ghastlier +ghastliest +ghastliness +ghastliness's +ghastly +gherkin +gherkin's +gherkins +ghetto +ghetto's +ghettoes +ghettos +ghost +ghost's +ghosted +ghosting +ghostlier +ghostliest +ghostliness +ghostliness's +ghostly +ghosts +ghostwrite +ghostwriter +ghostwriter's +ghostwriters +ghostwrites +ghostwriting +ghostwritten +ghostwrote +ghoul +ghoul's +ghoulish +ghouls +giant +giant's +giantess +giantess's +giantesses +giants +gibber +gibbered +gibbering +gibberish +gibberish's +gibbers +gibbet +gibbet's +gibbeted +gibbeting +gibbets +gibbon +gibbon's +gibbons +gibe +gibe's +gibed +gibes +gibing +giblet +giblet's +giblets +giddier +giddiest +giddily +giddiness +giddiness's +giddy +gift +gift's +gifted +gifting +gifts +gig +gig's +gigabit +gigabit's +gigabits +gigabyte +gigabyte's +gigabytes +gigahertz +gigahertz's +gigantic +gigapixel +gigapixel's +gigapixels +gigged +gigging +giggle +giggle's +giggled +giggler +giggler's +gigglers +giggles +gigglier +giggliest +giggling +giggly +gigolo +gigolo's +gigolos +gigs +gild +gild's +gilded +gilding +gilds +gill +gill's +gills +gilt +gilt's +gilts +gimcrack +gimcrack's +gimcracks +gimlet +gimlet's +gimleted +gimleting +gimlets +gimme +gimmick +gimmick's +gimmickry +gimmickry's +gimmicks +gimmicky +gimpy +gin +gin's +ginger +ginger's +gingerbread +gingerbread's +gingerly +gingersnap +gingersnap's +gingersnaps +gingham +gingham's +gingivitis +gingivitis's +gingko +gingko's +gingkoes +gingkos +ginkgo +ginkgo's +ginkgoes +ginkgos +ginned +ginning +gins +ginseng +ginseng's +gipsies +gipsy +gipsy's +giraffe +giraffe's +giraffes +gird +girded +girder +girder's +girders +girding +girdle +girdle's +girdled +girdles +girdling +girds +girl +girl's +girlfriend +girlfriend's +girlfriends +girlhood +girlhood's +girlhoods +girlish +girlishly +girls +girt +girt's +girted +girth +girth's +girths +girting +girts +gismo +gismo's +gismos +gist +gist's +give +giveaway +giveaway's +giveaways +given +given's +givens +gives +giving +gizmo +gizmo's +gizmos +gizzard +gizzard's +gizzards +glacial +glacially +glacier +glacier's +glaciers +glad +glad's +gladden +gladdened +gladdening +gladdens +gladder +gladdest +glade +glade's +glades +gladiator +gladiator's +gladiatorial +gladiators +gladiola +gladiola's +gladiolas +gladioli +gladiolus +gladiolus's +gladioluses +gladly +gladness +gladness's +glads +glamor +glamor's +glamored +glamoring +glamorize +glamorized +glamorizes +glamorizing +glamorous +glamorously +glamors +glamour +glamour's +glamoured +glamouring +glamourize +glamourized +glamourizes +glamourizing +glamourous +glamours +glance +glance's +glanced +glances +glancing +gland +gland's +glands +glandular +glare +glare's +glared +glares +glaring +glaringly +glass +glass's +glassed +glasses +glassful +glassful's +glassfuls +glassier +glassiest +glassing +glassware +glassware's +glassy +glaucoma +glaucoma's +glaze +glaze's +glazed +glazes +glazier +glazier's +glaziers +glazing +gleam +gleam's +gleamed +gleaming +gleamings +gleams +glean +gleaned +gleaning +gleans +glee +glee's +gleeful +gleefully +glen +glen's +glens +glib +glibber +glibbest +glibly +glibness +glibness's +glide +glide's +glided +glider +glider's +gliders +glides +gliding +glimmer +glimmer's +glimmered +glimmering +glimmering's +glimmerings +glimmers +glimpse +glimpse's +glimpsed +glimpses +glimpsing +glint +glint's +glinted +glinting +glints +glissandi +glissando +glissando's +glissandos +glisten +glisten's +glistened +glistening +glistens +glitch +glitch's +glitches +glitter +glitter's +glittered +glittering +glitters +glittery +glitz +glitz's +glitzier +glitziest +glitzy +gloaming +gloaming's +gloamings +gloat +gloat's +gloated +gloating +gloats +glob +glob's +global +globalization +globally +globe +globe's +globes +globetrotter +globetrotter's +globetrotters +globs +globular +globule +globule's +globules +glockenspiel +glockenspiel's +glockenspiels +gloom +gloom's +gloomier +gloomiest +gloomily +gloominess +gloominess's +gloomy +glop +glop's +gloried +glories +glorification +glorification's +glorified +glorifies +glorify +glorifying +glorious +gloriously +glory +glory's +glorying +gloss +gloss's +glossaries +glossary +glossary's +glossed +glosses +glossier +glossies +glossiest +glossiness +glossiness's +glossing +glossy +glossy's +glottides +glottis +glottis's +glottises +glove +glove's +gloved +gloves +gloving +glow +glow's +glowed +glower +glower's +glowered +glowering +glowers +glowing +glowingly +glows +glowworm +glowworm's +glowworms +glucose +glucose's +glue +glue's +glued +glueing +glues +gluey +gluier +gluiest +gluing +glum +glumly +glummer +glummest +glumness +glumness's +glut +glut's +gluten +gluten's +glutinous +gluts +glutted +glutting +glutton +glutton's +gluttonous +gluttonously +gluttons +gluttony +gluttony's +glycerin +glycerin's +glycerine +glycerine's +glycerol +glycerol's +glycogen +glycogen's +glyph +gnarl +gnarled +gnarlier +gnarliest +gnarling +gnarls +gnarly +gnash +gnash's +gnashed +gnashes +gnashing +gnat +gnat's +gnats +gnaw +gnawed +gnawing +gnawn +gnaws +gneiss +gneiss's +gnome +gnome's +gnomes +gnomish +gnu +gnu's +gnus +go +go's +goad +goad's +goaded +goading +goads +goal +goal's +goalie +goalie's +goalies +goalkeeper +goalkeeper's +goalkeepers +goalpost +goalpost's +goalposts +goals +goaltender +goaltender's +goaltenders +goat +goat's +goatee +goatee's +goatees +goatherd +goatherd's +goatherds +goats +goatskin +goatskin's +goatskins +gob +gob's +gobbed +gobbing +gobble +gobble's +gobbled +gobbledegook +gobbledegook's +gobbledygook +gobbledygook's +gobbler +gobbler's +gobblers +gobbles +gobbling +goblet +goblet's +goblets +goblin +goblin's +goblins +gobs +god +god's +godchild +godchild's +godchildren +godchildren's +goddam +goddamed +goddamn +goddamned +goddaughter +goddaughter's +goddaughters +goddess +goddess's +goddesses +godfather +godfather's +godfathers +godforsaken +godhood +godhood's +godless +godlier +godliest +godlike +godliness +godliness's +godly +godmother +godmother's +godmothers +godparent +godparent's +godparents +gods +godsend +godsend's +godsends +godson +godson's +godsons +goes +gofer +gofer's +gofers +goggle +goggle's +goggled +goggles +goggles's +goggling +going +going's +goings +goiter +goiter's +goiters +goitre +goitre's +goitres +gold +gold's +goldbrick +goldbrick's +goldbricked +goldbricking +goldbricks +golden +goldener +goldenest +goldenrod +goldenrod's +goldfinch +goldfinch's +goldfinches +goldfish +goldfish's +goldfishes +golds +goldsmith +goldsmith's +goldsmiths +golf +golf's +golfed +golfer +golfer's +golfers +golfing +golfs +gollies +golly +golly's +gonad +gonad's +gonads +gondola +gondola's +gondolas +gondolier +gondolier's +gondoliers +gone +goner +goner's +goners +gong +gong's +gonged +gonging +gongs +gonna +gonorrhea +gonorrhea's +gonorrhoea +gonorrhoea's +goo +goo's +goober +goober's +goobers +good +good's +goodby +goodby's +goodbye +goodbye's +goodbyes +goodbys +goodie +goodie's +goodies +goodlier +goodliest +goodly +goodness +goodness's +goodnight +goods +goods's +goodwill +goodwill's +goody +goody's +gooey +goof +goof's +goofed +goofier +goofiest +goofing +goofs +goofy +google +google's +googled +googles +googling +gooier +gooiest +gook +gook's +gooks +goon +goon's +goons +goop +goop's +goose +goose's +gooseberries +gooseberry +gooseberry's +goosed +gooses +goosing +gopher +gopher's +gophers +gore +gore's +gored +gores +gorge +gorge's +gorged +gorgeous +gorgeously +gorges +gorging +gorier +goriest +gorilla +gorilla's +gorillas +goriness +goriness's +goring +gorse +gorse's +gory +gosh +gosling +gosling's +goslings +gospel +gospel's +gospels +gossamer +gossamer's +gossip +gossip's +gossiped +gossiping +gossipped +gossipping +gossips +gossipy +got +gotta +gotten +gouge +gouge's +gouged +gouger +gouger's +gougers +gouges +gouging +goulash +goulash's +goulashes +gourd +gourd's +gourds +gourmand +gourmand's +gourmands +gourmet +gourmet's +gourmets +gout +gout's +goutier +goutiest +gouty +govern +governable +governance +governance's +governed +governess +governess's +governesses +governing +government +government's +governmental +governments +governor +governor's +governors +governorship +governorship's +governs +gown +gown's +gowned +gowning +gowns +grab +grab's +grabbed +grabber +grabbing +grabs +grace +grace's +graced +graceful +gracefully +gracefulness +gracefulness's +graceless +gracelessly +gracelessness +gracelessness's +graces +gracing +gracious +graciously +graciousness +graciousness's +grackle +grackle's +grackles +grad +grad's +gradation +gradation's +gradations +grade +grade's +graded +grader +grader's +graders +grades +gradient +gradient's +gradients +grading +grads +gradual +gradually +graduate +graduate's +graduated +graduates +graduating +graduation +graduation's +graduations +graffiti +graffito +graffito's +graft +graft's +grafted +grafter +grafter's +grafters +grafting +grafts +grail +grain +grain's +grainier +grainiest +grains +grainy +gram +gram's +grammar +grammar's +grammarian +grammarian's +grammarians +grammars +grammatical +grammatically +gramophone +grams +granaries +granary +granary's +grand +grand's +grandad +grandad's +grandads +grandchild +grandchild's +grandchildren +grandchildren's +granddad +granddad's +granddads +granddaughter +granddaughter's +granddaughters +grandee +grandee's +grandees +grander +grandest +grandeur +grandeur's +grandfather +grandfather's +grandfathered +grandfathering +grandfathers +grandiloquence +grandiloquence's +grandiloquent +grandiose +grandly +grandma +grandma's +grandmas +grandmother +grandmother's +grandmothers +grandness +grandness's +grandpa +grandpa's +grandparent +grandparent's +grandparents +grandpas +grands +grandson +grandson's +grandsons +grandstand +grandstand's +grandstanded +grandstanding +grandstands +grange +grange's +granges +granite +granite's +grannie +grannie's +grannies +granny +granny's +granola +granola's +grant +grant's +granted +granting +grants +granular +granularity +granularity's +granulate +granulated +granulates +granulating +granulation +granulation's +granule +granule's +granules +grape +grape's +grapefruit +grapefruit's +grapefruits +grapes +grapevine +grapevine's +grapevines +graph +graph's +graphed +graphic +graphic's +graphical +graphically +graphics +graphing +graphite +graphite's +graphologist +graphologist's +graphologists +graphology +graphology's +graphs +grapnel +grapnel's +grapnels +grapple +grapple's +grappled +grapples +grappling +grasp +grasp's +grasped +grasping +grasps +grass +grass's +grassed +grasses +grasshopper +grasshopper's +grasshoppers +grassier +grassiest +grassing +grassland +grassland's +grassy +grate +grate's +grated +grateful +gratefully +gratefulness +gratefulness's +grater +grater's +graters +grates +gratification +gratification's +gratifications +gratified +gratifies +gratify +gratifying +grating +grating's +gratings +gratis +gratitude +gratitude's +gratuities +gratuitous +gratuitously +gratuity +gratuity's +grave +grave's +graved +gravel +gravel's +graveled +graveling +gravelled +gravelling +gravelly +gravels +gravely +graven +graver +graves +gravest +gravestone +gravestone's +gravestones +graveyard +graveyard's +graveyards +gravies +graving +gravitate +gravitated +gravitates +gravitating +gravitation +gravitation's +gravitational +gravity +gravity's +gravy +gravy's +gray +gray's +graybeard +graybeard's +graybeards +grayed +grayer +grayest +graying +grayish +grayness +grayness's +grays +graze +graze's +grazed +grazes +grazing +grease +grease's +greased +greasepaint +greasepaint's +greases +greasier +greasiest +greasiness +greasiness's +greasing +greasy +great +great's +greater +greatest +greatly +greatness +greatness's +greats +grebe +grebe's +grebes +greed +greed's +greedier +greediest +greedily +greediness +greediness's +greedy +green +green's +greenback +greenback's +greenbacks +greened +greener +greenery +greenery's +greenest +greengrocer +greengrocer's +greengrocers +greenhorn +greenhorn's +greenhorns +greenhouse +greenhouse's +greenhouses +greening +greenish +greenness +greenness's +greens +greensward +greensward's +greet +greeted +greeting +greeting's +greetings +greets +gregarious +gregariously +gregariousness +gregariousness's +gremlin +gremlin's +gremlins +grenade +grenade's +grenades +grenadier +grenadier's +grenadiers +grew +grey +grey's +greyed +greyer +greyest +greyhound +greyhound's +greyhounds +greying +greyish +greys +grid +grid's +griddle +griddle's +griddlecake +griddlecake's +griddlecakes +griddles +gridiron +gridiron's +gridirons +gridlock +gridlock's +gridlocks +grids +grief +grief's +griefs +grievance +grievance's +grievances +grieve +grieved +grieves +grieving +grievous +grievously +griffin +griffin's +griffins +grill +grill's +grille +grille's +grilled +grilles +grilling +grills +grim +grimace +grimace's +grimaced +grimaces +grimacing +grime +grime's +grimed +grimes +grimier +grimiest +griming +grimly +grimmer +grimmest +grimness +grimness's +grimy +grin +grin's +grind +grind's +grinder +grinder's +grinders +grinding +grinds +grindstone +grindstone's +grindstones +gringo +gringo's +gringos +grinned +grinning +grins +grip +grip's +gripe +gripe's +griped +gripes +griping +grippe +grippe's +gripped +gripping +grips +grislier +grisliest +grisly +grist +grist's +gristle +gristle's +gristly +grit +grit's +grits +grits's +gritted +grittier +grittiest +gritting +gritty +grizzled +grizzlier +grizzlies +grizzliest +grizzly +grizzly's +groan +groan's +groaned +groaning +groans +grocer +grocer's +groceries +grocers +grocery +grocery's +grog +grog's +groggier +groggiest +groggily +grogginess +grogginess's +groggy +groin +groin's +groins +grommet +grommet's +grommets +groom +groom's +groomed +grooming +grooming's +grooms +groove +groove's +grooved +grooves +groovier +grooviest +grooving +groovy +grope +grope's +groped +gropes +groping +grosbeak +grosbeak's +grosbeaks +gross +gross's +grossed +grosser +grosses +grossest +grossing +grossly +grossness +grossness's +grotesque +grotesque's +grotesquely +grotesques +grotto +grotto's +grottoes +grottos +grouch +grouch's +grouched +grouches +grouchier +grouchiest +grouchiness +grouchiness's +grouching +grouchy +ground +ground's +groundbreaking +groundbreaking's +groundbreakings +grounded +grounder +grounder's +grounders +groundhog +groundhog's +groundhogs +grounding +grounding's +groundings +groundless +groundlessly +grounds +groundswell +groundswell's +groundswells +groundwork +groundwork's +group +group's +grouped +grouper +grouper's +groupers +groupie +groupie's +groupies +grouping +grouping's +groupings +groups +grouse +grouse's +groused +grouses +grousing +grout +grout's +grouted +grouting +grouts +grove +grove's +grovel +groveled +groveler +groveler's +grovelers +groveling +grovelled +groveller +groveller's +grovellers +grovelling +grovels +groves +grow +grower +grower's +growers +growing +growl +growl's +growled +growling +growls +grown +grownup +grownup's +grownups +grows +growth +growth's +growths +grub +grub's +grubbed +grubbier +grubbiest +grubbiness +grubbiness's +grubbing +grubby +grubs +grubstake +grubstake's +grudge +grudge's +grudged +grudges +grudging +grudgingly +gruel +gruel's +grueling +gruelings +gruelling +gruellings +gruesome +gruesomely +gruesomer +gruesomest +gruff +gruffer +gruffest +gruffly +gruffness +gruffness's +grumble +grumble's +grumbled +grumbler +grumbler's +grumblers +grumbles +grumbling +grumpier +grumpiest +grumpily +grumpiness +grumpiness's +grumpy +grunge +grunge's +grungier +grungiest +grungy +grunt +grunt's +grunted +grunting +grunts +gryphon +gryphon's +gryphons +gs +guacamole +guacamole's +guano +guano's +guarantee +guarantee's +guaranteed +guaranteeing +guarantees +guarantied +guaranties +guarantor +guarantor's +guarantors +guaranty +guaranty's +guarantying +guard +guard's +guarded +guardedly +guardhouse +guardhouse's +guardhouses +guardian +guardian's +guardians +guardianship +guardianship's +guarding +guardrail +guardrail's +guardrails +guardroom +guardroom's +guardrooms +guards +guardsman +guardsman's +guardsmen +guava +guava's +guavas +gubernatorial +guerilla +guerilla's +guerillas +guerrilla +guerrilla's +guerrillas +guess +guess's +guessable +guessed +guesser +guesser's +guessers +guesses +guessing +guesstimate +guesstimate's +guesstimated +guesstimates +guesstimating +guesswork +guesswork's +guest +guest's +guested +guesting +guests +guff +guff's +guffaw +guffaw's +guffawed +guffawing +guffaws +guidance +guidance's +guide +guide's +guidebook +guidebook's +guidebooks +guided +guideline +guideline's +guidelines +guides +guiding +guild +guild's +guilder +guilder's +guilders +guilds +guile +guile's +guileful +guileless +guillotine +guillotine's +guillotined +guillotines +guillotining +guilt +guilt's +guiltier +guiltiest +guiltily +guiltiness +guiltiness's +guiltless +guilty +guinea +guinea's +guineas +guise +guise's +guises +guitar +guitar's +guitarist +guitarist's +guitarists +guitars +gulag +gulag's +gulags +gulch +gulch's +gulches +gulf +gulf's +gulfs +gull +gull's +gulled +gullet +gullet's +gullets +gulley +gulley's +gullibility +gullibility's +gullible +gullies +gulling +gulls +gully +gully's +gulp +gulp's +gulped +gulping +gulps +gum +gum's +gumbo +gumbo's +gumbos +gumdrop +gumdrop's +gumdrops +gummed +gummier +gummiest +gumming +gummy +gumption +gumption's +gums +gun +gun's +gunboat +gunboat's +gunboats +gunfight +gunfight's +gunfights +gunfire +gunfire's +gunk +gunk's +gunman +gunman's +gunmen +gunned +gunner +gunner's +gunners +gunnery +gunnery's +gunning +gunny +gunny's +gunnysack +gunnysack's +gunnysacks +gunpoint +gunpoint's +gunpowder +gunpowder's +gunrunner +gunrunner's +gunrunners +gunrunning +gunrunning's +guns +gunshot +gunshot's +gunshots +gunslinger +gunslinger's +gunslingers +gunsmith +gunsmith's +gunsmiths +gunwale +gunwale's +gunwales +guppies +guppy +guppy's +gurgle +gurgle's +gurgled +gurgles +gurgling +gurney +gurney's +gurneys +guru +guru's +gurus +gush +gush's +gushed +gusher +gusher's +gushers +gushes +gushier +gushiest +gushing +gushy +gusset +gusset's +gusseted +gusseting +gussets +gust +gust's +gustatory +gusted +gustier +gustiest +gusting +gusto +gusto's +gusts +gusty +gut +gut's +gutless +guts +gutsier +gutsiest +gutsy +gutted +gutter +gutter's +guttered +guttering +gutters +guttersnipe +guttersnipe's +guttersnipes +gutting +guttural +guttural's +gutturals +guy +guy's +guyed +guying +guys +guzzle +guzzled +guzzler +guzzler's +guzzlers +guzzles +guzzling +gybe +gybe's +gybed +gybes +gybing +gym +gym's +gymnasia +gymnasium +gymnasium's +gymnasiums +gymnast +gymnast's +gymnastic +gymnastics +gymnastics's +gymnasts +gymnosperm +gymnosperm's +gymnosperms +gyms +gynecological +gynecologist +gynecologist's +gynecologists +gynecology +gynecology's +gyp +gyp's +gypped +gypping +gyps +gypsies +gypsum +gypsum's +gypsy +gypsy's +gyrate +gyrated +gyrates +gyrating +gyration +gyration's +gyrations +gyro +gyro's +gyros +gyroscope +gyroscope's +gyroscopes +h +h'm +ha +haberdasher +haberdasher's +haberdasheries +haberdashers +haberdashery +haberdashery's +habit +habit's +habitability +habitability's +habitable +habitat +habitat's +habitation +habitation's +habitations +habitats +habits +habitual +habitually +habituate +habituated +habituates +habituating +habituation +habituation's +habitué +habitué's +habitués +hacienda +hacienda's +haciendas +hack +hack's +hacked +hacker +hacker's +hackers +hacking +hackle +hackle's +hackles +hackney +hackney's +hackneyed +hackneying +hackneys +hacks +hacksaw +hacksaw's +hacksaws +hacktivist +hacktivist's +hacktivists +had +haddock +haddock's +haddocks +hadn't +haemoglobin +haemoglobin's +haemophilia +haemophilia's +haemorrhage +haemorrhage's +haemorrhaged +haemorrhages +haemorrhaging +haemorrhoids +hafnium +hafnium's +haft +haft's +hafts +hag +hag's +haggard +haggle +haggle's +haggled +haggler +haggler's +hagglers +haggles +haggling +hags +hah +haiku +haiku's +hail +hail's +hailed +hailing +hails +hailstone +hailstone's +hailstones +hailstorm +hailstorm's +hailstorms +hair +hair's +hairbreadth +hairbreadth's +hairbreadths +hairbrush +hairbrush's +hairbrushes +haircut +haircut's +haircuts +hairdo +hairdo's +hairdos +hairdresser +hairdresser's +hairdressers +hairdressing +hairdressing's +haired +hairier +hairiest +hairiness +hairiness's +hairless +hairline +hairline's +hairlines +hairnet +hairnet's +hairnets +hairpiece +hairpiece's +hairpieces +hairpin +hairpin's +hairpins +hairs +hairsbreadth +hairsbreadth's +hairsbreadths +hairsplitting +hairsplitting's +hairspring +hairspring's +hairsprings +hairstyle +hairstyle's +hairstyles +hairstylist +hairstylist's +hairstylists +hairy +hake +hake's +hakes +halberd +halberd's +halberds +halcyon +hale +haled +haler +hales +halest +half +half's +halfback +halfback's +halfbacks +halfhearted +halfheartedly +halfheartedness +halfheartedness's +halfpence +halfpennies +halfpenny +halfpenny's +halftime +halftime's +halftimes +halfway +halibut +halibut's +halibuts +haling +halitosis +halitosis's +hall +hall's +halleluiah +halleluiah's +halleluiahs +hallelujah +hallelujah's +hallelujahs +hallmark +hallmark's +hallmarked +hallmarking +hallmarks +hallow +hallowed +hallowing +hallows +halls +hallucinate +hallucinated +hallucinates +hallucinating +hallucination +hallucination's +hallucinations +hallucinatory +hallucinogen +hallucinogen's +hallucinogenic +hallucinogenic's +hallucinogenics +hallucinogens +hallway +hallway's +hallways +halo +halo's +haloed +haloes +halogen +halogen's +halogens +haloing +halon +halos +halt +halt's +halted +halter +halter's +haltered +haltering +halters +halting +haltingly +halts +halve +halved +halves +halving +halyard +halyard's +halyards +ham +ham's +hamburger +hamburger's +hamburgers +hamlet +hamlet's +hamlets +hammed +hammer +hammer's +hammered +hammerhead +hammerhead's +hammerheads +hammering +hammerings +hammers +hamming +hammock +hammock's +hammocks +hamper +hamper's +hampered +hampering +hampers +hams +hamster +hamster's +hamsters +hamstring +hamstring's +hamstringing +hamstrings +hamstrung +hand +hand's +handbag +handbag's +handbags +handball +handball's +handballs +handbill +handbill's +handbills +handbook +handbook's +handbooks +handcar +handcar's +handcars +handcart +handcart's +handcarts +handcraft +handcraft's +handcrafted +handcrafting +handcrafts +handcuff +handcuff's +handcuffed +handcuffing +handcuffs +handed +handedness +handful +handful's +handfuls +handgun +handgun's +handguns +handheld +handheld's +handhelds +handicap +handicap's +handicapped +handicapper +handicapper's +handicappers +handicapping +handicaps +handicraft +handicraft's +handicrafts +handier +handiest +handily +handiness +handiness's +handing +handiwork +handiwork's +handkerchief +handkerchief's +handkerchiefs +handkerchieves +handle +handle's +handlebar +handlebar's +handlebars +handled +handler +handler's +handlers +handles +handling +handmade +handmaid +handmaid's +handmaiden +handmaiden's +handmaidens +handmaids +handout +handout's +handouts +handpick +handpicked +handpicking +handpicks +handrail +handrail's +handrails +hands +handset +handset's +handsets +handsful +handshake +handshake's +handshakes +handshaking +handsome +handsomely +handsomeness +handsomeness's +handsomer +handsomest +handspring +handspring's +handsprings +handstand +handstand's +handstands +handwork +handwork's +handwriting +handwriting's +handwritten +handy +handyman +handyman's +handymen +hang +hang's +hangar +hangar's +hangars +hangdog +hanged +hanger +hanger's +hangers +hanging +hanging's +hangings +hangman +hangman's +hangmen +hangnail +hangnail's +hangnails +hangout +hangout's +hangouts +hangover +hangover's +hangovers +hangs +hank +hank's +hanker +hankered +hankering +hankering's +hankerings +hankers +hankie +hankie's +hankies +hanks +hanky +hanky's +hansom +hansom's +hansoms +haphazard +haphazardly +hapless +happen +happened +happening +happening's +happenings +happens +happenstance +happenstance's +happenstances +happier +happiest +happily +happiness +happiness's +happy +harangue +harangue's +harangued +harangues +haranguing +harass +harassed +harasses +harassing +harassment +harassment's +harbinger +harbinger's +harbingers +harbor +harbor's +harbored +harboring +harbors +hard +hardback +hardback's +hardbacks +hardball +hardball's +hardcover +hardcover's +hardcovers +harden +hardened +hardener +hardener's +hardeners +hardening +hardens +harder +hardest +hardheaded +hardheadedly +hardheadedness +hardheadedness's +hardhearted +hardheartedly +hardheartedness +hardheartedness's +hardier +hardiest +hardily +hardiness +hardiness's +hardline +hardliner +hardliner's +hardliners +hardly +hardness +hardness's +hardship +hardship's +hardships +hardtack +hardtack's +hardtop +hardtop's +hardtops +hardware +hardware's +hardwood +hardwood's +hardwoods +hardy +hare +hare's +harebrained +hared +harelip +harelip's +harelips +harem +harem's +harems +hares +haring +hark +harked +harken +harkened +harkening +harkens +harking +harks +harlequin +harlequin's +harlequins +harlot +harlot's +harlots +harm +harm's +harmed +harmful +harmfully +harmfulness +harmfulness's +harming +harmless +harmlessly +harmlessness +harmlessness's +harmonic +harmonic's +harmonica +harmonica's +harmonically +harmonicas +harmonics +harmonies +harmonious +harmoniously +harmoniousness +harmoniousness's +harmonization +harmonization's +harmonize +harmonized +harmonizes +harmonizing +harmony +harmony's +harms +harness +harness's +harnessed +harnesses +harnessing +harp +harp's +harped +harpies +harping +harpist +harpist's +harpists +harpoon +harpoon's +harpooned +harpooning +harpoons +harps +harpsichord +harpsichord's +harpsichords +harpy +harpy's +harridan +harridan's +harridans +harried +harries +harrow +harrow's +harrowed +harrowing +harrows +harry +harrying +harsh +harsher +harshest +harshly +harshness +harshness's +hart +hart's +harts +harvest +harvest's +harvested +harvester +harvester's +harvesters +harvesting +harvests +has +hash +hash's +hashed +hasheesh +hasheesh's +hashes +hashing +hashish +hashish's +hashtag +hashtag's +hashtags +hasn't +hasp +hasp's +hasps +hassle +hassle's +hassled +hassles +hassling +hassock +hassock's +hassocks +haste +haste's +hasted +hasten +hastened +hastening +hastens +hastes +hastier +hastiest +hastily +hastiness +hastiness's +hasting +hasty +hat +hat's +hatch +hatch's +hatchback +hatchback's +hatchbacks +hatched +hatcheries +hatchery +hatchery's +hatches +hatchet +hatchet's +hatchets +hatching +hatching's +hatchway +hatchway's +hatchways +hate +hate's +hated +hateful +hatefully +hatefulness +hatefulness's +hater +hater's +haters +hates +hath +hating +hatred +hatred's +hatreds +hats +hatted +hatter +hatter's +hatters +hatting +haughtier +haughtiest +haughtily +haughtiness +haughtiness's +haughty +haul +haul's +hauled +hauler +hauler's +haulers +hauling +hauls +haunch +haunch's +haunches +haunt +haunt's +haunted +haunting +hauntingly +haunts +hauteur +hauteur's +have +have's +haven +haven's +haven't +havens +haversack +haversack's +haversacks +haves +having +havoc +havoc's +haw +haw's +hawed +hawing +hawk +hawk's +hawked +hawker +hawker's +hawkers +hawking +hawkish +hawks +haws +hawser +hawser's +hawsers +hawthorn +hawthorn's +hawthorns +hay +hay's +haycock +haycock's +haycocks +hayed +haying +hayloft +hayloft's +haylofts +haymow +haymow's +haymows +hays +hayseed +hayseed's +hayseeds +haystack +haystack's +haystacks +haywire +hazard +hazard's +hazarded +hazarding +hazardous +hazards +haze +haze's +hazed +hazel +hazel's +hazelnut +hazelnut's +hazelnuts +hazels +hazes +hazier +haziest +hazily +haziness +haziness's +hazing +hazing's +hazings +hazmat +hazy +he +he'd +he'll +he's +head +head's +headache +headache's +headaches +headband +headband's +headbands +headboard +headboard's +headboards +headdress +headdress's +headdresses +headed +header +header's +headers +headfirst +headgear +headgear's +headhunter +headhunter's +headhunters +headier +headiest +heading +heading's +headings +headland +headland's +headlands +headless +headlight +headlight's +headlights +headline +headline's +headlined +headlines +headlining +headlock +headlock's +headlocks +headlong +headmaster +headmaster's +headmasters +headmistress +headmistress's +headmistresses +headphone +headphone's +headphones +headquarter +headquarters +headquarters's +headrest +headrest's +headrests +headroom +headroom's +heads +headset +headset's +headsets +headstone +headstone's +headstones +headstrong +headwaiter +headwaiter's +headwaiters +headwaters +headwaters's +headway +headway's +headwind +headwind's +headwinds +headword +headword's +headwords +heady +heal +healed +healer +healer's +healers +healing +heals +health +health's +healthcare +healthful +healthfully +healthfulness +healthfulness's +healthier +healthiest +healthily +healthiness +healthiness's +healthy +heap +heap's +heaped +heaping +heaps +hear +heard +hearer +hearer's +hearers +hearing +hearing's +hearings +hearken +hearkened +hearkening +hearkens +hears +hearsay +hearsay's +hearse +hearse's +hearses +heart +heart's +heartache +heartache's +heartaches +heartbeat +heartbeat's +heartbeats +heartbreak +heartbreak's +heartbreaking +heartbreaks +heartbroken +heartburn +heartburn's +hearten +heartened +heartening +heartens +heartfelt +hearth +hearth's +hearths +heartier +hearties +heartiest +heartily +heartiness +heartiness's +heartland +heartland's +heartlands +heartless +heartlessly +heartlessness +heartlessness's +heartrending +hearts +heartsick +heartstrings +heartstrings's +heartthrob +heartthrob's +heartthrobs +heartwarming +hearty +hearty's +heat +heat's +heated +heatedly +heater +heater's +heaters +heath +heath's +heathen +heathen's +heathenish +heathens +heather +heather's +heaths +heating +heats +heatstroke +heatstroke's +heave +heave's +heaved +heaven +heaven's +heavenlier +heavenliest +heavenly +heavens +heavens's +heavenward +heavenwards +heaves +heavier +heavies +heaviest +heavily +heaviness +heaviness's +heaving +heavy +heavy's +heavyset +heavyweight +heavyweight's +heavyweights +heck +heck's +heckle +heckle's +heckled +heckler +heckler's +hecklers +heckles +heckling +heckling's +hectare +hectare's +hectares +hectic +hectically +hector +hector's +hectored +hectoring +hectors +hedge +hedge's +hedged +hedgehog +hedgehog's +hedgehogs +hedgerow +hedgerow's +hedgerows +hedges +hedging +hedonism +hedonism's +hedonist +hedonist's +hedonistic +hedonists +heed +heed's +heeded +heedful +heeding +heedless +heedlessly +heedlessness +heedlessness's +heeds +heehaw +heehaw's +heehawed +heehawing +heehaws +heel +heel's +heeled +heeling +heels +heft +heft's +hefted +heftier +heftiest +hefting +hefts +hefty +hegemony +hegemony's +heifer +heifer's +heifers +height +height's +heighten +heightened +heightening +heightens +heights +heinous +heinously +heinousness +heinousness's +heir +heir's +heiress +heiress's +heiresses +heirloom +heirloom's +heirlooms +heirs +heist +heist's +heisted +heisting +heists +held +helical +helices +helicopter +helicopter's +helicoptered +helicoptering +helicopters +heliotrope +heliotrope's +heliotropes +heliport +heliport's +heliports +helium +helium's +helix +helix's +helixes +hell +hell's +hellebore +hellebore's +hellhole +hellhole's +hellholes +hellion +hellion's +hellions +hellish +hellishly +hello +hello's +hellos +helm +helm's +helmet +helmet's +helmets +helms +helmsman +helmsman's +helmsmen +helot +helot's +helots +help +help's +helped +helper +helper's +helpers +helpful +helpfully +helpfulness +helpfulness's +helping +helping's +helpings +helpless +helplessly +helplessness +helplessness's +helpline +helpline's +helplines +helpmate +helpmate's +helpmates +helpmeet +helpmeet's +helpmeets +helps +hem +hem's +hematologist +hematologist's +hematologists +hematology +hematology's +hemisphere +hemisphere's +hemispheres +hemispheric +hemispherical +hemline +hemline's +hemlines +hemlock +hemlock's +hemlocks +hemmed +hemming +hemoglobin +hemoglobin's +hemophilia +hemophilia's +hemophiliac +hemophiliac's +hemophiliacs +hemorrhage +hemorrhage's +hemorrhaged +hemorrhages +hemorrhaging +hemorrhoid +hemorrhoid's +hemorrhoids +hemp +hemp's +hempen +hems +hemstitch +hemstitch's +hemstitched +hemstitches +hemstitching +hen +hen's +hence +henceforth +henceforward +henchman +henchman's +henchmen +henna +henna's +hennaed +hennaing +hennas +henpeck +henpecked +henpecking +henpecks +hens +hep +hepatic +hepatitis +hepatitis's +hepper +heppest +heptagon +heptagon's +heptagons +her +herald +herald's +heralded +heraldic +heralding +heraldry +heraldry's +heralds +herb +herb's +herbaceous +herbage +herbage's +herbal +herbalist +herbalist's +herbalists +herbicide +herbicide's +herbicides +herbivore +herbivore's +herbivores +herbivorous +herbs +herculean +herd +herd's +herded +herder +herder's +herders +herding +herds +herdsman +herdsman's +herdsmen +here +here's +hereabout +hereabouts +hereafter +hereafter's +hereafters +hereby +hereditary +heredity +heredity's +herein +hereof +heresies +heresy +heresy's +heretic +heretic's +heretical +heretics +hereto +heretofore +hereupon +herewith +heritage +heritage's +heritages +hermaphrodite +hermaphrodite's +hermaphrodites +hermaphroditic +hermetic +hermetically +hermit +hermit's +hermitage +hermitage's +hermitages +hermits +hernia +hernia's +herniae +hernias +hero +hero's +heroes +heroic +heroically +heroics +heroics's +heroin +heroin's +heroine +heroine's +heroins +heroism +heroism's +heron +heron's +herons +heros +herpes +herpes's +herring +herring's +herringbone +herringbone's +herrings +hers +herself +hertz +hertz's +hertzes +hes +hesitancy +hesitancy's +hesitant +hesitantly +hesitate +hesitated +hesitates +hesitating +hesitatingly +hesitation +hesitation's +hesitations +heterodox +heterodoxy +heterodoxy's +heterogeneity +heterogeneity's +heterogeneous +heterosexual +heterosexual's +heterosexuality +heterosexuality's +heterosexuals +heuristic +heuristic's +heuristics +hew +hewed +hewer +hewer's +hewers +hewing +hewn +hews +hex +hex's +hexadecimal +hexagon +hexagon's +hexagonal +hexagons +hexameter +hexameter's +hexameters +hexed +hexes +hexing +hey +heyday +heyday's +heydays +hi +hiatus +hiatus's +hiatuses +hibachi +hibachi's +hibachis +hibernate +hibernated +hibernates +hibernating +hibernation +hibernation's +hibiscus +hibiscus's +hibiscuses +hiccough +hiccough's +hiccoughed +hiccoughing +hiccoughs +hiccup +hiccup's +hiccuped +hiccuping +hiccups +hick +hick's +hickey +hickey's +hickeys +hickories +hickory +hickory's +hicks +hid +hidden +hide +hide's +hideaway +hideaway's +hideaways +hidebound +hided +hideous +hideously +hideousness +hideousness's +hideout +hideout's +hideouts +hides +hiding +hiding's +hie +hied +hieing +hierarchical +hierarchically +hierarchies +hierarchy +hierarchy's +hieroglyphic +hieroglyphic's +hieroglyphics +hies +hifalutin +high +high's +highball +highball's +highballs +highborn +highboy +highboy's +highboys +highbrow +highbrow's +highbrows +highchair +highchair's +highchairs +higher +highest +highfalutin +highfaluting +highjack +highjack's +highjacked +highjacker +highjacker's +highjackers +highjacking +highjacks +highland +highland's +highlands +highlight +highlight's +highlighted +highlighter +highlighter's +highlighters +highlighting +highlights +highly +highness +highness's +highs +hightail +hightailed +hightailing +hightails +highway +highway's +highwayman +highwayman's +highwaymen +highways +hijack +hijack's +hijacked +hijacker +hijacker's +hijackers +hijacking +hijacking's +hijackings +hijacks +hike +hike's +hiked +hiker +hiker's +hikers +hikes +hiking +hilarious +hilariously +hilarity +hilarity's +hill +hill's +hillbillies +hillbilly +hillbilly's +hillier +hilliest +hillock +hillock's +hillocks +hills +hillside +hillside's +hillsides +hilltop +hilltop's +hilltops +hilly +hilt +hilt's +hilts +him +hims +himself +hind +hind's +hinder +hindered +hindering +hinders +hindmost +hindquarter +hindquarter's +hindquarters +hindrance +hindrance's +hindrances +hinds +hindsight +hindsight's +hinge +hinge's +hinged +hinges +hinging +hint +hint's +hinted +hinterland +hinterland's +hinterlands +hinting +hints +hip +hip's +hipped +hipper +hippest +hippie +hippie's +hippies +hipping +hippo +hippo's +hippopotami +hippopotamus +hippopotamus's +hippopotamuses +hippos +hippy +hippy's +hips +hire +hire's +hired +hireling +hireling's +hirelings +hires +hiring +hirsute +his +hiss +hiss's +hissed +hisses +hissing +histamine +histamine's +histamines +histogram +histogram's +histograms +historian +historian's +historians +historic +historical +historically +histories +history +history's +histrionic +histrionics +histrionics's +hit +hit's +hitch +hitch's +hitched +hitches +hitchhike +hitchhike's +hitchhiked +hitchhiker +hitchhiker's +hitchhikers +hitchhikes +hitchhiking +hitching +hither +hitherto +hits +hitter +hitter's +hitters +hitting +hive +hive's +hived +hives +hiving +ho +ho's +hoagie +hoagie's +hoagies +hoagy +hoagy's +hoard +hoard's +hoarded +hoarder +hoarder's +hoarders +hoarding +hoards +hoarfrost +hoarfrost's +hoarier +hoariest +hoariness +hoariness's +hoarse +hoarsely +hoarseness +hoarseness's +hoarser +hoarsest +hoary +hoax +hoax's +hoaxed +hoaxer +hoaxer's +hoaxers +hoaxes +hoaxing +hob +hob's +hobbies +hobbit +hobble +hobble's +hobbled +hobbles +hobbling +hobby +hobby's +hobbyhorse +hobbyhorse's +hobbyhorses +hobbyist +hobbyist's +hobbyists +hobgoblin +hobgoblin's +hobgoblins +hobnail +hobnail's +hobnailed +hobnailing +hobnails +hobnob +hobnobbed +hobnobbing +hobnobs +hobo +hobo's +hoboes +hobos +hobs +hock +hock's +hocked +hockey +hockey's +hocking +hocks +hockshop +hockshop's +hockshops +hod +hod's +hodgepodge +hodgepodge's +hodgepodges +hods +hoe +hoe's +hoed +hoedown +hoedown's +hoedowns +hoeing +hoes +hog +hog's +hogan +hogan's +hogans +hogged +hogging +hoggish +hogs +hogshead +hogshead's +hogsheads +hogwash +hogwash's +hoist +hoist's +hoisted +hoisting +hoists +hokey +hokier +hokiest +hokum +hokum's +hold +hold's +holder +holder's +holders +holding +holding's +holdings +holdout +holdout's +holdouts +holdover +holdover's +holdovers +holds +holdup +holdup's +holdups +hole +hole's +holed +holes +holiday +holiday's +holidayed +holidaying +holidays +holier +holiest +holiness +holiness's +holing +holistic +holler +holler's +hollered +hollering +hollers +hollies +hollow +hollow's +hollowed +hollower +hollowest +hollowing +hollowly +hollowness +hollowness's +hollows +holly +holly's +hollyhock +hollyhock's +hollyhocks +holocaust +holocaust's +holocausts +hologram +hologram's +holograms +holograph +holograph's +holographic +holographs +holography +holography's +holster +holster's +holstered +holstering +holsters +holy +homage +homage's +homages +homburg +homburg's +homburgs +home +home's +homebodies +homebody +homebody's +homeboy +homeboy's +homeboys +homecoming +homecoming's +homecomings +homed +homegrown +homeland +homeland's +homelands +homeless +homeless's +homelessness +homelessness's +homelier +homeliest +homeliness +homeliness's +homely +homemade +homemaker +homemaker's +homemakers +homeopathic +homeopathy +homeopathy's +homeowner +homeowner's +homeowners +homepage +homepage's +homepages +homer +homer's +homered +homering +homeroom +homeroom's +homerooms +homers +homes +homesick +homesickness +homesickness's +homespun +homespun's +homestead +homestead's +homesteaded +homesteader +homesteader's +homesteaders +homesteading +homesteads +homestretch +homestretch's +homestretches +hometown +hometown's +hometowns +homeward +homewards +homework +homework's +homewrecker +homewrecker's +homewreckers +homey +homey's +homeyness +homeyness's +homeys +homicidal +homicide +homicide's +homicides +homie +homie's +homier +homies +homiest +homilies +homily +homily's +hominess +hominess's +homing +hominy +hominy's +homogeneity +homogeneity's +homogeneous +homogeneously +homogenization +homogenization's +homogenize +homogenized +homogenizes +homogenizing +homograph +homograph's +homographs +homonym +homonym's +homonyms +homophobia +homophobia's +homophobic +homophone +homophone's +homophones +homosexual +homosexual's +homosexuality +homosexuality's +homosexuals +homy +honcho +honcho's +honchos +hone +hone's +honed +hones +honest +honester +honestest +honestly +honesty +honesty's +honey +honey's +honeybee +honeybee's +honeybees +honeycomb +honeycomb's +honeycombed +honeycombing +honeycombs +honeydew +honeydew's +honeydews +honeyed +honeying +honeymoon +honeymoon's +honeymooned +honeymooner +honeymooner's +honeymooners +honeymooning +honeymoons +honeys +honeysuckle +honeysuckle's +honeysuckles +honied +honing +honk +honk's +honked +honking +honks +honor +honor's +honorable +honorably +honoraria +honorarium +honorarium's +honorariums +honorary +honored +honorific +honorific's +honorifics +honoring +honors +hooch +hooch's +hood +hood's +hooded +hoodie +hoodie's +hoodies +hooding +hoodlum +hoodlum's +hoodlums +hoodoo +hoodoo's +hoodooed +hoodooing +hoodoos +hoods +hoodwink +hoodwinked +hoodwinking +hoodwinks +hooey +hooey's +hoof +hoof's +hoofed +hoofing +hoofs +hook +hook's +hookah +hookah's +hookahs +hooked +hooker +hooker's +hookers +hookey +hookey's +hooking +hooks +hookup +hookup's +hookups +hookworm +hookworm's +hookworms +hooky +hooky's +hooligan +hooligan's +hooliganism +hooliganism's +hooligans +hoop +hoop's +hooped +hooping +hoopla +hoopla's +hoops +hoorah +hoorah's +hoorahs +hooray +hooray's +hoorayed +hooraying +hoorays +hoot +hoot's +hootch +hootch's +hooted +hooter +hooter's +hooters +hooting +hoots +hooves +hop +hop's +hope +hope's +hoped +hopeful +hopeful's +hopefully +hopefulness +hopefulness's +hopefuls +hopeless +hopelessly +hopelessness +hopelessness's +hopes +hoping +hopped +hopper +hopper's +hoppers +hopping +hops +hopscotch +hopscotch's +hopscotched +hopscotches +hopscotching +horde +horde's +horded +hordes +hording +horizon +horizon's +horizons +horizontal +horizontal's +horizontally +horizontals +hormonal +hormone +hormone's +hormones +horn +horn's +horned +hornet +hornet's +hornets +hornier +horniest +hornless +hornpipe +hornpipe's +hornpipes +horns +horny +horology +horology's +horoscope +horoscope's +horoscopes +horrendous +horrendously +horrible +horribly +horrid +horridly +horrific +horrified +horrifies +horrify +horrifying +horror +horror's +horrors +horse +horse's +horseback +horseback's +horsed +horseflies +horsefly +horsefly's +horsehair +horsehair's +horsehide +horsehide's +horseman +horseman's +horsemanship +horsemanship's +horsemen +horseplay +horseplay's +horsepower +horsepower's +horseradish +horseradish's +horseradishes +horses +horseshoe +horseshoe's +horseshoed +horseshoeing +horseshoes +horsetail +horsetail's +horsetails +horsewhip +horsewhip's +horsewhipped +horsewhipping +horsewhips +horsewoman +horsewoman's +horsewomen +horsey +horsier +horsiest +horsing +horsy +horticultural +horticulture +horticulture's +horticulturist +horticulturist's +horticulturists +hos +hosanna +hosanna's +hosannas +hose +hose's +hosed +hoses +hosiery +hosiery's +hosing +hospice +hospice's +hospices +hospitable +hospitably +hospital +hospital's +hospitality +hospitality's +hospitalization +hospitalization's +hospitalizations +hospitalize +hospitalized +hospitalizes +hospitalizing +hospitals +host +host's +hostage +hostage's +hostages +hosted +hostel +hostel's +hosteled +hosteler +hosteler's +hostelers +hosteling +hostelled +hostelling +hostelries +hostelry +hostelry's +hostels +hostess +hostess's +hostessed +hostesses +hostessing +hostile +hostile's +hostilely +hostiles +hostilities +hostilities's +hostility +hostility's +hosting +hostler +hostler's +hostlers +hosts +hot +hotbed +hotbed's +hotbeds +hotcake +hotcake's +hotcakes +hotel +hotel's +hotelier +hotelier's +hoteliers +hotels +hothead +hothead's +hotheaded +hotheadedly +hotheadedness +hotheadedness's +hotheads +hothouse +hothouse's +hothouses +hotkey +hotkeys +hotly +hotness +hotness's +hotshot +hotshot's +hotshots +hotter +hottest +hoummos +houmous +hound +hound's +hounded +hounding +hounds +hour +hour's +hourglass +hourglass's +hourglasses +hourly +hours +house +house's +houseboat +houseboat's +houseboats +housebound +housebreak +housebreaking +housebreaking's +housebreaks +housebroke +housebroken +houseclean +housecleaned +housecleaning +housecleaning's +housecleans +housecoat +housecoat's +housecoats +housed +houseflies +housefly +housefly's +household +household's +householder +householder's +householders +households +househusband +househusband's +househusbands +housekeeper +housekeeper's +housekeepers +housekeeping +housekeeping's +housemaid +housemaid's +housemaids +housemother +housemother's +housemothers +houseplant +houseplant's +houseplants +houses +housetop +housetop's +housetops +housewares +housewares's +housewarming +housewarming's +housewarmings +housewife +housewife's +housewives +housework +housework's +housing +housing's +housings +hove +hovel +hovel's +hovels +hover +hovercraft +hovercraft's +hovercrafts +hovered +hovering +hovers +how +how's +howdah +howdah's +howdahs +howdy +however +howitzer +howitzer's +howitzers +howl +howl's +howled +howler +howler's +howlers +howling +howls +hows +howsoever +hub +hub's +hubbies +hubbub +hubbub's +hubbubs +hubby +hubby's +hubcap +hubcap's +hubcaps +hubris +hubris's +hubs +huckleberries +huckleberry +huckleberry's +huckster +huckster's +huckstered +huckstering +hucksters +huddle +huddle's +huddled +huddles +huddling +hue +hue's +hued +hues +huff +huff's +huffed +huffier +huffiest +huffily +huffing +huffs +huffy +hug +hug's +huge +hugely +hugeness +hugeness's +huger +hugest +hugged +hugging +hugs +huh +hula +hula's +hulas +hulk +hulk's +hulking +hulks +hull +hull's +hullabaloo +hullabaloo's +hullabaloos +hulled +hulling +hulls +hum +hum's +human +human's +humane +humanely +humaneness +humaneness's +humaner +humanest +humanism +humanism's +humanist +humanist's +humanistic +humanists +humanitarian +humanitarian's +humanitarianism +humanitarianism's +humanitarians +humanities +humanities's +humanity +humanity's +humanization +humanization's +humanize +humanized +humanizer +humanizer's +humanizers +humanizes +humanizing +humankind +humankind's +humanly +humanness +humanness's +humanoid +humanoid's +humanoids +humans +humble +humbled +humbleness +humbleness's +humbler +humbles +humblest +humbling +humblings +humbly +humbug +humbug's +humbugged +humbugging +humbugs +humdinger +humdinger's +humdingers +humdrum +humdrum's +humeri +humerus +humerus's +humid +humidified +humidifier +humidifier's +humidifiers +humidifies +humidify +humidifying +humidity +humidity's +humidor +humidor's +humidors +humiliate +humiliated +humiliates +humiliating +humiliation +humiliation's +humiliations +humility +humility's +hummed +humming +hummingbird +hummingbird's +hummingbirds +hummock +hummock's +hummocks +hummus +humongous +humor +humor's +humored +humoring +humorist +humorist's +humorists +humorless +humorlessness +humorlessness's +humorous +humorously +humors +hump +hump's +humpback +humpback's +humpbacked +humpbacks +humped +humping +humps +hums +humungous +humus +humus's +hunch +hunch's +hunchback +hunchback's +hunchbacked +hunchbacks +hunched +hunches +hunching +hundred +hundred's +hundredfold +hundreds +hundredth +hundredth's +hundredths +hundredweight +hundredweight's +hundredweights +hung +hunger +hunger's +hungered +hungering +hungers +hungover +hungrier +hungriest +hungrily +hungry +hunk +hunk's +hunker +hunkered +hunkering +hunkers +hunks +hunt +hunt's +hunted +hunter +hunter's +hunters +hunting +hunting's +huntress +huntress's +huntresses +hunts +huntsman +huntsman's +huntsmen +hurdle +hurdle's +hurdled +hurdler +hurdler's +hurdlers +hurdles +hurdling +hurl +hurl's +hurled +hurler +hurler's +hurlers +hurling +hurls +hurrah +hurrah's +hurrahed +hurrahing +hurrahs +hurray +hurray's +hurrayed +hurraying +hurrays +hurricane +hurricane's +hurricanes +hurried +hurriedly +hurries +hurry +hurry's +hurrying +hurt +hurt's +hurtful +hurting +hurtle +hurtled +hurtles +hurtling +hurts +husband +husband's +husbanded +husbanding +husbandry +husbandry's +husbands +hush +hush's +hushed +hushes +hushing +husk +husk's +husked +husker +husker's +huskers +huskier +huskies +huskiest +huskily +huskiness +huskiness's +husking +husks +husky +husky's +hussar +hussar's +hussars +hussies +hussy +hussy's +hustings +hustings's +hustle +hustle's +hustled +hustler +hustler's +hustlers +hustles +hustling +hut +hut's +hutch +hutch's +hutches +huts +hutzpa +hutzpa's +hutzpah +hutzpah's +hyacinth +hyacinth's +hyacinths +hyaena +hyaena's +hyaenas +hybrid +hybrid's +hybridize +hybridized +hybridizes +hybridizing +hybrids +hydra +hydra's +hydrae +hydrangea +hydrangea's +hydrangeas +hydrant +hydrant's +hydrants +hydras +hydrate +hydrate's +hydrated +hydrates +hydrating +hydraulic +hydraulically +hydraulics +hydraulics's +hydrocarbon +hydrocarbon's +hydrocarbons +hydroelectric +hydroelectricity +hydroelectricity's +hydrofoil +hydrofoil's +hydrofoils +hydrogen +hydrogen's +hydrogenate +hydrogenated +hydrogenates +hydrogenating +hydrology +hydrology's +hydrolysis +hydrolysis's +hydrometer +hydrometer's +hydrometers +hydrophobia +hydrophobia's +hydroplane +hydroplane's +hydroplaned +hydroplanes +hydroplaning +hydroponic +hydroponics +hydroponics's +hydrosphere +hydrosphere's +hydrotherapy +hydrotherapy's +hyena +hyena's +hyenas +hygiene +hygiene's +hygienic +hygienically +hygienist +hygienist's +hygienists +hygrometer +hygrometer's +hygrometers +hying +hymen +hymen's +hymens +hymn +hymn's +hymnal +hymnal's +hymnals +hymned +hymning +hymns +hype +hype's +hyped +hyper +hyperactive +hyperactivity +hyperactivity's +hyperbola +hyperbola's +hyperbolae +hyperbolas +hyperbole +hyperbole's +hyperbolic +hypercritical +hypercritically +hyperlink +hyperlink's +hyperlinked +hyperlinking +hyperlinks +hypermarket +hypersensitive +hypersensitivities +hypersensitivity +hypersensitivity's +hyperspace +hypertension +hypertension's +hypertext +hypertext's +hyperventilate +hyperventilated +hyperventilates +hyperventilating +hyperventilation +hyperventilation's +hypes +hyphen +hyphen's +hyphenate +hyphenate's +hyphenated +hyphenates +hyphenating +hyphenation +hyphenation's +hyphenations +hyphened +hyphening +hyphens +hyping +hypnoses +hypnosis +hypnosis's +hypnotic +hypnotic's +hypnotically +hypnotics +hypnotism +hypnotism's +hypnotist +hypnotist's +hypnotists +hypnotize +hypnotized +hypnotizes +hypnotizing +hypo +hypo's +hypoallergenic +hypochondria +hypochondria's +hypochondriac +hypochondriac's +hypochondriacs +hypocrisies +hypocrisy +hypocrisy's +hypocrite +hypocrite's +hypocrites +hypocritical +hypocritically +hypodermic +hypodermic's +hypodermics +hypoglycemia +hypoglycemia's +hypoglycemic +hypoglycemic's +hypoglycemics +hypos +hypotenuse +hypotenuse's +hypotenuses +hypothalami +hypothalamus +hypothalamus's +hypothermia +hypothermia's +hypotheses +hypothesis +hypothesis's +hypothesize +hypothesized +hypothesizes +hypothesizing +hypothetical +hypothetically +hysterectomies +hysterectomy +hysterectomy's +hysteresis +hysteria +hysteria's +hysteric +hysteric's +hysterical +hysterically +hysterics +hysterics's +i +iOS +iOS's +iPad +iPad's +iPhone +iPhone's +iPod +iPod's +iTunes +iTunes's +iamb +iamb's +iambic +iambic's +iambics +iambs +ibex +ibex's +ibexes +ibices +ibis +ibis's +ibises +ibuprofen +ibuprofen's +ice +ice's +iceberg +iceberg's +icebergs +icebound +icebox +icebox's +iceboxes +icebreaker +icebreaker's +icebreakers +icecap +icecap's +icecaps +iced +ices +icicle +icicle's +icicles +icier +iciest +icily +iciness +iciness's +icing +icing's +icings +ickier +ickiest +icky +icon +icon's +iconoclast +iconoclast's +iconoclastic +iconoclasts +icons +icy +id +id's +idea +idea's +ideal +ideal's +idealism +idealism's +idealist +idealist's +idealistic +idealistically +idealists +idealization +idealization's +idealize +idealized +idealizes +idealizing +ideally +ideals +ideas +identical +identically +identifiable +identification +identification's +identified +identifier +identifiers +identifies +identify +identifying +identities +identity +identity's +ideogram +ideogram's +ideograms +ideograph +ideograph's +ideographs +ideological +ideologically +ideologies +ideologist +ideologist's +ideologists +ideology +ideology's +ides +ides's +idiocies +idiocy +idiocy's +idiom +idiom's +idiomatic +idiomatically +idioms +idiosyncrasies +idiosyncrasy +idiosyncrasy's +idiosyncratic +idiot +idiot's +idiotic +idiotically +idiots +idle +idle's +idled +idleness +idleness's +idler +idler's +idlers +idles +idlest +idling +idly +idol +idol's +idolater +idolater's +idolaters +idolatrous +idolatry +idolatry's +idolize +idolized +idolizes +idolizing +idols +ids +idyl +idyl's +idyll +idyll's +idyllic +idylls +idyls +if +if's +iffier +iffiest +iffy +ifs +igloo +igloo's +igloos +igneous +ignite +ignited +ignites +igniting +ignition +ignition's +ignitions +ignoble +ignobly +ignominies +ignominious +ignominiously +ignominy +ignominy's +ignoramus +ignoramus's +ignoramuses +ignorance +ignorance's +ignorant +ignorantly +ignore +ignored +ignores +ignoring +iguana +iguana's +iguanas +ikon +ikon's +ikons +ilk +ilk's +ilks +ill +ill's +illegal +illegal's +illegalities +illegality +illegality's +illegally +illegals +illegibility +illegibility's +illegible +illegibly +illegitimacy +illegitimacy's +illegitimate +illegitimately +illiberal +illicit +illicitly +illicitness +illicitness's +illiteracy +illiteracy's +illiterate +illiterate's +illiterates +illness +illness's +illnesses +illogical +illogically +ills +illuminate +illuminated +illuminates +illuminating +illumination +illumination's +illuminations +illumine +illumined +illumines +illumining +illusion +illusion's +illusions +illusive +illusory +illustrate +illustrated +illustrates +illustrating +illustration +illustration's +illustrations +illustrative +illustrator +illustrator's +illustrators +illustrious +image +image's +imaged +imagery +imagery's +images +imaginable +imaginably +imaginary +imagination +imagination's +imaginations +imaginative +imaginatively +imagine +imagined +imagines +imaging +imagining +imam +imam's +imams +imbalance +imbalance's +imbalanced +imbalances +imbecile +imbecile's +imbeciles +imbecilic +imbecilities +imbecility +imbecility's +imbed +imbedded +imbedding +imbeds +imbibe +imbibed +imbibes +imbibing +imbroglio +imbroglio's +imbroglios +imbue +imbued +imbues +imbuing +imitate +imitated +imitates +imitating +imitation +imitation's +imitations +imitative +imitator +imitator's +imitators +immaculate +immaculately +immaculateness +immaculateness's +immanence +immanence's +immanent +immaterial +immature +immaturely +immaturity +immaturity's +immeasurable +immeasurably +immediacy +immediacy's +immediate +immediately +immemorial +immense +immensely +immensities +immensity +immensity's +immerse +immersed +immerses +immersing +immersion +immersion's +immersions +immersive +immigrant +immigrant's +immigrants +immigrate +immigrated +immigrates +immigrating +immigration +immigration's +imminence +imminence's +imminent +imminently +immobile +immobility +immobility's +immobilization +immobilization's +immobilize +immobilized +immobilizes +immobilizing +immoderate +immoderately +immodest +immodestly +immodesty +immodesty's +immolate +immolated +immolates +immolating +immolation +immolation's +immoral +immoralities +immorality +immorality's +immorally +immortal +immortal's +immortality +immortality's +immortalize +immortalized +immortalizes +immortalizing +immortally +immortals +immovable +immovably +immoveable +immune +immunity +immunity's +immunization +immunization's +immunizations +immunize +immunized +immunizes +immunizing +immunology +immunology's +immure +immured +immures +immuring +immutability +immutability's +immutable +immutably +imp +imp's +impact +impact's +impacted +impacting +impacts +impair +impaired +impairing +impairment +impairment's +impairments +impairs +impala +impala's +impalas +impale +impaled +impalement +impalement's +impales +impaling +impalpable +impanel +impaneled +impaneling +impanels +impart +imparted +impartial +impartiality +impartiality's +impartially +imparting +imparts +impassable +impasse +impasse's +impasses +impassioned +impassive +impassively +impassivity +impassivity's +impatience +impatience's +impatiences +impatient +impatiently +impeach +impeached +impeaches +impeaching +impeachment +impeachment's +impeachments +impeccability +impeccability's +impeccable +impeccably +impecunious +impecuniousness +impecuniousness's +impedance +impedance's +impede +impeded +impedes +impediment +impediment's +impedimenta +impedimenta's +impediments +impeding +impel +impelled +impelling +impels +impend +impended +impending +impends +impenetrability +impenetrability's +impenetrable +impenetrably +impenitence +impenitence's +impenitent +imperative +imperative's +imperatively +imperatives +imperceptible +imperceptibly +imperfect +imperfect's +imperfection +imperfection's +imperfections +imperfectly +imperfects +imperial +imperial's +imperialism +imperialism's +imperialist +imperialist's +imperialistic +imperialists +imperially +imperials +imperil +imperiled +imperiling +imperilled +imperilling +imperils +imperious +imperiously +imperiousness +imperiousness's +imperishable +impermanence +impermanence's +impermanent +impermeable +impermissible +impersonal +impersonally +impersonate +impersonated +impersonates +impersonating +impersonation +impersonation's +impersonations +impersonator +impersonator's +impersonators +impertinence +impertinence's +impertinent +impertinently +imperturbability +imperturbability's +imperturbable +imperturbably +impervious +impetigo +impetigo's +impetuosity +impetuosity's +impetuous +impetuously +impetus +impetus's +impetuses +impieties +impiety +impiety's +impinge +impinged +impingement +impingement's +impinges +impinging +impious +impiously +impish +impishly +impishness +impishness's +implacability +implacability's +implacable +implacably +implant +implant's +implantation +implantation's +implanted +implanting +implants +implausibilities +implausibility +implausibility's +implausible +implausibly +implement +implement's +implementable +implementation +implementation's +implementations +implemented +implementer +implementing +implements +implicate +implicated +implicates +implicating +implication +implication's +implications +implicit +implicitly +implied +implies +implode +imploded +implodes +imploding +implore +implored +implores +imploring +implosion +implosion's +implosions +imply +implying +impolite +impolitely +impoliteness +impoliteness's +impolitenesses +impolitic +imponderable +imponderable's +imponderables +import +import's +importance +importance's +important +importantly +importation +importation's +importations +imported +importer +importer's +importers +importing +imports +importunate +importune +importuned +importunes +importuning +importunity +importunity's +impose +imposed +imposes +imposing +imposingly +imposition +imposition's +impositions +impossibilities +impossibility +impossibility's +impossible +impossibles +impossibly +imposter +imposter's +imposters +impostor +impostor's +impostors +imposture +imposture's +impostures +impotence +impotence's +impotent +impotently +impound +impounded +impounding +impounds +impoverish +impoverished +impoverishes +impoverishing +impoverishment +impoverishment's +impracticable +impracticably +impractical +impracticality +impracticality's +imprecation +imprecation's +imprecations +imprecise +imprecisely +imprecision +imprecision's +impregnability +impregnability's +impregnable +impregnably +impregnate +impregnated +impregnates +impregnating +impregnation +impregnation's +impresario +impresario's +impresarios +impress +impress's +impressed +impresses +impressing +impression +impression's +impressionable +impressionism +impressionism's +impressionist +impressionist's +impressionistic +impressionists +impressions +impressive +impressively +impressiveness +impressiveness's +imprimatur +imprimatur's +imprimaturs +imprint +imprint's +imprinted +imprinting +imprints +imprison +imprisoned +imprisoning +imprisonment +imprisonment's +imprisonments +imprisons +improbabilities +improbability +improbability's +improbable +improbably +impromptu +impromptu's +impromptus +improper +improperly +improprieties +impropriety +impropriety's +improvable +improve +improved +improvement +improvement's +improvements +improves +improvidence +improvidence's +improvident +improvidently +improving +improvisation +improvisation's +improvisations +improvise +improvised +improvises +improvising +imprudence +imprudence's +imprudent +imps +impudence +impudence's +impudent +impudently +impugn +impugned +impugning +impugns +impulse +impulse's +impulsed +impulses +impulsing +impulsion +impulsion's +impulsive +impulsively +impulsiveness +impulsiveness's +impunity +impunity's +impure +impurely +impurer +impurest +impurities +impurity +impurity's +imputation +imputation's +imputations +impute +imputed +imputes +imputing +in +in's +inabilities +inability +inability's +inaccessibility +inaccessibility's +inaccessible +inaccuracies +inaccuracy +inaccuracy's +inaccurate +inaccurately +inaction +inaction's +inactive +inactivity +inactivity's +inadequacies +inadequacy +inadequacy's +inadequate +inadequately +inadmissible +inadvertence +inadvertence's +inadvertent +inadvertently +inadvisable +inalienable +inamorata +inamorata's +inamoratas +inane +inanely +inaner +inanest +inanimate +inanities +inanity +inanity's +inapplicable +inappropriate +inappropriately +inapt +inarticulate +inarticulately +inasmuch +inattention +inattention's +inattentive +inaudible +inaudibly +inaugural +inaugural's +inaugurals +inaugurate +inaugurated +inaugurates +inaugurating +inauguration +inauguration's +inaugurations +inauspicious +inboard +inboard's +inboards +inborn +inbound +inbox +inbox's +inboxes +inbred +inbreed +inbreeding +inbreeding's +inbreeds +inbuilt +incalculable +incalculably +incandescence +incandescence's +incandescent +incantation +incantation's +incantations +incapability +incapability's +incapable +incapacitate +incapacitated +incapacitates +incapacitating +incapacity +incapacity's +incarcerate +incarcerated +incarcerates +incarcerating +incarceration +incarceration's +incarcerations +incarnate +incarnated +incarnates +incarnating +incarnation +incarnation's +incarnations +incautious +incendiaries +incendiary +incendiary's +incense +incense's +incensed +incenses +incensing +incentive +incentive's +incentives +inception +inception's +inceptions +incessant +incessantly +incest +incest's +incestuous +inch +inch's +inched +inches +inching +inchoate +incidence +incidence's +incidences +incident +incident's +incidental +incidental's +incidentally +incidentals +incidents +incinerate +incinerated +incinerates +incinerating +incineration +incineration's +incinerator +incinerator's +incinerators +incipient +incise +incised +incises +incising +incision +incision's +incisions +incisive +incisively +incisiveness +incisiveness's +incisor +incisor's +incisors +incite +incited +incitement +incitement's +incitements +incites +inciting +incivilities +incivility +incivility's +inclemency +inclemency's +inclement +inclination +inclination's +inclinations +incline +incline's +inclined +inclines +inclining +inclose +inclosed +incloses +inclosing +inclosure +inclosure's +inclosures +include +included +includes +including +inclusion +inclusion's +inclusions +inclusive +inclusively +incognito +incognito's +incognitos +incoherence +incoherence's +incoherent +incoherently +incombustible +income +income's +incomes +incoming +incommensurate +incommunicado +incomparable +incomparably +incompatibilities +incompatibility +incompatibility's +incompatible +incompatible's +incompatibles +incompatibly +incompetence +incompetence's +incompetent +incompetent's +incompetently +incompetents +incomplete +incompletely +incompleteness +incomprehensible +incomprehensibly +inconceivable +inconceivably +inconclusive +inconclusively +incongruities +incongruity +incongruity's +incongruous +incongruously +inconsequential +inconsequentially +inconsiderable +inconsiderate +inconsiderately +inconsiderateness +inconsiderateness's +inconsistencies +inconsistency +inconsistency's +inconsistent +inconsistently +inconsolable +inconspicuous +inconspicuously +inconspicuousness +inconspicuousness's +inconstancy +inconstancy's +inconstant +incontestable +incontestably +incontinence +incontinence's +incontinent +incontrovertible +incontrovertibly +inconvenience +inconvenience's +inconvenienced +inconveniences +inconveniencing +inconvenient +inconveniently +incorporate +incorporated +incorporates +incorporating +incorporation +incorporation's +incorporeal +incorrect +incorrectly +incorrectness +incorrectness's +incorrigibility +incorrigibility's +incorrigible +incorrigibly +incorruptibility +incorruptibility's +incorruptible +increase +increase's +increased +increases +increasing +increasingly +incredibility +incredibility's +incredible +incredibly +incredulity +incredulity's +incredulous +incredulously +increment +increment's +incremental +incremented +increments +incriminate +incriminated +incriminates +incriminating +incrimination +incrimination's +incriminatory +incrust +incrustation +incrustation's +incrustations +incrusted +incrusting +incrusts +incubate +incubated +incubates +incubating +incubation +incubation's +incubator +incubator's +incubators +incubi +incubus +incubus's +incubuses +inculcate +inculcated +inculcates +inculcating +inculcation +inculcation's +inculpate +inculpated +inculpates +inculpating +incumbencies +incumbency +incumbency's +incumbent +incumbent's +incumbents +incur +incurable +incurable's +incurables +incurably +incurious +incurred +incurring +incurs +incursion +incursion's +incursions +indebted +indebtedness +indebtedness's +indecencies +indecency +indecency's +indecent +indecently +indecipherable +indecision +indecision's +indecisive +indecisively +indecisiveness +indecisiveness's +indecorous +indeed +indefatigable +indefatigably +indefensible +indefensibly +indefinable +indefinably +indefinite +indefinitely +indelible +indelibly +indelicacies +indelicacy +indelicacy's +indelicate +indelicately +indemnification +indemnification's +indemnifications +indemnified +indemnifies +indemnify +indemnifying +indemnities +indemnity +indemnity's +indent +indent's +indentation +indentation's +indentations +indented +indenting +indents +indenture +indenture's +indentured +indentures +indenturing +independence +independence's +independent +independent's +independently +independents +indescribable +indescribably +indestructible +indestructibly +indeterminable +indeterminacy +indeterminacy's +indeterminate +indeterminately +index +index's +indexed +indexes +indexing +indicate +indicated +indicates +indicating +indication +indication's +indications +indicative +indicative's +indicatives +indicator +indicator's +indicators +indices +indict +indictable +indicted +indicting +indictment +indictment's +indictments +indicts +indifference +indifference's +indifferent +indifferently +indigence +indigence's +indigenous +indigent +indigent's +indigents +indigestible +indigestion +indigestion's +indignant +indignantly +indignation +indignation's +indignities +indignity +indignity's +indigo +indigo's +indirect +indirection +indirectly +indirectness +indirectness's +indiscernible +indiscreet +indiscreetly +indiscretion +indiscretion's +indiscretions +indiscriminate +indiscriminately +indispensable +indispensable's +indispensables +indispensably +indisposed +indisposition +indisposition's +indispositions +indisputable +indisputably +indissoluble +indistinct +indistinctly +indistinctness +indistinctness's +indistinguishable +individual +individual's +individualism +individualism's +individualist +individualist's +individualistic +individualists +individuality +individuality's +individualize +individualized +individualizes +individualizing +individually +individuals +indivisibility +indivisibility's +indivisible +indivisibly +indoctrinate +indoctrinated +indoctrinates +indoctrinating +indoctrination +indoctrination's +indolence +indolence's +indolent +indolently +indomitable +indomitably +indoor +indoors +indorse +indorsed +indorsement +indorsement's +indorsements +indorses +indorsing +indubitable +indubitably +induce +induced +inducement +inducement's +inducements +induces +inducing +induct +inductance +inductance's +inducted +inductee +inductee's +inductees +inducting +induction +induction's +inductions +inductive +inducts +indue +indued +indues +induing +indulge +indulged +indulgence +indulgence's +indulgences +indulgent +indulgently +indulges +indulging +industrial +industrialism +industrialism's +industrialist +industrialist's +industrialists +industrialization +industrialization's +industrialize +industrialized +industrializes +industrializing +industrially +industries +industrious +industriously +industriousness +industriousness's +industry +industry's +inebriate +inebriate's +inebriated +inebriates +inebriating +inebriation +inebriation's +inedible +ineducable +ineffable +ineffably +ineffective +ineffectively +ineffectiveness +ineffectiveness's +ineffectual +ineffectually +inefficiencies +inefficiency +inefficiency's +inefficient +inefficiently +inelastic +inelegance +inelegant +inelegantly +ineligibility +ineligibility's +ineligible +ineligible's +ineligibles +ineluctable +ineluctably +inept +ineptitude +ineptitude's +ineptly +ineptness +ineptness's +inequalities +inequality +inequality's +inequitable +inequities +inequity +inequity's +inert +inertia +inertia's +inertial +inertly +inertness +inertness's +inescapable +inescapably +inessential +inessential's +inessentials +inestimable +inestimably +inevitability +inevitability's +inevitable +inevitable's +inevitably +inexact +inexcusable +inexcusably +inexhaustible +inexhaustibly +inexorable +inexorably +inexpedient +inexpensive +inexpensively +inexperience +inexperience's +inexperienced +inexpert +inexplicable +inexplicably +inexpressible +inextinguishable +inextricable +inextricably +infallibility +infallibility's +infallible +infallibly +infamies +infamous +infamously +infamy +infamy's +infancy +infancy's +infant +infant's +infanticide +infanticide's +infanticides +infantile +infantries +infantry +infantry's +infantryman +infantryman's +infantrymen +infants +infarction +infarction's +infatuate +infatuated +infatuates +infatuating +infatuation +infatuation's +infatuations +infeasible +infect +infected +infecting +infection +infection's +infections +infectious +infectiously +infectiousness +infectiousness's +infects +infelicities +infelicitous +infelicity +infelicity's +infer +inference +inference's +inferences +inferential +inferior +inferior's +inferiority +inferiority's +inferiors +infernal +inferno +inferno's +infernos +inferred +inferring +infers +infertile +infertility +infertility's +infest +infestation +infestation's +infestations +infested +infesting +infests +infidel +infidel's +infidelities +infidelity +infidelity's +infidels +infield +infield's +infielder +infielder's +infielders +infields +infighting +infighting's +infiltrate +infiltrated +infiltrates +infiltrating +infiltration +infiltration's +infiltrator +infiltrator's +infiltrators +infinite +infinite's +infinitely +infinitesimal +infinitesimal's +infinitesimally +infinitesimals +infinities +infinitive +infinitive's +infinitives +infinitude +infinitude's +infinity +infinity's +infirm +infirmaries +infirmary +infirmary's +infirmities +infirmity +infirmity's +infix +inflame +inflamed +inflames +inflaming +inflammable +inflammation +inflammation's +inflammations +inflammatory +inflatable +inflatable's +inflatables +inflate +inflated +inflates +inflating +inflation +inflation's +inflationary +inflect +inflected +inflecting +inflection +inflection's +inflectional +inflections +inflects +inflexibility +inflexibility's +inflexible +inflexibly +inflict +inflicted +inflicting +infliction +infliction's +inflicts +inflorescence +inflorescence's +inflow +influence +influence's +influenced +influences +influencing +influential +influentially +influenza +influenza's +influx +influx's +influxes +info +info's +infomercial +infomercial's +infomercials +inform +informal +informality +informality's +informally +informant +informant's +informants +information +information's +informational +informative +informed +informer +informer's +informers +informing +informs +infotainment +infotainment's +infraction +infraction's +infractions +infrared +infrared's +infrastructure +infrastructure's +infrastructures +infrequency +infrequency's +infrequent +infrequently +infringe +infringed +infringement +infringement's +infringements +infringes +infringing +infuriate +infuriated +infuriates +infuriating +infuriatingly +infuse +infused +infuses +infusing +infusion +infusion's +infusions +ingenious +ingeniously +ingenuity +ingenuity's +ingenuous +ingenuously +ingenuousness +ingenuousness's +ingest +ingested +ingesting +ingestion +ingestion's +ingests +inglorious +ingot +ingot's +ingots +ingrain +ingrained +ingraining +ingrains +ingrate +ingrate's +ingrates +ingratiate +ingratiated +ingratiates +ingratiating +ingratiatingly +ingratitude +ingratitude's +ingredient +ingredient's +ingredients +ingress +ingress's +ingresses +ingrown +ingénue +ingénue's +ingénues +inhabit +inhabitable +inhabitant +inhabitant's +inhabitants +inhabited +inhabiting +inhabits +inhalant +inhalant's +inhalants +inhalation +inhalation's +inhalations +inhalator +inhalator's +inhalators +inhale +inhaled +inhaler +inhaler's +inhalers +inhales +inhaling +inhere +inhered +inherent +inherently +inheres +inhering +inherit +inheritance +inheritance's +inheritances +inherited +inheriting +inheritor +inheritor's +inheritors +inherits +inhibit +inhibited +inhibiting +inhibition +inhibition's +inhibitions +inhibits +inhospitable +inhuman +inhumane +inhumanely +inhumanities +inhumanity +inhumanity's +inhumanly +inimical +inimically +inimitable +inimitably +iniquities +iniquitous +iniquity +iniquity's +initial +initial's +initialed +initialing +initialization +initialize +initialized +initializes +initializing +initialled +initialling +initially +initials +initiate +initiate's +initiated +initiates +initiating +initiation +initiation's +initiations +initiative +initiative's +initiatives +initiator +initiator's +initiators +inject +injected +injecting +injection +injection's +injections +injector +injector's +injectors +injects +injudicious +injunction +injunction's +injunctions +injure +injured +injures +injuries +injuring +injurious +injury +injury's +injustice +injustice's +injustices +ink +ink's +inkblot +inkblot's +inkblots +inked +inkier +inkiest +inkiness +inkiness's +inking +inkling +inkling's +inklings +inks +inkwell +inkwell's +inkwells +inky +inlaid +inland +inland's +inlay +inlay's +inlaying +inlays +inlet +inlet's +inlets +inline +inmate +inmate's +inmates +inmost +inn +inn's +innards +innards's +innate +innately +inner +innermost +inning +inning's +innings +innkeeper +innkeeper's +innkeepers +innocence +innocence's +innocent +innocent's +innocently +innocents +innocuous +innocuously +innovate +innovated +innovates +innovating +innovation +innovation's +innovations +innovative +innovator +innovator's +innovators +inns +innuendo +innuendo's +innuendoes +innuendos +innumerable +inoculate +inoculated +inoculates +inoculating +inoculation +inoculation's +inoculations +inoffensive +inoffensively +inoperable +inoperative +inopportune +inordinate +inordinately +inorganic +inpatient +inpatient's +inpatients +input +input's +inputs +inputted +inputting +inquest +inquest's +inquests +inquietude +inquietude's +inquire +inquired +inquirer +inquirer's +inquirers +inquires +inquiries +inquiring +inquiringly +inquiry +inquiry's +inquisition +inquisition's +inquisitions +inquisitive +inquisitively +inquisitiveness +inquisitiveness's +inquisitor +inquisitor's +inquisitors +inroad +inroad's +inroads +ins +insane +insanely +insaner +insanest +insanity +insanity's +insatiable +insatiably +inscribe +inscribed +inscribes +inscribing +inscription +inscription's +inscriptions +inscrutable +inscrutably +inseam +inseam's +inseams +insect +insect's +insecticide +insecticide's +insecticides +insectivore +insectivore's +insectivores +insectivorous +insects +insecure +insecurely +insecurities +insecurity +insecurity's +inseminate +inseminated +inseminates +inseminating +insemination +insemination's +insensate +insensibility +insensibility's +insensible +insensibly +insensitive +insensitively +insensitivity +insensitivity's +insentience +insentience's +insentient +inseparability +inseparability's +inseparable +inseparable's +inseparables +inseparably +insert +insert's +inserted +inserting +insertion +insertion's +insertions +inserts +inset +inset's +insets +insetted +insetting +inshore +inside +inside's +insider +insider's +insiders +insides +insidious +insidiously +insidiousness +insidiousness's +insight +insight's +insightful +insights +insigne +insigne's +insignes +insignia +insignia's +insignias +insignificance +insignificance's +insignificant +insignificantly +insincere +insincerely +insincerity +insincerity's +insinuate +insinuated +insinuates +insinuating +insinuation +insinuation's +insinuations +insipid +insist +insisted +insistence +insistence's +insistent +insistently +insisting +insists +insofar +insole +insole's +insolence +insolence's +insolent +insolently +insoles +insolubility +insolubility's +insoluble +insolvable +insolvency +insolvency's +insolvent +insolvent's +insolvents +insomnia +insomnia's +insomniac +insomniac's +insomniacs +insouciance +insouciance's +insouciant +inspect +inspected +inspecting +inspection +inspection's +inspections +inspector +inspector's +inspectors +inspects +inspiration +inspiration's +inspirational +inspirations +inspire +inspired +inspires +inspiring +instability +instability's +instal +install +installation +installation's +installations +installed +installing +installment +installment's +installments +installs +instalment +instalment's +instalments +instals +instance +instance's +instanced +instances +instancing +instant +instant's +instantaneous +instantaneously +instantly +instants +instead +instep +instep's +insteps +instigate +instigated +instigates +instigating +instigation +instigation's +instigator +instigator's +instigators +instil +instill +instilled +instilling +instills +instils +instinct +instinct's +instinctive +instinctively +instincts +institute +institute's +instituted +institutes +instituting +institution +institution's +institutional +institutionalize +institutionalized +institutionalizes +institutionalizing +institutions +instruct +instructed +instructing +instruction +instruction's +instructional +instructions +instructive +instructively +instructor +instructor's +instructors +instructs +instrument +instrument's +instrumental +instrumental's +instrumentalist +instrumentalist's +instrumentalists +instrumentality +instrumentality's +instrumentals +instrumentation +instrumentation's +instrumented +instrumenting +instruments +insubordinate +insubordination +insubordination's +insubstantial +insufferable +insufferably +insufficiency +insufficiency's +insufficient +insufficiently +insular +insularity +insularity's +insulate +insulated +insulates +insulating +insulation +insulation's +insulator +insulator's +insulators +insulin +insulin's +insult +insult's +insulted +insulting +insults +insuperable +insupportable +insurance +insurance's +insurances +insure +insured +insured's +insureds +insurer +insurer's +insurers +insures +insurgence +insurgence's +insurgences +insurgencies +insurgency +insurgency's +insurgent +insurgent's +insurgents +insuring +insurmountable +insurrection +insurrection's +insurrectionist +insurrectionist's +insurrectionists +insurrections +intact +intagli +intaglio +intaglio's +intaglios +intake +intake's +intakes +intangible +intangible's +intangibles +intangibly +integer +integer's +integers +integral +integral's +integrals +integrate +integrated +integrates +integrating +integration +integration's +integrator +integrity +integrity's +integument +integument's +integuments +intellect +intellect's +intellects +intellectual +intellectual's +intellectualism +intellectualize +intellectualized +intellectualizes +intellectualizing +intellectually +intellectuals +intelligence +intelligence's +intelligent +intelligently +intelligentsia +intelligentsia's +intelligibility +intelligibility's +intelligible +intelligibly +intemperance +intemperance's +intemperate +intend +intended +intended's +intendeds +intending +intends +intense +intensely +intenser +intensest +intensification +intensification's +intensified +intensifier +intensifier's +intensifiers +intensifies +intensify +intensifying +intensities +intensity +intensity's +intensive +intensive's +intensively +intensives +intent +intent's +intention +intention's +intentional +intentionally +intentions +intently +intentness +intentness's +intents +inter +interact +interacted +interacting +interaction +interaction's +interactions +interactive +interactively +interacts +interbred +interbreed +interbreeding +interbreeds +intercede +interceded +intercedes +interceding +intercept +intercept's +intercepted +intercepting +interception +interception's +interceptions +interceptor +interceptor's +interceptors +intercepts +intercession +intercession's +intercessions +intercessor +intercessor's +intercessors +interchange +interchange's +interchangeable +interchangeably +interchanged +interchanges +interchanging +intercollegiate +intercom +intercom's +intercoms +interconnect +interconnected +interconnecting +interconnection +interconnection's +interconnections +interconnects +intercontinental +intercourse +intercourse's +interdenominational +interdepartmental +interdependence +interdependence's +interdependent +interdict +interdict's +interdicted +interdicting +interdiction +interdiction's +interdicts +interdisciplinary +interest +interest's +interested +interesting +interestingly +interests +interface +interface's +interfaced +interfaces +interfacing +interfaith +interfere +interfered +interference +interference's +interferes +interfering +interferon +interferon's +intergalactic +interim +interim's +interior +interior's +interiors +interject +interjected +interjecting +interjection +interjection's +interjections +interjects +interlace +interlaced +interlaces +interlacing +interlard +interlarded +interlarding +interlards +interleave +interleaved +interleaves +interleaving +interleukin +interleukin's +interlink +interlinked +interlinking +interlinks +interlock +interlock's +interlocked +interlocking +interlocks +interlocutory +interloper +interloper's +interlopers +interlude +interlude's +interluded +interludes +interluding +intermarriage +intermarriage's +intermarriages +intermarried +intermarries +intermarry +intermarrying +intermediaries +intermediary +intermediary's +intermediate +intermediate's +intermediates +interment +interment's +interments +intermezzi +intermezzo +intermezzo's +intermezzos +interminable +interminably +intermingle +intermingled +intermingles +intermingling +intermission +intermission's +intermissions +intermittent +intermittently +intern +intern's +internal +internalize +internalized +internalizes +internalizing +internally +internals +international +international's +internationalism +internationalism's +internationalize +internationalized +internationalizes +internationalizing +internationally +internationals +interne +interne's +internecine +interned +internee +internee's +internees +internement +internes +interneship +interneships +internet +interning +internist +internist's +internists +internment +internment's +interns +internship +internship's +internships +interoffice +interpersonal +interplanetary +interplay +interplay's +interpolate +interpolated +interpolates +interpolating +interpolation +interpolation's +interpolations +interpose +interposed +interposes +interposing +interposition +interposition's +interpret +interpretation +interpretation's +interpretations +interpretative +interpreted +interpreter +interpreter's +interpreters +interpreting +interpretive +interprets +interracial +interred +interrelate +interrelated +interrelates +interrelating +interrelation +interrelation's +interrelations +interrelationship +interrelationship's +interrelationships +interring +interrogate +interrogated +interrogates +interrogating +interrogation +interrogation's +interrogations +interrogative +interrogative's +interrogatives +interrogator +interrogator's +interrogatories +interrogators +interrogatory +interrogatory's +interrupt +interrupt's +interrupted +interrupting +interruption +interruption's +interruptions +interrupts +inters +interscholastic +intersect +intersected +intersecting +intersection +intersection's +intersections +intersects +intersperse +interspersed +intersperses +interspersing +interstate +interstate's +interstates +interstellar +interstice +interstice's +interstices +intertwine +intertwined +intertwines +intertwining +interurban +interval +interval's +intervals +intervene +intervened +intervenes +intervening +intervention +intervention's +interventions +interview +interview's +interviewed +interviewee +interviewee's +interviewees +interviewer +interviewer's +interviewers +interviewing +interviews +interweave +interweaved +interweaves +interweaving +interwove +interwoven +intestate +intestinal +intestine +intestine's +intestines +intimacies +intimacy +intimacy's +intimate +intimate's +intimated +intimately +intimates +intimating +intimation +intimation's +intimations +intimidate +intimidated +intimidates +intimidating +intimidation +intimidation's +into +intolerable +intolerably +intolerance +intolerance's +intolerant +intonation +intonation's +intonations +intone +intoned +intones +intoning +intoxicant +intoxicant's +intoxicants +intoxicate +intoxicated +intoxicates +intoxicating +intoxication +intoxication's +intractability +intractability's +intractable +intramural +intranet +intranet's +intranets +intransigence +intransigence's +intransigent +intransigent's +intransigents +intransitive +intransitive's +intransitively +intransitives +intravenous +intravenous's +intravenouses +intravenously +intrench +intrenched +intrenches +intrenching +intrenchment +intrenchment's +intrenchments +intrepid +intrepidly +intricacies +intricacy +intricacy's +intricate +intricately +intrigue +intrigue's +intrigued +intrigues +intriguing +intriguingly +intrinsic +intrinsically +introduce +introduced +introduces +introducing +introduction +introduction's +introductions +introductory +intros +introspection +introspection's +introspective +introversion +introversion's +introvert +introvert's +introverted +introverts +intrude +intruded +intruder +intruder's +intruders +intrudes +intruding +intrusion +intrusion's +intrusions +intrusive +intrust +intrusted +intrusting +intrusts +intuit +intuited +intuiting +intuition +intuition's +intuitions +intuitive +intuitively +intuits +inundate +inundated +inundates +inundating +inundation +inundation's +inundations +inure +inured +inures +inuring +invade +invaded +invader +invader's +invaders +invades +invading +invalid +invalid's +invalidate +invalidated +invalidates +invalidating +invalidation +invalidation's +invalided +invaliding +invalidity +invalidity's +invalids +invaluable +invariable +invariable's +invariables +invariably +invariant +invasion +invasion's +invasions +invasive +invective +invective's +inveigh +inveighed +inveighing +inveighs +inveigle +inveigled +inveigles +inveigling +invent +invented +inventing +invention +invention's +inventions +inventive +inventiveness +inventiveness's +inventor +inventor's +inventoried +inventories +inventors +inventory +inventory's +inventorying +invents +inverse +inverse's +inversely +inverses +inversion +inversion's +inversions +invert +invert's +invertebrate +invertebrate's +invertebrates +inverted +inverting +inverts +invest +invested +investigate +investigated +investigates +investigating +investigation +investigation's +investigations +investigative +investigator +investigator's +investigators +investing +investiture +investiture's +investitures +investment +investment's +investments +investor +investor's +investors +invests +inveterate +invidious +invidiously +invigorate +invigorated +invigorates +invigorating +invigoration +invigoration's +invincibility +invincibility's +invincible +invincibly +inviolability +inviolability's +inviolable +inviolate +invisibility +invisibility's +invisible +invisibly +invitation +invitation's +invitational +invitational's +invitationals +invitations +invite +invite's +invited +invites +inviting +invitingly +invocation +invocation's +invocations +invoice +invoice's +invoiced +invoices +invoicing +invoke +invoked +invokes +invoking +involuntarily +involuntary +involve +involved +involvement +involvement's +involvements +involves +involving +invulnerability +invulnerability's +invulnerable +invulnerably +inward +inwardly +inwards +iodine +iodine's +iodize +iodized +iodizes +iodizing +ion +ion's +ionization +ionization's +ionize +ionized +ionizer +ionizer's +ionizers +ionizes +ionizing +ionosphere +ionosphere's +ionospheres +ions +iota +iota's +iotas +ipecac +ipecac's +ipecacs +irascibility +irascibility's +irascible +irate +irately +irateness +irateness's +ire +ire's +iridescence +iridescence's +iridescent +iridium +iridium's +iris +iris's +irises +irk +irked +irking +irks +irksome +iron +iron's +ironclad +ironclad's +ironclads +ironed +ironic +ironical +ironically +ironies +ironing +ironing's +irons +ironware +ironware's +ironwork +ironwork's +irony +irony's +irradiate +irradiated +irradiates +irradiating +irradiation +irradiation's +irrational +irrational's +irrationality +irrationality's +irrationally +irrationals +irreconcilable +irrecoverable +irredeemable +irrefutable +irregardless +irregular +irregular's +irregularities +irregularity +irregularity's +irregularly +irregulars +irrelevance +irrelevance's +irrelevances +irrelevancies +irrelevancy +irrelevancy's +irrelevant +irrelevantly +irreligious +irremediable +irremediably +irreparable +irreparably +irreplaceable +irrepressible +irreproachable +irresistible +irresistibly +irresolute +irresolutely +irresolution +irresolution's +irrespective +irresponsibility +irresponsibility's +irresponsible +irresponsibly +irretrievable +irretrievably +irreverence +irreverence's +irreverent +irreverently +irreversible +irreversibly +irrevocable +irrevocably +irrigate +irrigated +irrigates +irrigating +irrigation +irrigation's +irritability +irritability's +irritable +irritably +irritant +irritant's +irritants +irritate +irritated +irritates +irritating +irritatingly +irritation +irritation's +irritations +irruption +irruption's +irruptions +is +isinglass +isinglass's +island +island's +islander +islander's +islanders +islands +isle +isle's +isles +islet +islet's +islets +ism +ism's +isms +isn't +isobar +isobar's +isobars +isolate +isolate's +isolated +isolates +isolating +isolation +isolation's +isolationism +isolationism's +isolationist +isolationist's +isolationists +isometric +isometrics +isometrics's +isomorphic +isosceles +isotope +isotope's +isotopes +isotopic +isotropic +issuance +issuance's +issue +issue's +issued +issues +issuing +isthmi +isthmus +isthmus's +isthmuses +it +it'd +it'll +it's +italic +italic's +italicize +italicized +italicizes +italicizing +italics +italics's +itch +itch's +itched +itches +itchier +itchiest +itchiness +itchiness's +itching +itchy +item +item's +itemization +itemization's +itemize +itemized +itemizes +itemizing +items +iterate +iterated +iterates +iterating +iteration +iteration's +iterations +iterative +iterator +iterators +itinerant +itinerant's +itinerants +itineraries +itinerary +itinerary's +its +itself +ivies +ivories +ivory +ivory's +ivy +ivy's +j +jab +jab's +jabbed +jabber +jabber's +jabbered +jabberer +jabberer's +jabberers +jabbering +jabbers +jabbing +jabot +jabot's +jabots +jabs +jack +jack's +jackal +jackal's +jackals +jackass +jackass's +jackasses +jackboot +jackboot's +jackboots +jackdaw +jackdaw's +jackdaws +jacked +jacket +jacket's +jackets +jackhammer +jackhammer's +jackhammers +jacking +jackknife +jackknife's +jackknifed +jackknifes +jackknifing +jackknives +jackpot +jackpot's +jackpots +jackrabbit +jackrabbit's +jackrabbits +jacks +jade +jade's +jaded +jades +jading +jag +jag's +jagged +jaggeder +jaggedest +jaggedly +jaggedness +jaggedness's +jags +jaguar +jaguar's +jaguars +jail +jail's +jailbreak +jailbreak's +jailbreaks +jailed +jailer +jailer's +jailers +jailing +jailor +jailor's +jailors +jails +jalapeño +jalapeño's +jalapeños +jalopies +jalopy +jalopy's +jalousie +jalousie's +jalousies +jam +jam's +jamb +jamb's +jamboree +jamboree's +jamborees +jambs +jammed +jamming +jams +jangle +jangle's +jangled +jangles +jangling +janitor +janitor's +janitorial +janitors +japan +japan's +japanned +japanning +japans +jape +jape's +japed +japes +japing +jar +jar's +jardinière +jardinière's +jardinières +jargon +jargon's +jarred +jarring +jars +jasmine +jasmine's +jasmines +jasper +jasper's +jaundice +jaundice's +jaundiced +jaundices +jaundicing +jaunt +jaunt's +jaunted +jauntier +jauntiest +jauntily +jauntiness +jauntiness's +jaunting +jaunts +jaunty +javelin +javelin's +javelins +jaw +jaw's +jawbone +jawbone's +jawboned +jawbones +jawboning +jawbreaker +jawbreaker's +jawbreakers +jawed +jawing +jaws +jay +jay's +jays +jaywalk +jaywalked +jaywalker +jaywalker's +jaywalkers +jaywalking +jaywalks +jazz +jazz's +jazzed +jazzes +jazzier +jazziest +jazzing +jazzy +jealous +jealousies +jealously +jealousy +jealousy's +jeans +jeans's +jeep +jeep's +jeeps +jeer +jeer's +jeered +jeering +jeeringly +jeers +jeez +jehad +jehad's +jehads +jejune +jell +jelled +jellied +jellies +jelling +jello +jello's +jells +jelly +jelly's +jellybean +jellybean's +jellybeans +jellyfish +jellyfish's +jellyfishes +jellying +jeopardize +jeopardized +jeopardizes +jeopardizing +jeopardy +jeopardy's +jeremiad +jeremiad's +jeremiads +jerk +jerk's +jerked +jerkier +jerkiest +jerkily +jerkin +jerkin's +jerking +jerkins +jerks +jerkwater +jerky +jerky's +jersey +jersey's +jerseys +jessamine +jessamine's +jessamines +jest +jest's +jested +jester +jester's +jesters +jesting +jests +jet +jet's +jets +jetsam +jetsam's +jetted +jetties +jetting +jettison +jettison's +jettisoned +jettisoning +jettisons +jetty +jetty's +jewel +jewel's +jeweled +jeweler +jeweler's +jewelers +jeweling +jewelled +jeweller +jeweller's +jewellers +jewelling +jewelries +jewelry +jewelry's +jewels +jib +jib's +jibbed +jibbing +jibe +jibe's +jibed +jibes +jibing +jibs +jiffies +jiffy +jiffy's +jig +jig's +jigged +jigger +jigger's +jiggered +jiggering +jiggers +jigging +jiggle +jiggle's +jiggled +jiggles +jiggling +jigs +jigsaw +jigsaw's +jigsawed +jigsawing +jigsawn +jigsaws +jihad +jihad's +jihadist +jihadist's +jihadists +jihads +jilt +jilt's +jilted +jilting +jilts +jimmied +jimmies +jimmy +jimmy's +jimmying +jingle +jingle's +jingled +jingles +jingling +jingoism +jingoism's +jingoist +jingoist's +jingoistic +jingoists +jinn +jinn's +jinni +jinni's +jinnis +jinns +jinricksha +jinricksha's +jinrickshas +jinrikisha +jinrikisha's +jinrikishas +jinx +jinx's +jinxed +jinxes +jinxing +jitney +jitney's +jitneys +jitterbug +jitterbug's +jitterbugged +jitterbugging +jitterbugs +jitterier +jitteriest +jitters +jitters's +jittery +jiujitsu +jiujitsu's +jive +jive's +jived +jives +jiving +job +job's +jobbed +jobber +jobber's +jobbers +jobbing +jobless +joblessness +joblessness's +jobs +jock +jock's +jockey +jockey's +jockeyed +jockeying +jockeys +jocks +jockstrap +jockstrap's +jockstraps +jocose +jocosely +jocosity +jocosity's +jocular +jocularity +jocularity's +jocularly +jocund +jocundity +jocundity's +jocundly +jodhpurs +jodhpurs's +jog +jog's +jogged +jogger +jogger's +joggers +jogging +jogging's +joggle +joggle's +joggled +joggles +joggling +jogs +john +john's +johns +join +join's +joined +joiner +joiner's +joiners +joining +joins +joint +joint's +jointed +jointing +jointly +joints +joist +joist's +joists +joke +joke's +joked +joker +joker's +jokers +jokes +joking +jokingly +jollied +jollier +jollies +jolliest +jolliness +jolliness's +jollity +jollity's +jolly +jolly's +jollying +jolt +jolt's +jolted +jolting +jolts +jonquil +jonquil's +jonquils +josh +josh's +joshed +joshes +joshing +jostle +jostle's +jostled +jostles +jostling +jot +jot's +jots +jotted +jotting +jotting's +jottings +joule +joule's +joules +jounce +jounce's +jounced +jounces +jouncing +journal +journal's +journalese +journalese's +journalism +journalism's +journalist +journalist's +journalistic +journalists +journals +journey +journey's +journeyed +journeying +journeyman +journeyman's +journeymen +journeys +joust +joust's +jousted +jousting +jousts +jovial +joviality +joviality's +jovially +jowl +jowl's +jowls +joy +joy's +joyed +joyful +joyfuller +joyfullest +joyfully +joyfulness +joyfulness's +joying +joyless +joyous +joyously +joyousness +joyousness's +joyridden +joyride +joyride's +joyrider +joyrider's +joyriders +joyrides +joyriding +joyriding's +joyrode +joys +joystick +joystick's +joysticks +jubilant +jubilantly +jubilation +jubilation's +jubilee +jubilee's +jubilees +judge +judge's +judged +judgement +judgement's +judgemental +judgements +judges +judgeship +judgeship's +judging +judgment +judgment's +judgmental +judgments +judicature +judicature's +judicial +judicially +judiciaries +judiciary +judiciary's +judicious +judiciously +judiciousness +judiciousness's +judo +judo's +jug +jug's +jugged +juggernaut +juggernaut's +juggernauts +jugging +juggle +juggle's +juggled +juggler +juggler's +jugglers +juggles +juggling +jugs +jugular +jugular's +jugulars +juice +juice's +juiced +juicer +juicer's +juicers +juices +juicier +juiciest +juicily +juiciness +juiciness's +juicing +juicy +jujitsu +jujitsu's +jujube +jujube's +jujubes +jujutsu +jujutsu's +jukebox +jukebox's +jukeboxes +julep +julep's +juleps +julienne +jumble +jumble's +jumbled +jumbles +jumbling +jumbo +jumbo's +jumbos +jump +jump's +jumped +jumper +jumper's +jumpers +jumpier +jumpiest +jumpiness +jumpiness's +jumping +jumps +jumpsuit +jumpsuit's +jumpsuits +jumpy +junco +junco's +juncoes +juncos +junction +junction's +junctions +juncture +juncture's +junctures +jungle +jungle's +jungles +junior +junior's +juniors +juniper +juniper's +junipers +junk +junk's +junked +junker +junker's +junkers +junket +junket's +junketed +junketing +junkets +junkie +junkie's +junkier +junkies +junkiest +junking +junks +junky +junky's +junkyard +junkyard's +junkyards +junta +junta's +juntas +juridical +juries +jurisdiction +jurisdiction's +jurisdictional +jurisprudence +jurisprudence's +jurist +jurist's +jurists +juror +juror's +jurors +jury +jury's +just +juster +justest +justice +justice's +justices +justifiable +justifiably +justification +justification's +justifications +justified +justifies +justify +justifying +justly +justness +justness's +jut +jut's +jute +jute's +juts +jutted +jutting +juvenile +juvenile's +juveniles +juxtapose +juxtaposed +juxtaposes +juxtaposing +juxtaposition +juxtaposition's +juxtapositions +k +kHz +kW +kabob +kabob's +kabobs +kaboom +kaftan +kaftan's +kaftans +kale +kale's +kaleidoscope +kaleidoscope's +kaleidoscopes +kaleidoscopic +kamikaze +kamikaze's +kamikazes +kangaroo +kangaroo's +kangaroos +kaolin +kaolin's +kapok +kapok's +kaput +karakul +karakul's +karaoke +karaoke's +karaokes +karat +karat's +karate +karate's +karats +karma +karma's +katydid +katydid's +katydids +kayak +kayak's +kayaked +kayaking +kayaks +kazoo +kazoo's +kazoos +kebab +kebab's +kebabs +kebob +kebob's +kebobs +keel +keel's +keeled +keeling +keels +keen +keen's +keened +keener +keenest +keening +keenly +keenness +keenness's +keens +keep +keep's +keeper +keeper's +keepers +keeping +keeping's +keeps +keepsake +keepsake's +keepsakes +keg +keg's +kegs +kelp +kelp's +ken +ken's +kenned +kennel +kennel's +kenneled +kenneling +kennelled +kennelling +kennels +kenning +kens +kept +keratin +keratin's +kerchief +kerchief's +kerchiefs +kerchieves +kernel +kernel's +kernels +kerosene +kerosene's +kerosine +kerosine's +kestrel +kestrel's +kestrels +ketch +ketch's +ketches +ketchup +ketchup's +kettle +kettle's +kettledrum +kettledrum's +kettledrums +kettles +key +key's +keybinding +keybindings +keyboard +keyboard's +keyboarded +keyboarder +keyboarder's +keyboarders +keyboarding +keyboards +keyed +keyhole +keyhole's +keyholes +keying +keynote +keynote's +keynoted +keynotes +keynoting +keypunch +keypunch's +keypunched +keypunches +keypunching +keys +keystone +keystone's +keystones +keystroke +keystroke's +keystrokes +keyword +keyword's +keywords +khaki +khaki's +khakis +khan +khan's +khans +kibbutz +kibbutz's +kibbutzim +kibitz +kibitzed +kibitzer +kibitzer's +kibitzers +kibitzes +kibitzing +kibosh +kibosh's +kick +kick's +kickback +kickback's +kickbacks +kicked +kicker +kicker's +kickers +kickier +kickiest +kicking +kickoff +kickoff's +kickoffs +kicks +kickstand +kickstand's +kickstands +kicky +kid +kid's +kidded +kidder +kidder's +kidders +kiddie +kiddie's +kiddies +kidding +kiddo +kiddo's +kiddoes +kiddos +kiddy +kiddy's +kidnap +kidnaped +kidnaper +kidnaper's +kidnapers +kidnaping +kidnapped +kidnapper +kidnapper's +kidnappers +kidnapping +kidnapping's +kidnappings +kidnaps +kidney +kidney's +kidneys +kids +kielbasa +kielbasa's +kielbasas +kielbasy +kill +kill's +killdeer +killdeer's +killdeers +killed +killer +killer's +killers +killing +killing's +killings +killjoy +killjoy's +killjoys +kills +kiln +kiln's +kilned +kilning +kilns +kilo +kilo's +kilobyte +kilobyte's +kilobytes +kilocycle +kilocycle's +kilocycles +kilogram +kilogram's +kilograms +kilohertz +kilohertz's +kilohertzes +kilometer +kilometer's +kilometers +kilos +kiloton +kiloton's +kilotons +kilowatt +kilowatt's +kilowatts +kilt +kilt's +kilter +kilter's +kilts +kimono +kimono's +kimonos +kin +kin's +kind +kind's +kinda +kinder +kindergarten +kindergarten's +kindergartener +kindergartener's +kindergarteners +kindergartens +kindergärtner +kindergärtner's +kindergärtners +kindest +kindhearted +kindle +kindled +kindles +kindlier +kindliest +kindliness +kindliness's +kindling +kindling's +kindly +kindness +kindness's +kindnesses +kindred +kindred's +kinds +kinematic +kinematics +kinetic +kinfolk +kinfolk's +kinfolks +kinfolks's +king +king's +kingdom +kingdom's +kingdoms +kingfisher +kingfisher's +kingfishers +kinglier +kingliest +kingly +kingpin +kingpin's +kingpins +kings +kingship +kingship's +kink +kink's +kinked +kinkier +kinkiest +kinking +kinks +kinky +kinship +kinship's +kinsman +kinsman's +kinsmen +kinswoman +kinswoman's +kinswomen +kiosk +kiosk's +kiosks +kipper +kipper's +kippered +kippering +kippers +kismet +kismet's +kiss +kiss's +kissed +kisser +kisser's +kissers +kisses +kissing +kit +kit's +kitchen +kitchen's +kitchenette +kitchenette's +kitchenettes +kitchens +kitchenware +kitchenware's +kite +kite's +kited +kites +kith +kith's +kiting +kits +kitsch +kitsch's +kitschy +kitten +kitten's +kittenish +kittens +kitties +kitty +kitty's +kiwi +kiwi's +kiwis +kleptomania +kleptomania's +kleptomaniac +kleptomaniac's +kleptomaniacs +klutz +klutz's +klutzes +klutzier +klutziest +klutzy +knack +knack's +knacker +knacks +knackwurst +knackwurst's +knackwursts +knapsack +knapsack's +knapsacks +knave +knave's +knavery +knavery's +knaves +knavish +knead +kneaded +kneader +kneader's +kneaders +kneading +kneads +knee +knee's +kneecap +kneecap's +kneecapped +kneecapping +kneecaps +kneed +kneeing +kneel +kneeled +kneeling +kneels +knees +knell +knell's +knelled +knelling +knells +knelt +knew +knickers +knickers's +knickknack +knickknack's +knickknacks +knife +knife's +knifed +knifes +knifing +knight +knight's +knighted +knighthood +knighthood's +knighthoods +knighting +knightly +knights +knit +knit's +knits +knitted +knitter +knitter's +knitters +knitting +knitting's +knitwear +knitwear's +knives +knob +knob's +knobbier +knobbiest +knobby +knobs +knock +knock's +knocked +knocker +knocker's +knockers +knocking +knockout +knockout's +knockouts +knocks +knockwurst +knockwurst's +knockwursts +knoll +knoll's +knolls +knot +knot's +knothole +knothole's +knotholes +knots +knotted +knottier +knottiest +knotting +knotty +know +knowable +knowing +knowingly +knowings +knowledge +knowledge's +knowledgeable +knowledgeably +known +knows +knuckle +knuckle's +knuckled +knucklehead +knucklehead's +knuckleheads +knuckles +knuckling +koala +koala's +koalas +kohlrabi +kohlrabi's +kohlrabies +kook +kook's +kookaburra +kookaburra's +kookaburras +kookie +kookier +kookiest +kookiness +kookiness's +kooks +kooky +kopeck +kopeck's +kopecks +kopek +kopek's +kopeks +kosher +koshered +koshering +koshers +kowtow +kowtow's +kowtowed +kowtowing +kowtows +krone +krone's +kroner +kronor +krypton +krypton's +króna +króna's +krónur +ks +kudos +kudos's +kudzu +kudzu's +kudzus +kumquat +kumquat's +kumquats +l +la +la's +lab +lab's +label +label's +labeled +labeling +labelled +labelling +labels +labia +labial +labial's +labials +labium +labium's +labor +labor's +laboratories +laboratory +laboratory's +labored +laborer +laborer's +laborers +laboring +laborious +laboriously +labors +labs +laburnum +laburnum's +laburnums +labyrinth +labyrinth's +labyrinthine +labyrinths +lace +lace's +laced +lacerate +lacerated +lacerates +lacerating +laceration +laceration's +lacerations +laces +lachrymal +lachrymose +lacier +laciest +lacing +lack +lack's +lackadaisical +lackadaisically +lacked +lackey +lackey's +lackeys +lacking +lackluster +lacks +laconic +laconically +lacquer +lacquer's +lacquered +lacquering +lacquers +lacrimal +lacrosse +lacrosse's +lactate +lactated +lactates +lactating +lactation +lactation's +lactic +lactose +lactose's +lacuna +lacuna's +lacunae +lacunas +lacy +lad +lad's +ladder +ladder's +laddered +laddering +ladders +laddie +laddie's +laddies +lade +laded +laden +lades +ladies +lading +lading's +ladings +ladle +ladle's +ladled +ladles +ladling +lads +lady +lady's +ladybird +ladybird's +ladybirds +ladybug +ladybug's +ladybugs +ladyfinger +ladyfinger's +ladyfingers +ladylike +ladyship +ladyship's +lag +lag's +lager +lager's +lagers +laggard +laggard's +laggards +lagged +lagging +lagniappe +lagniappe's +lagniappes +lagoon +lagoon's +lagoons +lags +laid +lain +lair +lair's +lairs +laity +laity's +lake +lake's +lakes +lallygag +lallygagged +lallygagging +lallygags +lam +lam's +lama +lama's +lamas +lamaseries +lamasery +lamasery's +lamb +lamb's +lambast +lambaste +lambasted +lambastes +lambasting +lambasts +lambda +lambed +lambent +lambing +lambkin +lambkin's +lambkins +lambs +lambskin +lambskin's +lambskins +lame +lame's +lamebrain +lamebrain's +lamebrains +lamed +lamely +lameness +lameness's +lament +lament's +lamentable +lamentably +lamentation +lamentation's +lamentations +lamented +lamenting +laments +lamer +lames +lamest +laminate +laminate's +laminated +laminates +laminating +lamination +lamination's +laming +lammed +lamming +lamp +lamp's +lampblack +lampblack's +lampoon +lampoon's +lampooned +lampooning +lampoons +lamppost +lamppost's +lampposts +lamprey +lamprey's +lampreys +lamps +lampshade +lampshade's +lampshades +lams +lance +lance's +lanced +lancer +lancer's +lancers +lances +lancet +lancet's +lancets +lancing +land +land's +landed +lander +landfall +landfall's +landfalls +landfill +landfill's +landfills +landholder +landholder's +landholders +landing +landing's +landings +landladies +landlady +landlady's +landline +landline's +landlines +landlocked +landlord +landlord's +landlords +landlubber +landlubber's +landlubbers +landmark +landmark's +landmarks +landmass +landmass's +landmasses +landowner +landowner's +landowners +lands +landscape +landscape's +landscaped +landscaper +landscaper's +landscapers +landscapes +landscaping +landslid +landslidden +landslide +landslide's +landslides +landsliding +landward +landwards +lane +lane's +lanes +language +language's +languages +languid +languidly +languish +languished +languishes +languishing +languor +languor's +languorous +languorously +languors +lank +lanker +lankest +lankier +lankiest +lankiness +lankiness's +lanky +lanolin +lanolin's +lantern +lantern's +lanterns +lanyard +lanyard's +lanyards +lap +lap's +lapel +lapel's +lapels +lapidaries +lapidary +lapidary's +lapped +lapping +laps +lapse +lapse's +lapsed +lapses +lapsing +laptop +laptop's +laptops +lapwing +lapwing's +lapwings +larboard +larboard's +larboards +larcenies +larcenous +larceny +larceny's +larch +larch's +larches +lard +lard's +larded +larder +larder's +larders +larding +lards +large +large's +largely +largeness +largeness's +larger +larges +largess +largess's +largesse +largesse's +largest +largo +largo's +largos +lariat +lariat's +lariats +lark +lark's +larked +larking +larks +larkspur +larkspur's +larkspurs +larva +larva's +larvae +larval +larvas +larynges +laryngitis +laryngitis's +larynx +larynx's +larynxes +lasagna +lasagna's +lasagnas +lasagne +lasagne's +lasagnes +lascivious +lasciviously +lasciviousness +lasciviousness's +laser +laser's +lasers +lash +lash's +lashed +lashes +lashing +lass +lass's +lasses +lassie +lassie's +lassies +lassitude +lassitude's +lasso +lasso's +lassoed +lassoes +lassoing +lassos +last +last's +lasted +lasting +lastingly +lastly +lasts +latch +latch's +latched +latches +latching +late +latecomer +latecomer's +latecomers +lately +latency +latency's +lateness +lateness's +latent +later +lateral +lateral's +lateraled +lateraling +lateralled +lateralling +laterally +laterals +latest +latest's +latex +latex's +lath +lath's +lathe +lathe's +lathed +lather +lather's +lathered +lathering +lathers +lathes +lathing +laths +latitude +latitude's +latitudes +latitudinal +latrine +latrine's +latrines +lats +latte +latte's +latter +latter's +latterly +lattes +lattice +lattice's +latticed +lattices +latticework +latticework's +latticeworks +laud +laud's +laudable +laudably +laudanum +laudanum's +laudatory +lauded +lauding +lauds +laugh +laugh's +laughable +laughably +laughed +laughing +laughingly +laughingstock +laughingstock's +laughingstocks +laughs +laughter +laughter's +launch +launch's +launched +launcher +launcher's +launchers +launches +launching +launder +laundered +launderer +launderer's +launderers +laundering +launders +laundress +laundress's +laundresses +laundries +laundry +laundry's +laundryman +laundryman's +laundrymen +laureate +laureate's +laureates +laurel +laurel's +laurels +lava +lava's +lavatories +lavatory +lavatory's +lavender +lavender's +lavenders +lavish +lavished +lavisher +lavishes +lavishest +lavishing +lavishly +lavishness +lavishness's +law +law's +lawbreaker +lawbreaker's +lawbreakers +lawful +lawfully +lawfulness +lawfulness's +lawgiver +lawgiver's +lawgivers +lawless +lawlessly +lawlessness +lawlessness's +lawmaker +lawmaker's +lawmakers +lawn +lawn's +lawns +lawrencium +lawrencium's +laws +lawsuit +lawsuit's +lawsuits +lawyer +lawyer's +lawyers +lax +laxative +laxative's +laxatives +laxer +laxest +laxity +laxity's +laxly +laxness +laxness's +lay +lay's +layaway +layaway's +layer +layer's +layered +layering +layers +layette +layette's +layettes +laying +layman +layman's +laymen +layoff +layoff's +layoffs +layout +layout's +layouts +layover +layover's +layovers +laypeople +layperson +layperson's +laypersons +lays +laywoman +laywoman's +laywomen +laze +laze's +lazed +lazes +lazied +lazier +lazies +laziest +lazily +laziness +laziness's +lazing +lazy +lazybones +lazybones's +lazying +lea +lea's +leach +leached +leaches +leaching +lead +lead's +leaded +leaden +leader +leader's +leaders +leadership +leadership's +leading +leading's +leads +leaf +leaf's +leafed +leafier +leafiest +leafing +leafless +leaflet +leaflet's +leafleted +leafleting +leaflets +leafletted +leafletting +leafs +leafy +league +league's +leagued +leagues +leaguing +leak +leak's +leakage +leakage's +leakages +leaked +leakier +leakiest +leaking +leaks +leaky +lean +lean's +leaned +leaner +leanest +leaning +leaning's +leanings +leanness +leanness's +leans +leap +leap's +leaped +leapfrog +leapfrog's +leapfrogged +leapfrogging +leapfrogs +leaping +leaps +leapt +learn +learned +learner +learner's +learners +learning +learning's +learns +learnt +leas +lease +lease's +leased +leasehold +leasehold's +leaseholder +leaseholder's +leaseholders +leaseholds +leases +leash +leash's +leashed +leashes +leashing +leasing +least +least's +leastwise +leather +leather's +leatherneck +leatherneck's +leathernecks +leathers +leathery +leave +leave's +leaved +leaven +leaven's +leavened +leavening +leavening's +leavens +leaves +leaving +leavings +leavings's +lecher +lecher's +lecherous +lecherously +lechers +lechery +lechery's +lecithin +lecithin's +lectern +lectern's +lecterns +lecture +lecture's +lectured +lecturer +lecturer's +lecturers +lectures +lecturing +led +ledge +ledge's +ledger +ledger's +ledgers +ledges +lee +lee's +leech +leech's +leeched +leeches +leeching +leek +leek's +leeks +leer +leer's +leered +leerier +leeriest +leering +leers +leery +lees +leeward +leeward's +leewards +leeway +leeway's +left +left's +lefter +leftest +leftie +leftie's +lefties +leftism +leftism's +leftist +leftist's +leftists +leftmost +leftover +leftover's +leftovers +lefts +leftwards +lefty +lefty's +leg +leg's +legacies +legacy +legacy's +legal +legal's +legalese +legalese's +legalism +legalism's +legalisms +legalistic +legality +legality's +legalization +legalization's +legalize +legalized +legalizes +legalizing +legally +legals +legate +legate's +legatee +legatee's +legatees +legates +legation +legation's +legations +legato +legato's +legatos +legend +legend's +legendary +legends +legerdemain +legerdemain's +legged +leggier +leggiest +leggin +leggin's +legging +legging's +leggings +leggins +leggy +legibility +legibility's +legible +legibly +legion +legion's +legionnaire +legionnaire's +legionnaires +legions +legislate +legislated +legislates +legislating +legislation +legislation's +legislative +legislator +legislator's +legislators +legislature +legislature's +legislatures +legit +legitimacy +legitimacy's +legitimate +legitimated +legitimately +legitimates +legitimating +legitimize +legitimized +legitimizes +legitimizing +legless +legman +legman's +legmen +legroom +legroom's +legrooms +legs +legume +legume's +legumes +leguminous +legwork +legwork's +lei +lei's +leis +leisure +leisure's +leisurely +leitmotif +leitmotif's +leitmotifs +lemma +lemmas +lemme +lemming +lemming's +lemmings +lemon +lemon's +lemonade +lemonade's +lemons +lemony +lemur +lemur's +lemurs +lend +lender +lender's +lenders +lending +lends +length +length's +lengthen +lengthened +lengthening +lengthens +lengthier +lengthiest +lengthily +lengths +lengthways +lengthwise +lengthy +leniency +leniency's +lenient +leniently +lens +lens's +lenses +lent +lentil +lentil's +lentils +leonine +leopard +leopard's +leopards +leotard +leotard's +leotards +leper +leper's +lepers +leprechaun +leprechaun's +leprechauns +leprosy +leprosy's +leprous +lept +lesbian +lesbian's +lesbianism +lesbianism's +lesbians +lesion +lesion's +lesions +less +less's +lessee +lessee's +lessees +lessen +lessened +lessening +lessens +lesser +lesson +lesson's +lessons +lessor +lessor's +lessors +lest +let +let's +letdown +letdown's +letdowns +lethal +lethally +lethargic +lethargically +lethargy +lethargy's +lets +letter +letter's +letterbox +lettered +letterhead +letterhead's +letterheads +lettering +lettering's +letters +letting +lettuce +lettuce's +lettuces +letup +letup's +letups +leukemia +leukemia's +leukocyte +leukocyte's +leukocytes +levee +levee's +levees +level +level's +leveled +leveler +leveler's +levelers +levelheaded +levelheadedness +levelheadedness's +leveling +levelled +leveller's +levellers +levelling +levelness +levelness's +levels +lever +lever's +leverage +leverage's +leveraged +leverages +leveraging +levered +levering +levers +leviathan +leviathan's +leviathans +levied +levies +levitate +levitated +levitates +levitating +levitation +levitation's +levity +levity's +levy +levy's +levying +lewd +lewder +lewdest +lewdly +lewdness +lewdness's +lexica +lexical +lexicographer +lexicographer's +lexicographers +lexicography +lexicography's +lexicon +lexicon's +lexicons +liabilities +liability +liability's +liable +liaise +liaised +liaises +liaising +liaison +liaison's +liaisons +liar +liar's +liars +lib +lib's +libation +libation's +libations +libel +libel's +libeled +libeler +libeler's +libelers +libeling +libelled +libeller +libeller's +libellers +libelling +libellous +libelous +libels +liberal +liberal's +liberalism +liberalism's +liberality +liberality's +liberalization +liberalization's +liberalizations +liberalize +liberalized +liberalizes +liberalizing +liberally +liberals +liberate +liberated +liberates +liberating +liberation +liberation's +liberator +liberator's +liberators +libertarian +libertarian's +libertarians +liberties +libertine +libertine's +libertines +liberty +liberty's +libidinous +libido +libido's +libidos +librarian +librarian's +librarians +libraries +library +library's +libretti +librettist +librettist's +librettists +libretto +libretto's +librettos +lice +licence +licence's +licenced +licences +licencing +license +license's +licensed +licensee +licensee's +licensees +licenses +licensing +licentiate +licentiate's +licentiates +licentious +licentiously +licentiousness +licentiousness's +lichee +lichee's +lichees +lichen +lichen's +lichens +licit +lick +lick's +licked +licking +licking's +lickings +licks +licorice +licorice's +licorices +lid +lid's +lidded +lids +lie +lie's +lied +lief +liefer +liefest +liege +liege's +lieges +lien +lien's +liens +lies +lieu +lieu's +lieutenancy +lieutenancy's +lieutenant +lieutenant's +lieutenants +life +life's +lifeblood +lifeblood's +lifeboat +lifeboat's +lifeboats +lifeforms +lifeguard +lifeguard's +lifeguards +lifeless +lifelike +lifeline +lifeline's +lifelines +lifelong +lifer +lifer's +lifers +lifesaver +lifesaver's +lifesavers +lifesaving +lifesaving's +lifespan +lifespans +lifestyle +lifestyle's +lifestyles +lifetime +lifetime's +lifetimes +lifework +lifework's +lifeworks +lift +lift's +lifted +lifting +liftoff +liftoff's +liftoffs +lifts +ligament +ligament's +ligaments +ligature +ligature's +ligatured +ligatures +ligaturing +light +light's +lighted +lighten +lightened +lightening +lightens +lighter +lighter's +lighters +lightest +lightheaded +lighthearted +lightheartedly +lightheartedness +lightheartedness's +lighthouse +lighthouse's +lighthouses +lighting +lighting's +lightly +lightness +lightness's +lightning +lightning's +lightninged +lightnings +lights +lightweight +lightweight's +lightweights +lignite +lignite's +likable +likableness +likableness's +like +like's +likeable +likeableness +likeableness's +liked +likelier +likeliest +likelihood +likelihood's +likelihoods +likely +liken +likened +likeness +likeness's +likenesses +likening +likens +liker +likes +likest +likewise +liking +liking's +lilac +lilac's +lilacs +lilies +lilt +lilt's +lilted +lilting +lilts +lily +lily's +limb +limb's +limber +limbered +limbering +limbers +limbless +limbo +limbo's +limbos +limbs +lime +lime's +limeade +limeade's +limeades +limed +limelight +limelight's +limerick +limerick's +limericks +limes +limestone +limestone's +limier +limiest +liming +limit +limit's +limitation +limitation's +limitations +limited +limiting +limitings +limitless +limits +limn +limned +limning +limns +limo +limo's +limos +limousine +limousine's +limousines +limp +limp's +limped +limper +limpest +limpet +limpet's +limpets +limpid +limpidity +limpidity's +limpidly +limping +limply +limpness +limpness's +limps +limy +linage +linage's +linchpin +linchpin's +linchpins +linden +linden's +lindens +line +line's +lineage +lineage's +lineages +lineal +lineally +lineament +lineament's +lineaments +linear +linearly +linebacker +linebacker's +linebackers +lined +linefeed +lineman +lineman's +linemen +linen +linen's +linens +linens's +liner +liner's +liners +lines +linesman +linesman's +linesmen +lineup +lineup's +lineups +linger +lingered +lingerer +lingerer's +lingerers +lingerie +lingerie's +lingering +lingeringly +lingerings +lingers +lingo +lingo's +lingoes +lingos +lingual +linguist +linguist's +linguistic +linguistics +linguistics's +linguists +liniment +liniment's +liniments +lining +lining's +linings +link +link's +linkage +linkage's +linkages +linked +linker +linking +links +linkup +linkup's +linkups +linnet +linnet's +linnets +linoleum +linoleum's +linseed +linseed's +lint +lint's +lintel +lintel's +lintels +lion +lion's +lioness +lioness's +lionesses +lionhearted +lionize +lionized +lionizes +lionizing +lions +lip +lip's +lipid +lipid's +lipids +liposuction +liposuction's +lipread +lipreading +lipreading's +lipreads +lips +lipstick +lipstick's +lipsticked +lipsticking +lipsticks +liquefaction +liquefaction's +liquefied +liquefies +liquefy +liquefying +liqueur +liqueur's +liqueurs +liquid +liquid's +liquidate +liquidated +liquidates +liquidating +liquidation +liquidation's +liquidations +liquidator +liquidator's +liquidators +liquidity +liquidity's +liquidize +liquidized +liquidizes +liquidizing +liquids +liquified +liquifies +liquify +liquifying +liquor +liquor's +liquored +liquoring +liquors +lira +lira's +liras +lire +lisle +lisle's +lisp +lisp's +lisped +lisping +lisps +lissom +lissome +list +list's +listed +listen +listen's +listened +listener +listener's +listeners +listening +listens +listing +listing's +listings +listless +listlessly +listlessness +listlessness's +lists +lit +litanies +litany +litany's +litchi +litchi's +litchis +lite +liter +liter's +literacy +literacy's +literal +literal's +literally +literals +literary +literate +literate's +literates +literati +literati's +literature +literature's +liters +lithe +lither +lithest +lithium +lithium's +lithograph +lithograph's +lithographed +lithographer +lithographer's +lithographers +lithographic +lithographing +lithographs +lithography +lithography's +lithosphere +lithosphere's +lithospheres +litigant +litigant's +litigants +litigate +litigated +litigates +litigating +litigation +litigation's +litigious +litigiousness +litigiousness's +litmus +litmus's +litter +litter's +litterbug +litterbug's +litterbugs +littered +littering +litters +little +little's +littleness +littleness's +littler +littlest +littoral +littoral's +littorals +liturgical +liturgies +liturgy +liturgy's +livability +livability's +livable +live +liveable +lived +livelier +liveliest +livelihood +livelihood's +livelihoods +liveliness +liveliness's +livelong +livelongs +lively +liven +livened +livening +livens +liver +liver's +liveried +liveries +livers +liverwurst +liverwurst's +livery +livery's +lives +livest +livestock +livestock's +livid +lividly +living +living's +livings +lizard +lizard's +lizards +llama +llama's +llamas +llano +llano's +llanos +lo +load +load's +loadable +loaded +loader +loader's +loaders +loading +loads +loadstar +loadstar's +loadstars +loadstone +loadstone's +loadstones +loaf +loaf's +loafed +loafer +loafer's +loafers +loafing +loafs +loam +loam's +loamier +loamiest +loamy +loan +loan's +loaned +loaner +loaner's +loaners +loaning +loans +loanword +loanword's +loanwords +loath +loathe +loathed +loathes +loathing +loathing's +loathings +loathsome +loathsomeness +loathsomeness's +loaves +lob +lob's +lobbed +lobbied +lobbies +lobbing +lobby +lobby's +lobbying +lobbyist +lobbyist's +lobbyists +lobe +lobe's +lobed +lobes +lobotomies +lobotomy +lobotomy's +lobs +lobster +lobster's +lobsters +local +local's +locale +locale's +locales +localities +locality +locality's +localization +localization's +localize +localized +localizes +localizing +locally +locals +locate +located +locates +locating +location +location's +locations +locavore +locavore's +locavores +loci +lock +lock's +lockable +locked +locker +locker's +lockers +locket +locket's +lockets +locking +lockjaw +lockjaw's +lockout +lockout's +lockouts +locks +locksmith +locksmith's +locksmiths +lockstep +lockstep's +lockup +lockup's +lockups +loco +locomotion +locomotion's +locomotive +locomotive's +locomotives +locoweed +locoweed's +locoweeds +locus +locus's +locust +locust's +locusts +locution +locution's +locutions +lode +lode's +lodes +lodestar +lodestar's +lodestars +lodestone +lodestone's +lodestones +lodge +lodge's +lodged +lodger +lodger's +lodgers +lodges +lodging +lodging's +lodgings +lodgings's +loft +loft's +lofted +loftier +loftiest +loftily +loftiness +loftiness's +lofting +lofts +lofty +log +log's +loganberries +loganberry +loganberry's +logarithm +logarithm's +logarithmic +logarithms +logbook +logbook's +logbooks +loge +loge's +loges +logged +logger +logger's +loggerhead +loggerhead's +loggerheads +loggers +logging +logging's +logic +logic's +logical +logically +logician +logician's +logicians +login +login's +logins +logistic +logistical +logistically +logistics +logistics's +logjam +logjam's +logjams +logo +logo's +logoff +logoff's +logoffs +logon +logon's +logons +logos +logotype +logotype's +logotypes +logout +logout's +logouts +logrolling +logrolling's +logs +loin +loin's +loincloth +loincloth's +loincloths +loins +loiter +loitered +loiterer +loiterer's +loiterers +loitering +loiters +lolcat +lolcat's +lolcats +loll +lolled +lolling +lollipop +lollipop's +lollipops +lolls +lollygag +lollygagged +lollygagging +lollygags +lollypop +lollypop's +lollypops +lone +lonelier +loneliest +loneliness +loneliness's +lonely +loner +loner's +loners +lonesome +long +long's +longboat +longboat's +longboats +longed +longer +longest +longevity +longevity's +longhair +longhair's +longhairs +longhand +longhand's +longhorn +longhorn's +longhorns +longing +longing's +longingly +longings +longish +longitude +longitude's +longitudes +longitudinal +longitudinally +longs +longshoreman +longshoreman's +longshoremen +longtime +loofah +look +look's +lookalike +lookalike's +lookalikes +looked +looking +lookout +lookout's +lookouts +looks +lookup +loom +loom's +loomed +looming +looms +loon +loon's +looney +looney's +looneyier +looneyies +looneys +loonie +loonie's +loonier +loonies +looniest +loons +loony +loony's +loop +loop's +looped +loophole +loophole's +loopholes +loopier +loopiest +looping +loops +loopy +loose +loosed +loosely +loosen +loosened +looseness +looseness's +loosening +loosens +looser +looses +loosest +loosing +loot +loot's +looted +looter +looter's +looters +looting +loots +lop +lope +lope's +loped +lopes +loping +lopped +lopping +lops +lopsided +lopsidedly +lopsidedness +lopsidedness's +loquacious +loquacity +loquacity's +lord +lord's +lorded +lording +lordlier +lordliest +lordly +lords +lordship +lordship's +lordships +lore +lore's +lorgnette +lorgnette's +lorgnettes +lorn +lorries +lorry +lorry's +lose +loser +loser's +losers +loses +losing +loss +loss's +losses +lost +lot +lot's +loth +lotion +lotion's +lotions +lots +lotteries +lottery +lottery's +lotto +lotto's +lotus +lotus's +lotuses +loud +louder +loudest +loudly +loudmouth +loudmouth's +loudmouthed +loudmouths +loudness +loudness's +loudspeaker +loudspeaker's +loudspeakers +lounge +lounge's +lounged +lounges +lounging +louse +louse's +louses +lousier +lousiest +lousiness +lousiness's +lousy +lout +lout's +loutish +louts +louver +louver's +louvered +louvers +louvred +lovable +love +love's +loveable +lovebird +lovebird's +lovebirds +loved +loveless +lovelier +lovelies +loveliest +loveliness +loveliness's +lovelorn +lovely +lovely's +lovemaking +lovemaking's +lover +lover's +lovers +loves +lovesick +loving +lovingly +low +low's +lowbrow +lowbrow's +lowbrows +lowdown +lowdown's +lowed +lower +lowercase +lowercase's +lowered +lowering +lowers +lowest +lowing +lowish +lowland +lowland's +lowlands +lowlier +lowliest +lowliness +lowliness's +lowly +lowness +lowness's +lows +lox +lox's +loxes +loyal +loyaler +loyalest +loyalist +loyalist's +loyalists +loyaller +loyallest +loyally +loyalties +loyalty +loyalty's +lozenge +lozenge's +lozenges +ls +luau +luau's +luaus +lubber +lubber's +lubbers +lube +lube's +lubed +lubes +lubing +lubricant +lubricant's +lubricants +lubricate +lubricated +lubricates +lubricating +lubrication +lubrication's +lubricator +lubricator's +lubricators +lucid +lucidity +lucidity's +lucidly +lucidness +lucidness's +luck +luck's +lucked +luckier +luckiest +luckily +luckiness +luckiness's +lucking +luckless +lucks +lucky +lucrative +lucratively +lucre +lucre's +ludicrous +ludicrously +ludicrousness +ludicrousness's +lug +lug's +luggage +luggage's +lugged +lugging +lugs +lugubrious +lugubriously +lugubriousness +lugubriousness's +lukewarm +lull +lull's +lullabies +lullaby +lullaby's +lulled +lulling +lulls +lumbago +lumbago's +lumbar +lumber +lumber's +lumbered +lumbering +lumbering's +lumberjack +lumberjack's +lumberjacks +lumberman +lumberman's +lumbermen +lumbers +lumberyard +lumberyard's +lumberyards +luminaries +luminary +luminary's +luminescence +luminescence's +luminescent +luminosity +luminosity's +luminous +luminously +lummox +lummox's +lummoxes +lump +lump's +lumped +lumpier +lumpiest +lumpiness +lumpiness's +lumping +lumpish +lumps +lumpy +lunacies +lunacy +lunacy's +lunar +lunatic +lunatic's +lunatics +lunch +lunch's +lunchbox +lunched +luncheon +luncheon's +luncheonette +luncheonette's +luncheonettes +luncheons +lunches +lunching +lunchroom +lunchroom's +lunchrooms +lunchtime +lunchtime's +lunchtimes +lung +lung's +lunge +lunge's +lunged +lunges +lunging +lungs +lupin +lupin's +lupine +lupine's +lupines +lupins +lupus +lupus's +lurch +lurch's +lurched +lurches +lurching +lure +lure's +lured +lures +lurid +luridly +luridness +luridness's +luring +lurk +lurked +lurking +lurks +luscious +lusciously +lusciousness +lusciousness's +lush +lush's +lusher +lushes +lushest +lushness +lushness's +lust +lust's +lusted +luster +luster's +lustful +lustfully +lustier +lustiest +lustily +lustiness +lustiness's +lusting +lustre +lustre's +lustrous +lusts +lusty +lute +lute's +lutes +luxuriance +luxuriance's +luxuriant +luxuriantly +luxuriate +luxuriated +luxuriates +luxuriating +luxuries +luxurious +luxuriously +luxuriousness +luxuriousness's +luxury +luxury's +lyceum +lyceum's +lyceums +lychee +lychee's +lychees +lye +lye's +lying +lying's +lymph +lymph's +lymphatic +lymphatic's +lymphatics +lymphoma +lymphoma's +lymphomas +lymphomata +lynch +lynched +lynches +lynching +lynching's +lynchings +lynchpin +lynchpin's +lynchpins +lynx +lynx's +lynxes +lyre +lyre's +lyres +lyric +lyric's +lyrical +lyrically +lyricist +lyricist's +lyricists +lyrics +m +ma +ma'am +ma's +macabre +macadam +macadam's +macaroni +macaroni's +macaronies +macaronis +macaroon +macaroon's +macaroons +macaw +macaw's +macaws +mace +mace's +maced +macerate +macerated +macerates +macerating +maceration +maceration's +maces +machete +machete's +machetes +machination +machination's +machinations +machine +machine's +machined +machinery +machinery's +machines +machining +machinist +machinist's +machinists +machismo +machismo's +macho +macho's +macing +macintosh +macintosh's +macintoshes +mackerel +mackerel's +mackerels +mackinaw +mackinaw's +mackinaws +mackintosh +mackintosh's +mackintoshes +macramé +macramé's +macro +macro's +macrobiotic +macrobiotics +macrobiotics's +macrocosm +macrocosm's +macrocosms +macron +macron's +macrons +macros +macroscopic +mad +mad's +madam +madam's +madame +madame's +madams +madcap +madcap's +madcaps +madden +maddened +maddening +maddeningly +maddens +madder +madder's +madders +maddest +made +mademoiselle +mademoiselle's +mademoiselles +madhouse +madhouse's +madhouses +madly +madman +madman's +madmen +madness +madness's +madras +madras's +madrasa +madrasa's +madrasah +madrasah's +madrasahs +madrasas +madrases +madrassa +madrassa's +madrassas +madrigal +madrigal's +madrigals +mads +madwoman +madwoman's +madwomen +maelstrom +maelstrom's +maelstroms +maestri +maestro +maestro's +maestros +magazine +magazine's +magazines +magenta +magenta's +maggot +maggot's +maggots +magic +magic's +magical +magically +magician +magician's +magicians +magisterial +magisterially +magistrate +magistrate's +magistrates +magma +magma's +magnanimity +magnanimity's +magnanimous +magnanimously +magnate +magnate's +magnates +magnesia +magnesia's +magnesium +magnesium's +magnet +magnet's +magnetic +magnetically +magnetism +magnetism's +magnetization +magnetization's +magnetize +magnetized +magnetizes +magnetizing +magneto +magneto's +magnetos +magnetosphere +magnets +magnification +magnification's +magnifications +magnificence +magnificence's +magnificent +magnificently +magnified +magnifier +magnifier's +magnifiers +magnifies +magnify +magnifying +magnitude +magnitude's +magnitudes +magnolia +magnolia's +magnolias +magnum +magnum's +magnums +magpie +magpie's +magpies +maharaja +maharaja's +maharajah +maharajah's +maharajahs +maharajas +maharanee +maharanee's +maharanees +maharani +maharani's +maharanis +maharishi +maharishi's +maharishis +mahatma +mahatma's +mahatmas +mahjong +mahjong's +mahoganies +mahogany +mahogany's +maid +maid's +maiden +maiden's +maidenhair +maidenhair's +maidenhead +maidenhead's +maidenheads +maidenhood +maidenhood's +maidenly +maidens +maids +maidservant +maidservant's +maidservants +mail +mail's +mailbox +mailbox's +mailboxes +mailed +mailer +mailer's +mailers +mailing +mailing's +mailings +mailman +mailman's +mailmen +mails +maim +maimed +maiming +maims +main +main's +mainframe +mainframe's +mainframes +mainland +mainland's +mainlands +mainline +mainline's +mainlined +mainlines +mainlining +mainly +mainmast +mainmast's +mainmasts +mains +mainsail +mainsail's +mainsails +mainspring +mainspring's +mainsprings +mainstay +mainstay's +mainstays +mainstream +mainstream's +mainstreamed +mainstreaming +mainstreams +maintain +maintainability +maintainable +maintained +maintainer +maintainers +maintaining +maintains +maintenance +maintenance's +maize +maize's +maizes +majestic +majestically +majesties +majesty +majesty's +major +major's +majored +majorette +majorette's +majorettes +majoring +majorities +majority +majority's +majorly +majors +make +make's +maker +maker's +makers +makes +makeshift +makeshift's +makeshifts +makeup +makeup's +makeups +making +making's +makings +maladies +maladjusted +maladjustment +maladjustment's +maladroit +malady +malady's +malaise +malaise's +malapropism +malapropism's +malapropisms +malaria +malaria's +malarial +malarkey +malarkey's +malcontent +malcontent's +malcontents +male +male's +malediction +malediction's +maledictions +malefactor +malefactor's +malefactors +maleness +maleness's +males +malevolence +malevolence's +malevolent +malevolently +malfeasance +malfeasance's +malformation +malformation's +malformations +malformed +malfunction +malfunction's +malfunctioned +malfunctioning +malfunctions +malice +malice's +malicious +maliciously +malign +malignancies +malignancy +malignancy's +malignant +malignantly +maligned +maligning +malignity +malignity's +maligns +malinger +malingered +malingerer +malingerer's +malingerers +malingering +malingers +mall +mall's +mallard +mallard's +mallards +malleability +malleability's +malleable +mallet +mallet's +mallets +mallow +mallow's +mallows +malls +malnourished +malnutrition +malnutrition's +malodorous +malpractice +malpractice's +malpractices +malt +malt's +malted +malted's +malteds +malting +maltreat +maltreated +maltreating +maltreatment +maltreatment's +maltreats +malts +malware +malware's +mama +mama's +mamas +mambo +mambo's +mamboed +mamboing +mambos +mamma +mamma's +mammal +mammal's +mammalian +mammalian's +mammalians +mammals +mammary +mammas +mammogram +mammogram's +mammograms +mammography +mammography's +mammon +mammon's +mammoth +mammoth's +mammoths +man +man's +manacle +manacle's +manacled +manacles +manacling +manage +manageability +manageability's +manageable +managed +management +management's +manager +manager's +managerial +managers +manages +managing +manatee +manatee's +manatees +mandarin +mandarin's +mandarins +mandate +mandate's +mandated +mandates +mandating +mandatory +mandible +mandible's +mandibles +mandolin +mandolin's +mandolins +mandrake +mandrake's +mandrakes +mandrill +mandrill's +mandrills +mane +mane's +manes +maneuver +maneuver's +maneuverability +maneuverability's +maneuverable +maneuvered +maneuvering +maneuvers +manful +manfully +manga +manga's +manganese +manganese's +mange +mange's +manger +manger's +mangers +mangier +mangiest +mangle +mangle's +mangled +mangles +mangling +mango +mango's +mangoes +mangos +mangrove +mangrove's +mangroves +mangy +manhandle +manhandled +manhandles +manhandling +manhole +manhole's +manholes +manhood +manhood's +manhunt +manhunt's +manhunts +mania +mania's +maniac +maniac's +maniacal +maniacs +manias +manic +manic's +manics +manicure +manicure's +manicured +manicures +manicuring +manicurist +manicurist's +manicurists +manifest +manifest's +manifestation +manifestation's +manifestations +manifested +manifesting +manifestly +manifesto +manifesto's +manifestoes +manifestos +manifests +manifold +manifold's +manifolded +manifolding +manifolds +manikin +manikin's +manikins +manipulate +manipulated +manipulates +manipulating +manipulation +manipulation's +manipulations +manipulative +manipulator +manipulator's +manipulators +mankind +mankind's +manlier +manliest +manliness +manliness's +manly +manna +manna's +manned +mannequin +mannequin's +mannequins +manner +manner's +mannered +mannerism +mannerism's +mannerisms +mannerly +manners +mannikin +mannikin's +mannikins +manning +mannish +mannishly +mannishness +mannishness's +manor +manor's +manorial +manors +manpower +manpower's +manqué +mans +mansard +mansard's +mansards +manse +manse's +manservant +manservant's +manses +mansion +mansion's +mansions +manslaughter +manslaughter's +mantel +mantel's +mantelpiece +mantelpiece's +mantelpieces +mantels +mantes +mantilla +mantilla's +mantillas +mantis +mantis's +mantises +mantissa +mantle +mantle's +mantled +mantlepiece +mantlepieces +mantles +mantling +mantra +mantra's +mantras +manual +manual's +manually +manuals +manufacture +manufacture's +manufactured +manufacturer +manufacturer's +manufacturers +manufactures +manufacturing +manufacturing's +manumit +manumits +manumitted +manumitting +manure +manure's +manured +manures +manuring +manuscript +manuscript's +manuscripts +many +many's +manège +manège's +map +map's +maple +maple's +maples +mapped +mapper +mapping +mappings +maps +mar +marabou +marabou's +marabous +maraca +maraca's +maracas +marathon +marathon's +marathoner +marathoner's +marathoners +marathons +maraud +marauded +marauder +marauder's +marauders +marauding +marauds +marble +marble's +marbled +marbles +marbling +marbling's +march +march's +marched +marcher +marcher's +marchers +marches +marching +marchioness +marchioness's +marchionesses +mare +mare's +mares +margarine +margarine's +margarita +margarita's +margaritas +margin +margin's +marginal +marginalia +marginalia's +marginally +margins +maria +maria's +mariachi +mariachi's +mariachis +marigold +marigold's +marigolds +marihuana +marihuana's +marijuana +marijuana's +marimba +marimba's +marimbas +marina +marina's +marinade +marinade's +marinaded +marinades +marinading +marinas +marinate +marinated +marinates +marinating +marine +marine's +mariner +mariner's +mariners +marines +marionette +marionette's +marionettes +marital +maritime +marjoram +marjoram's +mark +mark's +markdown +markdown's +markdowns +marked +markedly +marker +marker's +markers +market +market's +marketability +marketability's +marketable +marketed +marketer +marketer's +marketers +marketing +marketing's +marketplace +marketplace's +marketplaces +markets +marking +marking's +markings +marks +marksman +marksman's +marksmanship +marksmanship's +marksmen +markup +markup's +markups +marlin +marlin's +marlins +marmalade +marmalade's +marmoset +marmoset's +marmosets +marmot +marmot's +marmots +maroon +maroon's +marooned +marooning +maroons +marquee +marquee's +marquees +marquess +marquess's +marquesses +marquetry +marquetry's +marquis +marquis's +marquise +marquise's +marquises +marred +marriage +marriage's +marriageable +marriages +married +married's +marrieds +marries +marring +marrow +marrow's +marrows +marry +marrying +mars +marsh +marsh's +marshal +marshal's +marshaled +marshaling +marshalled +marshalling +marshals +marshes +marshier +marshiest +marshmallow +marshmallow's +marshmallows +marshy +marsupial +marsupial's +marsupials +mart +mart's +marten +marten's +martens +martial +martin +martin's +martinet +martinet's +martinets +martini +martini's +martinis +martins +marts +martyr +martyr's +martyrdom +martyrdom's +martyred +martyring +martyrs +marvel +marvel's +marveled +marveling +marvelled +marvelling +marvellously +marvelous +marvelously +marvels +marzipan +marzipan's +mas +mascara +mascara's +mascaraed +mascaraing +mascaras +mascot +mascot's +mascots +masculine +masculine's +masculines +masculinity +masculinity's +mash +mash's +mashed +masher +masher's +mashers +mashes +mashing +mashup +mashup's +mashups +mask +mask's +masked +masking +masks +masochism +masochism's +masochist +masochist's +masochistic +masochists +mason +mason's +masonic +masonry +masonry's +masons +masque +masque's +masquerade +masquerade's +masqueraded +masquerader +masquerader's +masqueraders +masquerades +masquerading +masques +mass +mass's +massacre +massacre's +massacred +massacres +massacring +massage +massage's +massaged +massages +massaging +massed +masses +masseur +masseur's +masseurs +masseuse +masseuse's +masseuses +massing +massive +massively +massiveness +massiveness's +mast +mast's +mastectomies +mastectomy +mastectomy's +master +master's +mastered +masterful +masterfully +mastering +masterly +mastermind +mastermind's +masterminded +masterminding +masterminds +masterpiece +masterpiece's +masterpieces +masters +masterstroke +masterstroke's +masterstrokes +masterwork +masterwork's +masterworks +mastery +mastery's +masthead +masthead's +mastheads +masticate +masticated +masticates +masticating +mastication +mastication's +mastiff +mastiff's +mastiffs +mastodon +mastodon's +mastodons +mastoid +mastoid's +mastoids +masts +masturbate +masturbated +masturbates +masturbating +masturbation +masturbation's +mat +mat's +matador +matador's +matadors +match +match's +matchbook +matchbook's +matchbooks +matchbox +matchbox's +matchboxes +matched +matches +matching +matchless +matchmaker +matchmaker's +matchmakers +matchmaking +matchmaking's +matchstick +matchstick's +matchsticks +mate +mate's +mated +material +material's +materialism +materialism's +materialist +materialist's +materialistic +materialistically +materialists +materialization +materialization's +materialize +materialized +materializes +materializing +materially +materials +maternal +maternally +maternity +maternity's +mates +math +mathematical +mathematically +mathematician +mathematician's +mathematicians +mathematics +mathematics's +mating +matins +matins's +matinée +matinée's +matinées +matriarch +matriarch's +matriarchal +matriarchies +matriarchs +matriarchy +matriarchy's +matrices +matricide +matricide's +matricides +matriculate +matriculated +matriculates +matriculating +matriculation +matriculation's +matrimonial +matrimony +matrimony's +matrix +matrix's +matrixes +matron +matron's +matronly +matrons +mats +matt +matte +matte's +matted +matter +matter's +mattered +mattering +matters +mattes +matting +matting's +mattock +mattock's +mattocks +mattress +mattress's +mattresses +matts +maturation +maturation's +mature +matured +maturely +maturer +matures +maturest +maturing +maturities +maturity +maturity's +matzo +matzo's +matzoh +matzoh's +matzohs +matzos +matzot +matzoth +matériel +matériel's +maudlin +maul +maul's +mauled +mauling +mauls +maunder +maundered +maundering +maunders +mausolea +mausoleum +mausoleum's +mausoleums +mauve +mauve's +maven +maven's +mavens +maverick +maverick's +mavericks +mavin +mavin's +mavins +maw +maw's +mawkish +mawkishly +maws +maxed +maxes +maxilla +maxilla's +maxillae +maxillary +maxillas +maxim +maxim's +maxima +maximal +maximally +maximization +maximization's +maximize +maximized +maximizes +maximizing +maxims +maximum +maximum's +maximums +maxing +may +may's +maybe +maybe's +maybes +mayday +mayday's +maydays +mayflies +mayflower +mayflower's +mayflowers +mayfly +mayfly's +mayhem +mayhem's +mayo +mayo's +mayonnaise +mayonnaise's +mayor +mayor's +mayoral +mayoralty +mayoralty's +mayors +maypole +maypole's +maypoles +maze +maze's +mazes +mazourka +mazourka's +mazourkas +mazurka +mazurka's +mazurkas +me +mead +mead's +meadow +meadow's +meadowlark +meadowlark's +meadowlarks +meadows +meager +meagerly +meagerness +meagerness's +meal +meal's +mealier +mealiest +meals +mealtime +mealtime's +mealtimes +mealy +mean +mean's +meander +meander's +meandered +meandering +meanders +meaner +meanest +meaning +meaning's +meaningful +meaningfully +meaningless +meanings +meanly +meanness +meanness's +means +meant +meantime +meantime's +meanwhile +meanwhile's +measles +measles's +measlier +measliest +measly +measurable +measurably +measure +measure's +measured +measureless +measurement +measurement's +measurements +measures +measuring +meat +meat's +meatball +meatball's +meatballs +meatier +meatiest +meatloaf +meatloaf's +meatloaves +meats +meaty +mecca +mecca's +meccas +mechanic +mechanic's +mechanical +mechanically +mechanics +mechanics's +mechanism +mechanism's +mechanisms +mechanistic +mechanization +mechanization's +mechanize +mechanized +mechanizes +mechanizing +medal +medal's +medalist +medalist's +medalists +medallion +medallion's +medallions +medals +meddle +meddled +meddler +meddler's +meddlers +meddles +meddlesome +meddling +media +media's +mediaeval +medial +median +median's +medians +medias +mediate +mediated +mediates +mediating +mediation +mediation's +mediator +mediator's +mediators +medic +medic's +medical +medical's +medically +medicals +medicate +medicated +medicates +medicating +medication +medication's +medications +medicinal +medicinally +medicine +medicine's +medicines +medics +medieval +mediocre +mediocrities +mediocrity +mediocrity's +meditate +meditated +meditates +meditating +meditation +meditation's +meditations +meditative +meditatively +medium +medium's +mediums +medley +medley's +medleys +medulla +medulla's +medullae +medullas +meek +meeker +meekest +meekly +meekness +meekness's +meet +meet's +meeting +meeting's +meetinghouse +meetinghouse's +meetinghouses +meetings +meets +meg +megabyte +megabyte's +megabytes +megachurch +megachurch's +megachurches +megacycle +megacycle's +megacycles +megahertz +megahertz's +megahertzes +megalith +megalith's +megaliths +megalomania +megalomania's +megalomaniac +megalomaniac's +megalomaniacs +megalopolis +megalopolis's +megalopolises +megaphone +megaphone's +megaphoned +megaphones +megaphoning +megapixel +megapixel's +megapixels +megaton +megaton's +megatons +megs +meh +melancholia +melancholia's +melancholic +melancholics +melancholy +melancholy's +melange +melange's +melanges +melanin +melanin's +melanoma +melanoma's +melanomas +melanomata +meld +meld's +melded +melding +melds +mellifluous +mellifluously +mellow +mellowed +mellower +mellowest +mellowing +mellowness +mellowness's +mellows +melodic +melodically +melodies +melodious +melodiously +melodiousness +melodiousness's +melodrama +melodrama's +melodramas +melodramatic +melodramatically +melody +melody's +melon +melon's +melons +melt +melt's +meltdown +meltdown's +meltdowns +melted +melting +melts +member +member's +members +membership +membership's +memberships +membrane +membrane's +membranes +membranous +meme +meme's +memento +memento's +mementoes +mementos +memes +memo +memo's +memoir +memoir's +memoirs +memorabilia +memorabilia's +memorable +memorably +memoranda +memorandum +memorandum's +memorandums +memorial +memorial's +memorialize +memorialized +memorializes +memorializing +memorials +memories +memorization +memorization's +memorize +memorized +memorizes +memorizing +memory +memory's +memos +men +men's +menace +menace's +menaced +menaces +menacing +menacingly +menage +menage's +menagerie +menagerie's +menageries +menages +mend +mend's +mendacious +mendacity +mendacity's +mended +mender +mender's +menders +mendicant +mendicant's +mendicants +mending +mends +menfolk +menfolk's +menhaden +menhaden's +menhadens +menial +menial's +menially +menials +meningitis +meningitis's +menopausal +menopause +menopause's +menorah +menorah's +menorahs +menservants +menses +menses's +menstrual +menstruate +menstruated +menstruates +menstruating +menstruation +menstruation's +menswear +menswear's +mental +mentalities +mentality +mentality's +mentally +menthol +menthol's +mentholated +mention +mention's +mentioned +mentioning +mentions +mentor +mentor's +mentored +mentoring +mentors +menu +menu's +menus +meow +meow's +meowed +meowing +meows +mercantile +mercenaries +mercenary +mercenary's +mercerize +mercerized +mercerizes +mercerizing +merchandise +merchandise's +merchandised +merchandises +merchandising +merchandize +merchandized +merchandizes +merchandizing +merchant +merchant's +merchantman +merchantman's +merchantmen +merchants +mercies +merciful +mercifully +merciless +mercilessly +mercurial +mercuric +mercury +mercury's +mercy +mercy's +mere +mere's +merely +meres +merest +meretricious +merganser +merganser's +mergansers +merge +merged +merger +merger's +mergers +merges +merging +meridian +meridian's +meridians +meringue +meringue's +meringues +merino +merino's +merinos +merit +merit's +merited +meriting +meritocracies +meritocracy +meritocracy's +meritorious +meritoriously +merits +mermaid +mermaid's +mermaids +merman +merman's +mermen +merrier +merriest +merrily +merriment +merriment's +merriness +merriness's +merry +merrymaker +merrymaker's +merrymakers +merrymaking +merrymaking's +mes +mesa +mesa's +mesas +mescal +mescal's +mescaline +mescaline's +mescals +mesdames +mesdemoiselles +mesh +mesh's +meshed +meshes +meshing +mesmerism +mesmerism's +mesmerize +mesmerized +mesmerizes +mesmerizing +mesquite +mesquite's +mesquites +mess +mess's +message +message's +messages +messed +messenger +messenger's +messengers +messes +messiah +messiah's +messiahs +messier +messiest +messieurs +messily +messiness +messiness's +messing +messy +mestizo +mestizo's +mestizoes +mestizos +met +metabolic +metabolism +metabolism's +metabolisms +metabolize +metabolized +metabolizes +metabolizing +metacarpal +metacarpal's +metacarpals +metacarpi +metacarpus +metacarpus's +metal +metal's +metallic +metallurgical +metallurgist +metallurgist's +metallurgists +metallurgy +metallurgy's +metals +metamorphic +metamorphism +metamorphism's +metamorphose +metamorphosed +metamorphoses +metamorphosing +metamorphosis +metamorphosis's +metaphor +metaphor's +metaphorical +metaphorically +metaphors +metaphysical +metaphysics +metaphysics's +metastases +metastasis +metastasis's +metastasize +metastasized +metastasizes +metastasizing +metatarsal +metatarsal's +metatarsals +mete +mete's +meted +meteor +meteor's +meteoric +meteorite +meteorite's +meteorites +meteoroid +meteoroid's +meteoroids +meteorological +meteorologist +meteorologist's +meteorologists +meteorology +meteorology's +meteors +meter +meter's +metered +metering +meters +metes +methadon +methadon's +methadone +methadone's +methane +methane's +methanol +methanol's +methinks +method +method's +methodical +methodically +methodological +methodologies +methodology +methodology's +methods +methought +meticulous +meticulously +meticulousness +meticulousness's +meting +metric +metrical +metrically +metrication +metrication's +metrics +metro +metro's +metronome +metronome's +metronomes +metropolis +metropolis's +metropolises +metropolitan +metros +mettle +mettle's +mettlesome +mew +mew's +mewed +mewing +mewl +mewled +mewling +mewls +mews +mews's +mezzanine +mezzanine's +mezzanines +mi +mi's +miaow +miaow's +miaowed +miaowing +miaows +miasma +miasma's +miasmas +miasmata +mica +mica's +mice +micra +microaggression +microaggression's +microaggressions +microbe +microbe's +microbes +microbiologist +microbiologist's +microbiologists +microbiology +microbiology's +microchip +microchip's +microchips +microcode +microcomputer +microcomputer's +microcomputers +microcosm +microcosm's +microcosms +microeconomics +microeconomics's +microfiche +microfiche's +microfiches +microfilm +microfilm's +microfilmed +microfilming +microfilms +microloan +microloan's +microloans +micrometer +micrometer's +micrometers +micron +micron's +microns +microorganism +microorganism's +microorganisms +microphone +microphone's +microphones +microprocessor +microprocessor's +microprocessors +microscope +microscope's +microscopes +microscopic +microscopically +microscopy +microscopy's +microsecond +microsecond's +microseconds +microsurgery +microsurgery's +microwave +microwave's +microwaved +microwaves +microwaving +mid +midair +midair's +midday +midday's +middies +middle +middle's +middlebrow +middlebrow's +middlebrows +middleman +middleman's +middlemen +middles +middleweight +middleweight's +middleweights +middling +middy +middy's +midge +midge's +midges +midget +midget's +midgets +midland +midland's +midlands +midmost +midnight +midnight's +midpoint +midpoint's +midpoints +midriff +midriff's +midriffs +midshipman +midshipman's +midshipmen +midst +midst's +midstream +midstream's +midsummer +midsummer's +midterm +midterm's +midterms +midtown +midtown's +midway +midway's +midways +midweek +midweek's +midweeks +midwife +midwife's +midwifed +midwiferies +midwifery +midwifery's +midwifes +midwifing +midwinter +midwinter's +midwived +midwives +midwiving +midyear +midyear's +midyears +mien +mien's +miens +miff +miffed +miffing +miffs +might +might's +mightier +mightiest +mightily +mightiness +mightiness's +mighty +migraine +migraine's +migraines +migrant +migrant's +migrants +migrate +migrated +migrates +migrating +migration +migration's +migrations +migratory +mike +mike's +miked +mikes +miking +mil +mil's +milch +mild +mild's +milder +mildest +mildew +mildew's +mildewed +mildewing +mildews +mildly +mildness +mildness's +mile +mile's +mileage +mileage's +mileages +milepost +milepost's +mileposts +miler +miler's +milers +miles +milestone +milestone's +milestones +milf +milf's +milfs +milieu +milieu's +milieus +milieux +militancy +militancy's +militant +militant's +militantly +militants +militaries +militarily +militarism +militarism's +militarist +militarist's +militaristic +militarists +militarization +militarization's +militarize +militarized +militarizes +militarizing +military +military's +militate +militated +militates +militating +militia +militia's +militiaman +militiaman's +militiamen +militias +milk +milk's +milked +milker +milkier +milkiest +milkiness +milkiness's +milking +milkmaid +milkmaid's +milkmaids +milkman +milkman's +milkmen +milks +milkshake +milkshake's +milkshakes +milksop +milksop's +milksops +milkweed +milkweed's +milkweeds +milky +mill +mill's +millage +millage's +milled +millennia +millennial +millennial's +millennium +millennium's +millenniums +millepede +millepede's +millepedes +miller +miller's +millers +millet +millet's +milligram +milligram's +milligrams +milliliter +milliliter's +milliliters +millimeter +millimeter's +millimeters +milliner +milliner's +milliners +millinery +millinery's +milling +million +million's +millionaire +millionaire's +millionaires +millions +millionth +millionth's +millionths +millipede +millipede's +millipedes +millisecond +millisecond's +milliseconds +millrace +millrace's +millraces +mills +millstone +millstone's +millstones +milquetoast +milquetoast's +milquetoasts +mils +mime +mime's +mimed +mimeograph +mimeograph's +mimeographed +mimeographing +mimeographs +mimes +mimetic +mimic +mimic's +mimicked +mimicking +mimicries +mimicry +mimicry's +mimics +miming +mimosa +mimosa's +mimosas +minaret +minaret's +minarets +minatory +mince +mince's +minced +mincemeat +mincemeat's +minces +mincing +mind +mind's +mindbogglingly +minded +mindedness +mindful +mindfully +mindfulness +mindfulness's +minding +mindless +mindlessly +mindlessness +mindlessness's +minds +mine +mine's +mined +minefield +minefield's +minefields +miner +miner's +mineral +mineral's +mineralogist +mineralogist's +mineralogists +mineralogy +mineralogy's +minerals +miners +mines +minestrone +minestrone's +minesweeper +minesweeper's +minesweepers +mingle +mingled +mingles +mingling +mini +mini's +miniature +miniature's +miniatures +miniaturist +miniaturist's +miniaturists +miniaturization +miniaturization's +miniaturize +miniaturized +miniaturizes +miniaturizing +minibike +minibike's +minibikes +minibus +minibus's +minibuses +minibusses +minicam +minicam's +minicams +minicomputer +minicomputer's +minicomputers +minim +minim's +minima +minimal +minimalism +minimalism's +minimalist +minimalist's +minimalists +minimally +minimization +minimize +minimized +minimizes +minimizing +minims +minimum +minimum's +minimums +mining +mining's +minion +minion's +minions +minis +miniscule +miniscule's +miniscules +miniseries +miniseries's +miniskirt +miniskirt's +miniskirts +minister +minister's +ministered +ministerial +ministering +ministers +ministrant +ministrant's +ministrants +ministration +ministration's +ministrations +ministries +ministry +ministry's +minivan +minivan's +minivans +mink +mink's +minks +minnow +minnow's +minnows +minor +minor's +minored +minoring +minorities +minority +minority's +minors +minster +minstrel +minstrel's +minstrels +mint +mint's +minted +mintier +mintiest +minting +mints +minty +minuend +minuend's +minuends +minuet +minuet's +minuets +minus +minus's +minuscule +minuscule's +minuscules +minuses +minute +minute's +minuted +minutely +minuteman +minuteman's +minutemen +minuteness +minuteness's +minuter +minutes +minutest +minutia +minutia's +minutiae +minuting +minx +minx's +minxes +miracle +miracle's +miracles +miraculous +miraculously +mirage +mirage's +mirages +mire +mire's +mired +mires +miring +mirror +mirror's +mirrored +mirroring +mirrors +mirth +mirth's +mirthful +mirthfully +mirthless +misadventure +misadventure's +misadventures +misalignment +misalliance +misalliance's +misalliances +misanthrope +misanthrope's +misanthropes +misanthropic +misanthropist +misanthropist's +misanthropists +misanthropy +misanthropy's +misapplication +misapplication's +misapplied +misapplies +misapply +misapplying +misapprehend +misapprehended +misapprehending +misapprehends +misapprehension +misapprehension's +misapprehensions +misappropriate +misappropriated +misappropriates +misappropriating +misappropriation +misappropriation's +misappropriations +misbegotten +misbehave +misbehaved +misbehaves +misbehaving +misbehavior +misbehavior's +miscalculate +miscalculated +miscalculates +miscalculating +miscalculation +miscalculation's +miscalculations +miscall +miscalled +miscalling +miscalls +miscarriage +miscarriage's +miscarriages +miscarried +miscarries +miscarry +miscarrying +miscast +miscasting +miscasts +miscegenation +miscegenation's +miscellaneous +miscellanies +miscellany +miscellany's +mischance +mischance's +mischances +mischief +mischief's +mischievous +mischievously +mischievousness +mischievousness's +miscommunication +misconceive +misconceived +misconceives +misconceiving +misconception +misconception's +misconceptions +misconduct +misconduct's +misconducted +misconducting +misconducts +misconstruction +misconstruction's +misconstructions +misconstrue +misconstrued +misconstrues +misconstruing +miscount +miscount's +miscounted +miscounting +miscounts +miscreant +miscreant's +miscreants +miscue +miscue's +miscued +miscues +miscuing +misdeal +misdeal's +misdealing +misdeals +misdealt +misdeed +misdeed's +misdeeds +misdemeanor +misdemeanor's +misdemeanors +misdiagnose +misdiagnosed +misdiagnoses +misdiagnosing +misdiagnosis +misdiagnosis's +misdid +misdirect +misdirected +misdirecting +misdirection +misdirection's +misdirects +misdo +misdoes +misdoing +misdoing's +misdoings +misdone +miser +miser's +miserable +miserably +miseries +miserliness +miserliness's +miserly +misers +misery +misery's +misfeasance +misfeasance's +misfire +misfire's +misfired +misfires +misfiring +misfit +misfit's +misfits +misfitted +misfitting +misfortune +misfortune's +misfortunes +misgiving +misgiving's +misgivings +misgovern +misgoverned +misgoverning +misgoverns +misguide +misguided +misguidedly +misguides +misguiding +mishandle +mishandled +mishandles +mishandling +mishap +mishap's +mishaps +mishmash +mishmash's +mishmashes +misidentified +misidentifies +misidentify +misidentifying +misinform +misinformation +misinformation's +misinformed +misinforming +misinforms +misinterpret +misinterpretation +misinterpretation's +misinterpretations +misinterpreted +misinterpreting +misinterprets +misjudge +misjudged +misjudgement +misjudgement's +misjudgements +misjudges +misjudging +misjudgment +misjudgment's +misjudgments +mislaid +mislay +mislaying +mislays +mislead +misleading +misleads +misled +mismanage +mismanaged +mismanagement +mismanagement's +mismanages +mismanaging +mismatch +mismatch's +mismatched +mismatches +mismatching +misnomer +misnomer's +misnomers +misogynist +misogynist's +misogynistic +misogynists +misogyny +misogyny's +misplace +misplaced +misplaces +misplacing +misplay +misplay's +misplayed +misplaying +misplays +misprint +misprint's +misprinted +misprinting +misprints +mispronounce +mispronounced +mispronounces +mispronouncing +mispronunciation +mispronunciation's +mispronunciations +misquotation +misquotation's +misquotations +misquote +misquote's +misquoted +misquotes +misquoting +misread +misreading +misreading's +misreadings +misreads +misrepresent +misrepresentation +misrepresentation's +misrepresentations +misrepresented +misrepresenting +misrepresents +misrule +misrule's +misruled +misrules +misruling +miss +miss's +missal +missal's +missals +missed +misses +misshapen +missile +missile's +missilery +missilery's +missiles +missing +mission +mission's +missionaries +missionary +missionary's +missions +missive +missive's +missives +misspell +misspelled +misspelling +misspelling's +misspellings +misspells +misspelt +misspend +misspending +misspends +misspent +misstate +misstated +misstatement +misstatement's +misstatements +misstates +misstating +misstep +misstep's +missteps +mist +mist's +mistake +mistake's +mistaken +mistakenly +mistakes +mistaking +misted +mister +mister's +misters +mistier +mistiest +mistily +mistime +mistimed +mistimes +mistiming +mistiness +mistiness's +misting +mistletoe +mistletoe's +mistook +mistranslated +mistreat +mistreated +mistreating +mistreatment +mistreatment's +mistreats +mistress +mistress's +mistresses +mistrial +mistrial's +mistrials +mistrust +mistrust's +mistrusted +mistrustful +mistrusting +mistrusts +mists +misty +mistype +mistypes +mistyping +misunderstand +misunderstanding +misunderstanding's +misunderstandings +misunderstands +misunderstood +misuse +misuse's +misused +misuses +misusing +mite +mite's +miter +miter's +mitered +mitering +miters +mites +mitigate +mitigated +mitigates +mitigating +mitigation +mitigation's +mitosis +mitosis's +mitt +mitt's +mitten +mitten's +mittens +mitts +mix +mix's +mixed +mixer +mixer's +mixers +mixes +mixing +mixture +mixture's +mixtures +mizzen +mizzen's +mizzenmast +mizzenmast's +mizzenmasts +mizzens +mkay +mnemonic +mnemonic's +mnemonics +moan +moan's +moaned +moaning +moans +moat +moat's +moats +mob +mob's +mobbed +mobbing +mobile +mobile's +mobiles +mobility +mobility's +mobilization +mobilization's +mobilizations +mobilize +mobilized +mobilizes +mobilizing +mobs +mobster +mobster's +mobsters +moccasin +moccasin's +moccasins +mocha +mocha's +mochas +mock +mocked +mocker +mocker's +mockeries +mockers +mockery +mockery's +mocking +mockingbird +mockingbird's +mockingbirds +mockingly +mocks +mod +mod's +modal +modal's +modals +mode +mode's +model +model's +modeled +modeling +modeling's +modelings +modelled +modelling +models +modem +modem's +modems +moderate +moderate's +moderated +moderately +moderates +moderating +moderation +moderation's +moderator +moderator's +moderators +modern +modern's +modernism +modernism's +modernist +modernist's +modernistic +modernists +modernity +modernity's +modernization +modernization's +modernize +modernized +modernizes +modernizing +moderns +modes +modest +modestly +modesty +modesty's +modicum +modicum's +modicums +modifiable +modification +modification's +modifications +modified +modifier +modifier's +modifiers +modifies +modify +modifying +modish +modishly +modishness +modishness's +mods +modular +modulate +modulated +modulates +modulating +modulation +modulation's +modulations +modulator +modulator's +modulators +module +module's +modules +modulus +mogul +mogul's +moguls +mohair +mohair's +moieties +moiety +moiety's +moire +moire's +moires +moist +moisten +moistened +moistening +moistens +moister +moistest +moistly +moistness +moistness's +moisture +moisture's +moisturize +moisturized +moisturizer +moisturizer's +moisturizers +moisturizes +moisturizing +molar +molar's +molars +molasses +molasses's +mold +mold's +molded +molder +molder's +moldered +moldering +molders +moldier +moldiest +moldiness +moldiness's +molding +molding's +moldings +molds +moldy +mole +mole's +molecular +molecule +molecule's +molecules +molehill +molehill's +molehills +moles +moleskin +moleskin's +molest +molestation +molestation's +molested +molester +molester's +molesters +molesting +molests +moll +moll's +mollification +mollification's +mollified +mollifies +mollify +mollifying +molls +mollusc +mollusc's +molluscs +mollusk +mollusk's +mollusks +mollycoddle +mollycoddle's +mollycoddled +mollycoddles +mollycoddling +molt +molt's +molted +molten +molting +molts +molybdenum +molybdenum's +mom +mom's +moment +moment's +momentarily +momentary +momentous +momentousness +momentousness's +moments +momentum +momentum's +momma +momma's +mommas +mommies +mommy +mommy's +moms +monarch +monarch's +monarchic +monarchical +monarchies +monarchism +monarchism's +monarchist +monarchist's +monarchists +monarchs +monarchy +monarchy's +monasteries +monastery +monastery's +monastic +monastic's +monasticism +monasticism's +monastics +monaural +monetarily +monetarism +monetary +monetize +monetized +monetizes +monetizing +money +money's +moneybag +moneybag's +moneybags +moneyed +moneymaker +moneymaker's +moneymakers +moneymaking +moneymaking's +mongeese +monger +monger's +mongered +mongering +mongers +mongolism +mongolism's +mongoose +mongoose's +mongooses +mongrel +mongrel's +mongrels +monicker +monicker's +monickers +monied +monies +moniker +moniker's +monikers +monitor +monitor's +monitored +monitoring +monitors +monk +monk's +monkey +monkey's +monkeyed +monkeying +monkeys +monkeyshine +monkeyshine's +monkeyshines +monks +mono +mono's +monochromatic +monochrome +monochrome's +monochromes +monocle +monocle's +monocles +monocotyledon +monocotyledon's +monocotyledons +monogamous +monogamy +monogamy's +monogram +monogram's +monogrammed +monogramming +monograms +monograph +monograph's +monographs +monolingual +monolingual's +monolinguals +monolith +monolith's +monolithic +monoliths +monolog +monolog's +monologs +monologue +monologue's +monologues +monomania +monomania's +monomaniac +monomaniac's +monomaniacs +mononucleosis +mononucleosis's +monophonic +monopolies +monopolist +monopolist's +monopolistic +monopolists +monopolization +monopolization's +monopolize +monopolized +monopolizes +monopolizing +monopoly +monopoly's +monorail +monorail's +monorails +monosyllabic +monosyllable +monosyllable's +monosyllables +monotheism +monotheism's +monotheist +monotheist's +monotheistic +monotheists +monotone +monotone's +monotones +monotonic +monotonically +monotonous +monotonously +monotony +monotony's +monoxide +monoxide's +monoxides +monsieur +monsieur's +monsignor +monsignor's +monsignori +monsignors +monsoon +monsoon's +monsoons +monster +monster's +monsters +monstrance +monstrance's +monstrances +monstrosities +monstrosity +monstrosity's +monstrous +monstrously +montage +montage's +montages +month +month's +monthlies +monthly +monthly's +months +monument +monument's +monumental +monumentally +monuments +moo +moo's +mooch +mooch's +mooched +moocher +moocher's +moochers +mooches +mooching +mood +mood's +moodier +moodiest +moodily +moodiness +moodiness's +moods +moody +mooed +mooing +moon +moon's +moonbeam +moonbeam's +moonbeams +mooned +mooning +moonlight +moonlight's +moonlighted +moonlighter +moonlighter's +moonlighters +moonlighting +moonlighting's +moonlights +moonlit +moons +moonscape +moonscape's +moonscapes +moonshine +moonshine's +moonshines +moonshot +moonshot's +moonshots +moonstone +moonstone's +moonstones +moonstruck +moor +moor's +moored +mooring +mooring's +moorings +moorland +moors +moos +moose +moose's +moot +mooted +mooting +moots +mop +mop's +mope +mope's +moped +moped's +mopeds +mopes +moping +mopped +moppet +moppet's +moppets +mopping +mops +moraine +moraine's +moraines +moral +moral's +morale +morale's +moralist +moralist's +moralistic +moralists +moralities +morality +morality's +moralize +moralized +moralizes +moralizing +morally +morals +morass +morass's +morasses +moratoria +moratorium +moratorium's +moratoriums +moray +moray's +morays +morbid +morbidity +morbidity's +morbidly +mordant +mordant's +mordants +more +more's +moreover +mores +mores's +morgue +morgue's +morgues +moribund +morn +morn's +morning +morning's +mornings +morns +morocco +morocco's +moron +moron's +moronic +morons +morose +morosely +moroseness +moroseness's +morpheme +morpheme's +morphemes +morphine +morphine's +morphological +morphology +morphology's +morrow +morrow's +morrows +morsel +morsel's +morsels +mortal +mortal's +mortality +mortality's +mortally +mortals +mortar +mortar's +mortarboard +mortarboard's +mortarboards +mortared +mortaring +mortars +mortgage +mortgage's +mortgaged +mortgagee +mortgagee's +mortgagees +mortgager +mortgager's +mortgagers +mortgages +mortgaging +mortgagor +mortgagor's +mortgagors +mortice +mortice's +morticed +mortices +mortician +mortician's +morticians +morticing +mortification +mortification's +mortified +mortifies +mortify +mortifying +mortise +mortise's +mortised +mortises +mortising +mortuaries +mortuary +mortuary's +mosaic +mosaic's +mosaics +mosey +moseyed +moseying +moseys +mosque +mosque's +mosques +mosquito +mosquito's +mosquitoes +mosquitos +moss +moss's +mosses +mossier +mossiest +mossy +most +most's +mostly +mote +mote's +motel +motel's +motels +motes +moth +moth's +mothball +mothball's +mothballed +mothballing +mothballs +mother +mother's +motherboard +motherboard's +motherboards +mothered +motherfucker +motherfucker's +motherfuckers +motherfucking +motherhood +motherhood's +mothering +motherland +motherland's +motherlands +motherless +motherliness +motherliness's +motherly +mothers +moths +motif +motif's +motifs +motile +motiles +motility +motility's +motion +motion's +motioned +motioning +motionless +motions +motivate +motivated +motivates +motivating +motivation +motivation's +motivational +motivations +motivator +motivator's +motivators +motive +motive's +motives +motley +motley's +motleys +motlier +motliest +motocross +motocross's +motocrosses +motor +motor's +motorbike +motorbike's +motorbiked +motorbikes +motorbiking +motorboat +motorboat's +motorboats +motorcade +motorcade's +motorcades +motorcar +motorcar's +motorcars +motorcycle +motorcycle's +motorcycled +motorcycles +motorcycling +motorcyclist +motorcyclist's +motorcyclists +motored +motoring +motorist +motorist's +motorists +motorize +motorized +motorizes +motorizing +motorman +motorman's +motormen +motormouth +motormouth's +motormouths +motors +motorway +motorway's +motorways +mottle +mottled +mottles +mottling +motto +motto's +mottoes +mottos +mound +mound's +mounded +mounding +mounds +mount +mount's +mountain +mountain's +mountaineer +mountaineer's +mountaineered +mountaineering +mountaineering's +mountaineers +mountainous +mountains +mountainside +mountainside's +mountainsides +mountaintop +mountaintop's +mountaintops +mountebank +mountebank's +mountebanks +mounted +mounting +mounting's +mountings +mounts +mourn +mourned +mourner +mourner's +mourners +mournful +mournfully +mournfulness +mournfulness's +mourning +mourning's +mourns +mouse +mouse's +moused +mouser +mouser's +mousers +mouses +mousetrap +mousetrap's +mousetrapped +mousetrapping +mousetraps +mousey +mousier +mousiest +mousiness +mousiness's +mousing +mousse +mousse's +moussed +mousses +moussing +moustache +moustache's +moustaches +mousy +mouth +mouth's +mouthed +mouthful +mouthful's +mouthfuls +mouthing +mouthpiece +mouthpiece's +mouthpieces +mouths +mouthwash +mouthwash's +mouthwashes +mouthwatering +movable +movable's +movables +move +move's +moveable +moveable's +moveables +moved +movement +movement's +movements +mover +mover's +movers +moves +movie +movie's +movies +moving +movingly +mow +mow's +mowed +mower +mower's +mowers +mowing +mown +mows +mozzarella +mozzarella's +ms +mu +much +much's +mucilage +mucilage's +muck +muck's +mucked +muckier +muckiest +mucking +muckrake +muckraked +muckraker +muckraker's +muckrakers +muckrakes +muckraking +mucks +mucky +mucous +mucus +mucus's +mud +mud's +muddied +muddier +muddies +muddiest +muddiness +muddiness's +muddle +muddle's +muddled +muddles +muddling +muddy +muddying +mudguard +mudguard's +mudguards +mudslide +mudslide's +mudslides +mudslinger +mudslinger's +mudslingers +mudslinging +mudslinging's +muesli +muezzin +muezzin's +muezzins +muff +muff's +muffed +muffin +muffin's +muffing +muffins +muffle +muffled +muffler +muffler's +mufflers +muffles +muffling +muffs +mufti +mufti's +muftis +mug +mug's +mugged +mugger +mugger's +muggers +muggier +muggiest +mugginess +mugginess's +mugging +mugging's +muggings +muggle +muggle's +muggles +muggy +mugs +mukluk +mukluk's +mukluks +mulatto +mulatto's +mulattoes +mulattos +mulberries +mulberry +mulberry's +mulch +mulch's +mulched +mulches +mulching +mule +mule's +mules +muleteer +muleteer's +muleteers +mulish +mulishly +mulishness +mulishness's +mull +mullah +mullah's +mullahs +mulled +mullet +mullet's +mullets +mulligatawny +mulligatawny's +mulling +mullion +mullion's +mullions +mulls +multi +multicolored +multicultural +multiculturalism +multiculturalism's +multidimensional +multifaceted +multifarious +multifariousness +multifariousness's +multilateral +multilingual +multimedia +multimedia's +multimillionaire +multimillionaire's +multimillionaires +multinational +multinational's +multinationals +multiplayer +multiplayer's +multiple +multiple's +multiples +multiplex +multiplex's +multiplexed +multiplexer +multiplexer's +multiplexers +multiplexes +multiplexing +multiplexor +multiplexor's +multiplexors +multiplicand +multiplicand's +multiplicands +multiplication +multiplication's +multiplications +multiplicative +multiplicities +multiplicity +multiplicity's +multiplied +multiplier +multiplier's +multipliers +multiplies +multiply +multiplying +multiprocessing +multipurpose +multiracial +multitasking +multitude +multitude's +multitudes +multitudinous +multivariate +multiverse +multiverse's +multiverses +multivitamin +multivitamin's +multivitamins +mum +mumble +mumble's +mumbled +mumbler +mumbler's +mumblers +mumbles +mumbling +mummer +mummer's +mummers +mummery +mummery's +mummies +mummification +mummification's +mummified +mummifies +mummify +mummifying +mummy +mummy's +mumps +mumps's +munch +munched +munches +munchies +munchies's +munching +mundane +mundanely +municipal +municipal's +municipalities +municipality +municipality's +municipally +municipals +munificence +munificence's +munificent +munition +munition's +munitions +mural +mural's +muralist +muralist's +muralists +murals +murder +murder's +murdered +murderer +murderer's +murderers +murderess +murderess's +murderesses +murdering +murderous +murderously +murders +murk +murk's +murkier +murkiest +murkily +murkiness +murkiness's +murks +murky +murmur +murmur's +murmured +murmuring +murmurs +muscat +muscatel +muscatel's +muscatels +muscle +muscle's +muscled +muscles +muscling +muscular +muscularity +muscularity's +musculature +musculature's +muse +muse's +mused +muses +museum +museum's +museums +mush +mush's +mushed +mushes +mushier +mushiest +mushiness +mushiness's +mushing +mushroom +mushroom's +mushroomed +mushrooming +mushrooms +mushy +music +music's +musical +musical's +musicale +musicale's +musicales +musically +musicals +musician +musician's +musicians +musicianship +musicianship's +musicologist +musicologist's +musicologists +musicology +musicology's +musing +musing's +musings +musk +musk's +muskellunge +muskellunge's +muskellunges +musket +musket's +musketeer +musketeer's +musketeers +musketry +musketry's +muskets +muskier +muskiest +muskiness +muskiness's +muskmelon +muskmelon's +muskmelons +muskrat +muskrat's +muskrats +musky +muslin +muslin's +muss +muss's +mussed +mussel +mussel's +mussels +musses +mussier +mussiest +mussing +mussy +must +must's +mustache +mustache's +mustaches +mustang +mustang's +mustangs +mustard +mustard's +muster +muster's +mustered +mustering +musters +mustier +mustiest +mustiness +mustiness's +mustn't +musts +musty +mutability +mutability's +mutable +mutant +mutant's +mutants +mutate +mutated +mutates +mutating +mutation +mutation's +mutations +mute +mute's +muted +mutely +muteness +muteness's +muter +mutes +mutest +mutilate +mutilated +mutilates +mutilating +mutilation +mutilation's +mutilations +mutineer +mutineer's +mutineers +muting +mutinied +mutinies +mutinous +mutinously +mutiny +mutiny's +mutinying +mutt +mutt's +mutter +mutter's +muttered +muttering +mutters +mutton +mutton's +mutts +mutual +mutuality +mutuality's +mutually +muumuu +muumuu's +muumuus +muzzle +muzzle's +muzzled +muzzles +muzzling +my +myna +myna's +mynah +mynah's +mynahes +mynahs +mynas +myopia +myopia's +myopic +myriad +myriad's +myriads +myrrh +myrrh's +myrtle +myrtle's +myrtles +myself +mysteries +mysterious +mysteriously +mysteriousness +mysteriousness's +mystery +mystery's +mystic +mystic's +mystical +mystically +mysticism +mysticism's +mystics +mystification +mystification's +mystified +mystifies +mystify +mystifying +mystique +mystique's +myth +myth's +mythic +mythical +mythological +mythologies +mythologist +mythologist's +mythologists +mythology +mythology's +myths +métier +métier's +métiers +mêlée +mêlée's +mêlées +n +nab +nabbed +nabbing +nabob +nabob's +nabobs +nabs +nacho +nacho's +nachos +nacre +nacre's +nadir +nadir's +nadirs +nag +nag's +nagged +nagging +nags +naiad +naiad's +naiades +naiads +nail +nail's +nailbrush +nailbrush's +nailbrushes +nailed +nailing +nails +naive +naively +naiver +naivest +naivety +naiveté +naiveté's +naked +nakedly +nakedness +nakedness's +name +name's +named +nameless +namely +names +namesake +namesake's +namesakes +naming +nannies +nanny +nanny's +nanosecond +nanosecond's +nanoseconds +nanotechnology +nanotechnology's +nap +nap's +napalm +napalm's +napalmed +napalming +napalms +nape +nape's +napes +naphtha +naphtha's +naphthalene +naphthalene's +napkin +napkin's +napkins +napped +nappier +nappies +nappiest +napping +nappy +nappy's +naps +narc +narc's +narcissi +narcissism +narcissism's +narcissist +narcissist's +narcissistic +narcissists +narcissus +narcissus's +narcissuses +narcosis +narcosis's +narcotic +narcotic's +narcotics +narcs +nark +nark's +narked +narking +narks +narrate +narrated +narrates +narrating +narration +narration's +narrations +narrative +narrative's +narratives +narrator +narrator's +narrators +narrow +narrow's +narrowed +narrower +narrowest +narrowing +narrowly +narrowness +narrowness's +narrows +narwhal +narwhal's +narwhals +nary +nasal +nasal's +nasalize +nasalized +nasalizes +nasalizing +nasally +nasals +nascent +nastier +nastiest +nastily +nastiness +nastiness's +nasturtium +nasturtium's +nasturtiums +nasty +natal +nation +nation's +national +national's +nationalism +nationalism's +nationalist +nationalist's +nationalistic +nationalists +nationalities +nationality +nationality's +nationalization +nationalization's +nationalizations +nationalize +nationalized +nationalizes +nationalizing +nationally +nationals +nations +nationwide +native +native's +natives +nativities +nativity +nativity's +nattier +nattiest +nattily +natty +natural +natural's +naturalism +naturalism's +naturalist +naturalist's +naturalistic +naturalists +naturalization +naturalization's +naturalize +naturalized +naturalizes +naturalizing +naturally +naturalness +naturalness's +naturals +nature +nature's +natures +naught +naught's +naughtier +naughtiest +naughtily +naughtiness +naughtiness's +naughts +naughty +nausea +nausea's +nauseate +nauseated +nauseates +nauseating +nauseatingly +nauseous +nautical +nautically +nautili +nautilus +nautilus's +nautiluses +naval +nave +nave's +navel +navel's +navels +naves +navies +navigability +navigability's +navigable +navigate +navigated +navigates +navigating +navigation +navigation's +navigational +navigator +navigator's +navigators +navy +navy's +nay +nay's +nays +naysayer +naysayer's +naysayers +ne'er +near +nearby +neared +nearer +nearest +nearing +nearly +nearness +nearness's +nears +nearsighted +nearsightedness +nearsightedness's +neat +neater +neatest +neath +neatly +neatness +neatness's +nebula +nebula's +nebulae +nebular +nebulas +nebulous +necessaries +necessarily +necessary +necessary's +necessitate +necessitated +necessitates +necessitating +necessities +necessity +necessity's +neck +neck's +necked +neckerchief +neckerchief's +neckerchiefs +neckerchieves +necking +necklace +necklace's +necklaces +neckline +neckline's +necklines +necks +necktie +necktie's +neckties +necromancer +necromancer's +necromancers +necromancy +necromancy's +necrophilia +necrosis +necrosis's +nectar +nectar's +nectarine +nectarine's +nectarines +need +need's +needed +needful +needier +neediest +neediness +neediness's +needing +needle +needle's +needled +needlepoint +needlepoint's +needles +needless +needlessly +needlework +needlework's +needling +needn't +needs +needy +nefarious +nefariously +nefariousness +nefariousness's +negate +negated +negates +negating +negation +negation's +negations +negative +negative's +negatived +negatively +negatives +negativing +negativity +negativity's +neglect +neglect's +neglected +neglectful +neglectfully +neglecting +neglects +neglig +neglig's +negligee +negligee's +negligees +negligence +negligence's +negligent +negligently +negligible +negligibly +negligs +negotiable +negotiate +negotiated +negotiates +negotiating +negotiation +negotiation's +negotiations +negotiator +negotiator's +negotiators +neigh +neigh's +neighbor +neighbor's +neighbored +neighborhood +neighborhood's +neighborhoods +neighboring +neighborliness +neighborliness's +neighborly +neighbors +neighed +neighing +neighs +neither +nematode +nematode's +nematodes +nemeses +nemesis +nemesis's +neoclassic +neoclassical +neoclassicism +neoclassicism's +neocolonialism +neocolonialism's +neocon +neocon's +neocons +neoconservative +neoconservative's +neoconservatives +neodymium +neodymium's +neologism +neologism's +neologisms +neon +neon's +neonatal +neonate +neonate's +neonates +neophyte +neophyte's +neophytes +neoprene +neoprene's +nephew +nephew's +nephews +nephritis +nephritis's +nepotism +nepotism's +neptunium +neptunium's +nerd +nerd's +nerdier +nerdiest +nerds +nerdy +nerve +nerve's +nerved +nerveless +nervelessly +nerves +nervier +nerviest +nerving +nervous +nervously +nervousness +nervousness's +nervy +nest +nest's +nested +nesting +nestle +nestled +nestles +nestling +nestling's +nestlings +nests +net +net's +netbook +netbook's +netbooks +nether +nethermost +nets +netted +netting +netting's +nettle +nettle's +nettled +nettles +nettlesome +nettling +network +network's +networked +networking +networking's +networks +neural +neuralgia +neuralgia's +neuralgic +neuritis +neuritis's +neurological +neurologist +neurologist's +neurologists +neurology +neurology's +neuron +neuron's +neurons +neuroses +neurosis +neurosis's +neurosurgery +neurosurgery's +neurotic +neurotic's +neurotically +neurotics +neurotransmitter +neurotransmitter's +neurotransmitters +neuter +neuter's +neutered +neutering +neuters +neutral +neutral's +neutrality +neutrality's +neutralization +neutralization's +neutralize +neutralized +neutralizer +neutralizer's +neutralizers +neutralizes +neutralizing +neutrally +neutrals +neutrino +neutrino's +neutrinos +neutron +neutron's +neutrons +never +nevermore +nevertheless +new +new's +newbie +newbie's +newbies +newborn +newborn's +newborns +newcomer +newcomer's +newcomers +newel +newel's +newels +newer +newest +newfangled +newly +newlywed +newlywed's +newlyweds +newness +newness's +news +news's +newsagents +newsboy +newsboy's +newsboys +newscast +newscast's +newscaster +newscaster's +newscasters +newscasts +newsflash +newsier +newsiest +newsletter +newsletter's +newsletters +newsman +newsman's +newsmen +newspaper +newspaper's +newspaperman +newspaperman's +newspapermen +newspapers +newspaperwoman +newspaperwoman's +newspaperwomen +newsprint +newsprint's +newsreel +newsreel's +newsreels +newsstand +newsstand's +newsstands +newsworthy +newsy +newt +newt's +newton +newton's +newtons +newts +next +next's +nexus +nexus's +nexuses +niacin +niacin's +nib +nib's +nibble +nibble's +nibbled +nibbler +nibbler's +nibblers +nibbles +nibbling +nibs +nice +nicely +niceness +niceness's +nicer +nicest +niceties +nicety +nicety's +niche +niche's +niches +nick +nick's +nicked +nickel +nickel's +nickelodeon +nickelodeon's +nickelodeons +nickels +nicking +nicknack +nicknack's +nicknacks +nickname +nickname's +nicknamed +nicknames +nicknaming +nicks +nicotine +nicotine's +niece +niece's +nieces +niftier +niftiest +nifty +nigga +nigga's +niggard +niggard's +niggardliness +niggardliness's +niggardly +niggards +niggas +niggaz +nigger +nigger's +niggers +niggle +niggle's +niggled +niggles +niggling +nigh +nigher +nighest +night +night's +nightcap +nightcap's +nightcaps +nightclothes +nightclothes's +nightclub +nightclub's +nightclubbed +nightclubbing +nightclubs +nightfall +nightfall's +nightgown +nightgown's +nightgowns +nighthawk +nighthawk's +nighthawks +nightie +nightie's +nighties +nightingale +nightingale's +nightingales +nightlife +nightlife's +nightly +nightmare +nightmare's +nightmares +nightmarish +nights +nightshade +nightshade's +nightshades +nightshirt +nightshirt's +nightshirts +nightstick +nightstick's +nightsticks +nighttime +nighttime's +nighty +nighty's +nihilism +nihilism's +nihilist +nihilist's +nihilistic +nihilists +nil +nil's +nimbi +nimble +nimbleness +nimbleness's +nimbler +nimblest +nimbly +nimbus +nimbus's +nimbuses +nincompoop +nincompoop's +nincompoops +nine +nine's +ninepin +ninepin's +ninepins +ninepins's +nines +nineteen +nineteen's +nineteens +nineteenth +nineteenth's +nineteenths +nineties +ninetieth +ninetieth's +ninetieths +ninety +ninety's +ninja +ninja's +ninjas +ninnies +ninny +ninny's +ninth +ninth's +ninths +nip +nip's +nipped +nipper +nipper's +nippers +nippier +nippiest +nipping +nipple +nipple's +nipples +nippy +nips +nirvana +nirvana's +nit +nit's +nite +nite's +niter +niter's +nites +nitpick +nitpicked +nitpicker +nitpicker's +nitpickers +nitpicking +nitpicks +nitrate +nitrate's +nitrated +nitrates +nitrating +nitrogen +nitrogen's +nitrogenous +nitroglycerin +nitroglycerin's +nitroglycerine +nitroglycerine's +nits +nitwit +nitwit's +nitwits +nix +nix's +nixed +nixes +nixing +no +no's +nobility +nobility's +noble +noble's +nobleman +nobleman's +noblemen +nobleness +nobleness's +nobler +nobles +noblest +noblewoman +noblewoman's +noblewomen +nobly +nobodies +nobody +nobody's +nocturnal +nocturnally +nocturne +nocturne's +nocturnes +nod +nod's +nodal +nodded +nodding +noddy +node +node's +nodes +nods +nodular +nodule +nodule's +nodules +noel +noel's +noels +noes +noggin +noggin's +noggins +noise +noise's +noised +noiseless +noiselessly +noiselessness +noiselessness's +noisemaker +noisemaker's +noisemakers +noises +noisier +noisiest +noisily +noisiness +noisiness's +noising +noisome +noisy +nomad +nomad's +nomadic +nomads +nomenclature +nomenclature's +nomenclatures +nominal +nominally +nominate +nominated +nominates +nominating +nomination +nomination's +nominations +nominative +nominative's +nominatives +nominee +nominee's +nominees +non +nonabrasive +nonabsorbent +nonabsorbent's +nonabsorbents +nonagenarian +nonagenarian's +nonagenarians +nonalcoholic +nonaligned +nonbeliever +nonbeliever's +nonbelievers +nonbreakable +nonce +nonce's +nonchalance +nonchalance's +nonchalant +nonchalantly +noncom +noncom's +noncombatant +noncombatant's +noncombatants +noncommercial +noncommercial's +noncommercials +noncommittal +noncommittally +noncompetitive +noncompliance +noncompliance's +noncoms +nonconductor +nonconductor's +nonconductors +nonconformist +nonconformist's +nonconformists +nonconformity +nonconformity's +noncontagious +noncooperation +noncooperation's +nondairy +nondeductible +nondeductible's +nondenominational +nondescript +nondrinker +nondrinker's +nondrinkers +none +nonempty +nonentities +nonentity +nonentity's +nonessential +nonesuch +nonesuch's +nonesuches +nonetheless +nonevent +nonevent's +nonevents +nonexempt +nonexempt's +nonexistence +nonexistence's +nonexistent +nonfat +nonfatal +nonfiction +nonfiction's +nonflammable +nongovernmental +nonhazardous +nonhuman +nonindustrial +noninterference +noninterference's +nonintervention +nonintervention's +nonjudgmental +nonliving +nonliving's +nonmalignant +nonmember +nonmember's +nonmembers +nonnegotiable +nonobjective +nonpareil +nonpareil's +nonpareils +nonpartisan +nonpartisan's +nonpartisans +nonpayment +nonpayment's +nonpayments +nonphysical +nonplus +nonplused +nonpluses +nonplusing +nonplussed +nonplusses +nonplussing +nonpoisonous +nonpolitical +nonpolluting +nonprescription +nonproductive +nonprofessional +nonprofessional's +nonprofessionals +nonprofit +nonprofit's +nonprofits +nonproliferation +nonproliferation's +nonrefillable +nonrefundable +nonrenewable +nonrepresentational +nonresident +nonresident's +nonresidents +nonrestrictive +nonreturnable +nonreturnable's +nonreturnables +nonrigid +nonscheduled +nonseasonal +nonsectarian +nonsense +nonsense's +nonsensical +nonsensically +nonsexist +nonskid +nonsmoker +nonsmoker's +nonsmokers +nonsmoking +nonstandard +nonstick +nonstop +nonsupport +nonsupport's +nontaxable +nontechnical +nontoxic +nontransferable +nontrivial +nonunion +nonuser +nonuser's +nonusers +nonverbal +nonviolence +nonviolence's +nonviolent +nonvoting +nonwhite +nonwhite's +nonwhites +nonzero +noodle +noodle's +noodled +noodles +noodling +nook +nook's +nooks +noon +noon's +noonday +noonday's +noontime +noontime's +noose +noose's +nooses +nope +nor +norm +norm's +normal +normal's +normalcy +normalcy's +normality +normality's +normalization +normalization's +normalize +normalized +normalizes +normalizing +normally +normative +norms +north +north's +northbound +northeast +northeast's +northeaster +northeaster's +northeasterly +northeastern +northeasters +northeastward +northerlies +northerly +northerly's +northern +northerner +northerner's +northerners +northernmost +northward +northwards +northwest +northwest's +northwesterly +northwestern +northwestward +nose +nose's +nosebleed +nosebleed's +nosebleeds +nosed +nosedive +nosedive's +nosedived +nosedives +nosediving +nosedove +nosegay +nosegay's +nosegays +noses +nosey +nosh +nosh's +noshed +noshes +noshing +nosier +nosiest +nosiness +nosiness's +nosing +nostalgia +nostalgia's +nostalgic +nostalgically +nostril +nostril's +nostrils +nostrum +nostrum's +nostrums +nosy +not +notable +notable's +notables +notably +notaries +notarize +notarized +notarizes +notarizing +notary +notary's +notation +notation's +notations +notch +notch's +notched +notches +notching +note +note's +notebook +notebook's +notebooks +noted +notepad +notepaper +notes +noteworthy +nothing +nothing's +nothingness +nothingness's +nothings +notice +notice's +noticeable +noticeably +noticeboard +noticeboards +noticed +notices +noticing +notification +notification's +notifications +notified +notifies +notify +notifying +noting +notion +notion's +notional +notionally +notions +notoriety +notoriety's +notorious +notoriously +notwithstanding +nougat +nougat's +nougats +nought +nought's +noughts +noun +noun's +nouns +nourish +nourished +nourishes +nourishing +nourishment +nourishment's +nous +nova +nova's +novae +novas +novel +novel's +novelette +novelette's +novelettes +novelist +novelist's +novelists +novella +novella's +novellas +novelle +novels +novelties +novelty +novelty's +novice +novice's +novices +novitiate +novitiate's +novitiates +now +now's +nowadays +nowadays's +noway +nowhere +nowhere's +nowise +noxious +nozzle +nozzle's +nozzles +nth +nu +nuance +nuance's +nuanced +nuances +nub +nub's +nubile +nubs +nuclear +nuclei +nucleic +nucleus +nucleus's +nucleuses +nude +nude's +nuder +nudes +nudest +nudge +nudge's +nudged +nudges +nudging +nudism +nudism's +nudist +nudist's +nudists +nudity +nudity's +nugget +nugget's +nuggets +nuisance +nuisance's +nuisances +nuke +nuke's +nuked +nukes +nuking +null +nullification +nullification's +nullified +nullifies +nullify +nullifying +nullity +nullity's +nulls +numb +numbed +number +number's +numbered +numbering +numberless +numbers +numbest +numbing +numbly +numbness +numbness's +numbs +numbskull +numbskull's +numbskulls +numeracy +numeral +numeral's +numerals +numerate +numerated +numerates +numerating +numeration +numeration's +numerations +numerator +numerator's +numerators +numeric +numerical +numerically +numerology +numerology's +numerous +numismatic +numismatics +numismatics's +numismatist +numismatist's +numismatists +numskull +numskull's +numskulls +nun +nun's +nuncio +nuncio's +nuncios +nunneries +nunnery +nunnery's +nuns +nuptial +nuptial's +nuptials +nurse +nurse's +nursed +nursemaid +nursemaid's +nursemaids +nurseries +nursery +nursery's +nurseryman +nurseryman's +nurserymen +nurses +nursing +nursing's +nurture +nurture's +nurtured +nurtures +nurturing +nut +nut's +nutcracker +nutcracker's +nutcrackers +nuthatch +nuthatch's +nuthatches +nutmeat +nutmeat's +nutmeats +nutmeg +nutmeg's +nutmegs +nutria +nutria's +nutrias +nutrient +nutrient's +nutrients +nutriment +nutriment's +nutriments +nutrition +nutrition's +nutritional +nutritionally +nutritionist +nutritionist's +nutritionists +nutritious +nutritive +nuts +nutshell +nutshell's +nutshells +nutted +nuttier +nuttiest +nuttiness +nuttiness's +nutting +nutty +nuzzle +nuzzle's +nuzzled +nuzzles +nuzzling +nylon +nylon's +nylons +nylons's +nymph +nymph's +nymphomania +nymphomania's +nymphomaniac +nymphomaniac's +nymphomaniacs +nymphs +née +o +o'clock +o'er +oaf +oaf's +oafish +oafs +oak +oak's +oaken +oaks +oakum +oakum's +oar +oar's +oared +oaring +oarlock +oarlock's +oarlocks +oars +oarsman +oarsman's +oarsmen +oases +oasis +oasis's +oat +oat's +oaten +oath +oath's +oaths +oatmeal +oatmeal's +oats +oats's +obduracy +obduracy's +obdurate +obdurately +obedience +obedience's +obedient +obediently +obeisance +obeisance's +obeisances +obeisant +obelisk +obelisk's +obelisks +obese +obesity +obesity's +obey +obeyed +obeying +obeys +obfuscate +obfuscated +obfuscates +obfuscating +obfuscation +obfuscation's +obit +obit's +obits +obituaries +obituary +obituary's +object +object's +objected +objecting +objection +objection's +objectionable +objectionably +objections +objective +objective's +objectively +objectiveness +objectiveness's +objectives +objectivity +objectivity's +objector +objector's +objectors +objects +oblate +oblation +oblation's +oblations +obligate +obligated +obligates +obligating +obligation +obligation's +obligations +obligatory +oblige +obliged +obliges +obliging +obligingly +oblique +oblique's +obliquely +obliqueness +obliqueness's +obliques +obliterate +obliterated +obliterates +obliterating +obliteration +obliteration's +oblivion +oblivion's +oblivious +obliviously +obliviousness +obliviousness's +oblong +oblong's +oblongs +obloquy +obloquy's +obnoxious +obnoxiously +oboe +oboe's +oboes +oboist +oboist's +oboists +obscene +obscenely +obscener +obscenest +obscenities +obscenity +obscenity's +obscure +obscured +obscurely +obscurer +obscures +obscurest +obscuring +obscurities +obscurity +obscurity's +obsequies +obsequious +obsequiously +obsequiousness +obsequiousness's +obsequy +obsequy's +observable +observably +observance +observance's +observances +observant +observantly +observation +observation's +observational +observations +observatories +observatory +observatory's +observe +observed +observer +observer's +observers +observes +observing +obsess +obsessed +obsesses +obsessing +obsession +obsession's +obsessions +obsessive +obsessive's +obsessively +obsessives +obsidian +obsidian's +obsolescence +obsolescence's +obsolescent +obsolete +obsoleted +obsoletes +obsoleting +obstacle +obstacle's +obstacles +obstetric +obstetrical +obstetrician +obstetrician's +obstetricians +obstetrics +obstetrics's +obstinacy +obstinacy's +obstinate +obstinately +obstreperous +obstruct +obstructed +obstructing +obstruction +obstruction's +obstructionist +obstructionist's +obstructionists +obstructions +obstructive +obstructively +obstructiveness +obstructiveness's +obstructs +obtain +obtainable +obtained +obtaining +obtains +obtrude +obtruded +obtrudes +obtruding +obtrusive +obtrusively +obtrusiveness +obtrusiveness's +obtuse +obtusely +obtuseness +obtuseness's +obtuser +obtusest +obverse +obverse's +obverses +obviate +obviated +obviates +obviating +obvious +obviously +obviousness +obviousness's +ocarina +ocarina's +ocarinas +occasion +occasion's +occasional +occasionally +occasioned +occasioning +occasions +occidental +occidental's +occidentals +occlude +occluded +occludes +occluding +occlusion +occlusion's +occlusions +occlusive +occult +occult's +occupancy +occupancy's +occupant +occupant's +occupants +occupation +occupation's +occupational +occupations +occupied +occupies +occupy +occupying +occur +occurred +occurrence +occurrence's +occurrences +occurring +occurs +ocean +ocean's +oceangoing +oceanic +oceanic's +oceanographer +oceanographer's +oceanographers +oceanographic +oceanography +oceanography's +oceans +ocelot +ocelot's +ocelots +ocher +ocher's +ochre +ochre's +octagon +octagon's +octagonal +octagons +octal +octane +octane's +octave +octave's +octaves +octet +octet's +octets +octette +octette's +octettes +octogenarian +octogenarian's +octogenarians +octopi +octopus +octopus's +octopuses +ocular +ocular's +oculars +oculist +oculist's +oculists +odd +oddball +oddball's +oddballs +odder +oddest +oddities +oddity +oddity's +oddly +oddness +oddness's +odds +odds's +ode +ode's +odes +odious +odiously +odium +odium's +odometer +odometer's +odometers +odor +odor's +odoriferous +odorless +odorous +odors +odyssey +odyssey's +odysseys +of +off +offal +offal's +offbeat +offbeat's +offbeats +offed +offend +offended +offender +offender's +offenders +offending +offends +offense +offense's +offenses +offensive +offensive's +offensively +offensiveness +offensiveness's +offensives +offer +offer's +offered +offering +offering's +offerings +offers +offertories +offertory +offertory's +offhand +offhandedly +office +office's +officeholder +officeholder's +officeholders +officer +officer's +officers +offices +official +official's +officialdom +officialdom's +officially +officials +officiate +officiated +officiates +officiating +officious +officiously +officiousness +officiousness's +offing +offing's +offings +offload +offloaded +offloading +offloads +offs +offset +offset's +offsets +offsetting +offshoot +offshoot's +offshoots +offshore +offshoring +offside +offspring +offspring's +offsprings +offstage +offstages +oft +often +oftener +oftenest +oftentimes +ogle +ogle's +ogled +ogles +ogling +ogre +ogre's +ogres +oh +oh's +ohm +ohm's +ohms +oho +ohs +oil +oil's +oilcloth +oilcloth's +oilcloths +oiled +oilfield +oilfields +oilier +oiliest +oiliness +oiliness's +oiling +oils +oilskin +oilskin's +oily +oink +oink's +oinked +oinking +oinks +ointment +ointment's +ointments +okay +okay's +okayed +okaying +okays +okra +okra's +okras +old +old's +olden +older +oldest +oldie +oldie's +oldies +oleaginous +oleander +oleander's +oleanders +oleo +oleo's +oleomargarine +oleomargarine's +olfactories +olfactory +olfactory's +oligarch +oligarch's +oligarchic +oligarchies +oligarchs +oligarchy +oligarchy's +olive +olive's +olives +ombudsman +ombudsman's +ombudsmen +omega +omega's +omegas +omelet +omelet's +omelets +omelette +omelette's +omelettes +omen +omen's +omens +ominous +ominously +omission +omission's +omissions +omit +omits +omitted +omitting +omnibus +omnibus's +omnibuses +omnibusses +omnipotence +omnipotence's +omnipotent +omnipresence +omnipresence's +omnipresent +omniscience +omniscience's +omniscient +omnivore +omnivore's +omnivores +omnivorous +on +once +once's +oncology +oncology's +oncoming +one +one's +oneness +oneness's +onerous +ones +oneself +onetime +ongoing +onion +onion's +onions +onionskin +onionskin's +online +onlooker +onlooker's +onlookers +only +onomatopoeia +onomatopoeia's +onomatopoeic +onrush +onrush's +onrushes +onrushing +onset +onset's +onsets +onshore +onslaught +onslaught's +onslaughts +onto +onus +onus's +onuses +onward +onwards +onyx +onyx's +onyxes +oodles +oodles's +oops +ooze +ooze's +oozed +oozes +oozing +opacity +opacity's +opal +opal's +opalescence +opalescence's +opalescent +opals +opaque +opaqued +opaquely +opaqueness +opaqueness's +opaquer +opaques +opaquest +opaquing +open +open's +opened +opener +opener's +openers +openest +openhanded +opening +opening's +openings +openly +openness +openness's +opens +openwork +openwork's +opera +opera's +operable +operand +operands +operas +operate +operated +operates +operatic +operating +operation +operation's +operational +operationally +operations +operative +operative's +operatives +operator +operator's +operators +operetta +operetta's +operettas +ophthalmic +ophthalmologist +ophthalmologist's +ophthalmologists +ophthalmology +ophthalmology's +opiate +opiate's +opiates +opine +opined +opines +opining +opinion +opinion's +opinionated +opinions +opium +opium's +opossum +opossum's +opossums +opponent +opponent's +opponents +opportune +opportunism +opportunism's +opportunist +opportunist's +opportunistic +opportunists +opportunities +opportunity +opportunity's +oppose +opposed +opposes +opposing +opposite +opposite's +opposites +opposition +opposition's +oppress +oppressed +oppresses +oppressing +oppression +oppression's +oppressive +oppressively +oppressor +oppressor's +oppressors +opprobrious +opprobrium +opprobrium's +opt +opted +optic +optic's +optical +optically +optician +optician's +opticians +optics +optics's +optima +optimal +optimism +optimism's +optimist +optimist's +optimistic +optimistically +optimists +optimization +optimizations +optimize +optimized +optimizer +optimizes +optimizing +optimum +optimum's +optimums +opting +option +option's +optional +optionally +optioned +optioning +options +optometrist +optometrist's +optometrists +optometry +optometry's +opts +opulence +opulence's +opulent +opus +opus's +opuses +or +oracle +oracle's +oracles +oracular +oral +oral's +orally +orals +orange +orange's +orangeade +orangeade's +orangeades +oranges +orangutan +orangutan's +orangutang +orangutang's +orangutangs +orangutans +orate +orated +orates +orating +oration +oration's +orations +orator +orator's +oratorical +oratories +oratorio +oratorio's +oratorios +orators +oratory +oratory's +orb +orb's +orbit +orbit's +orbital +orbital's +orbitals +orbited +orbiting +orbits +orbs +orc +orc's +orchard +orchard's +orchards +orchestra +orchestra's +orchestral +orchestras +orchestrate +orchestrated +orchestrates +orchestrating +orchestration +orchestration's +orchestrations +orchid +orchid's +orchids +orcs +ordain +ordained +ordaining +ordains +ordeal +ordeal's +ordeals +order +order's +ordered +ordering +orderings +orderlies +orderliness +orderliness's +orderly +orderly's +orders +ordinal +ordinal's +ordinals +ordinance +ordinance's +ordinances +ordinaries +ordinarily +ordinariness +ordinariness's +ordinary +ordinary's +ordination +ordination's +ordinations +ordnance +ordnance's +ordure +ordure's +ore +ore's +oregano +oregano's +ores +organ +organ's +organdie +organdie's +organdy +organdy's +organelle +organelle's +organelles +organic +organic's +organically +organics +organism +organism's +organisms +organist +organist's +organists +organization +organization's +organizational +organizations +organize +organized +organizer +organizer's +organizers +organizes +organizing +organs +orgasm +orgasm's +orgasmic +orgasms +orgiastic +orgies +orgy +orgy's +orient +orient's +oriental +oriental's +orientals +orientate +orientated +orientates +orientating +orientation +orientation's +orientations +oriented +orienting +orients +orifice +orifice's +orifices +origami +origami's +origin +origin's +original +original's +originality +originality's +originally +originals +originate +originated +originates +originating +origination +origination's +originator +originator's +originators +origins +oriole +oriole's +orioles +ormolu +ormolu's +ornament +ornament's +ornamental +ornamentation +ornamentation's +ornamented +ornamenting +ornaments +ornate +ornately +ornateness +ornateness's +ornerier +orneriest +ornery +ornithologist +ornithologist's +ornithologists +ornithology +ornithology's +orotund +orphan +orphan's +orphanage +orphanage's +orphanages +orphaned +orphaning +orphans +orthodontia +orthodontia's +orthodontic +orthodontics +orthodontics's +orthodontist +orthodontist's +orthodontists +orthodox +orthodoxies +orthodoxy +orthodoxy's +orthogonal +orthogonality +orthographic +orthographies +orthography +orthography's +orthopaedic +orthopaedics +orthopaedics's +orthopaedist +orthopaedist's +orthopaedists +orthopedic +orthopedics +orthopedics's +orthopedist +orthopedist's +orthopedists +oscillate +oscillated +oscillates +oscillating +oscillation +oscillation's +oscillations +oscillator +oscillator's +oscillators +oscilloscope +oscilloscope's +oscilloscopes +osier +osier's +osiers +osmosis +osmosis's +osmotic +osprey +osprey's +ospreys +ossification +ossification's +ossified +ossifies +ossify +ossifying +ostensible +ostensibly +ostentation +ostentation's +ostentatious +ostentatiously +osteopath +osteopath's +osteopaths +osteopathy +osteopathy's +osteoporosis +osteoporosis's +ostracism +ostracism's +ostracize +ostracized +ostracizes +ostracizing +ostrich +ostrich's +ostriches +other +other's +others +otherwise +otherworldly +otiose +otter +otter's +otters +ottoman +ottoman's +ottomans +ouch +ought +ounce +ounce's +ounces +our +ours +ourselves +oust +ousted +ouster +ouster's +ousters +ousting +ousts +out +out's +outage +outage's +outages +outback +outback's +outbacks +outbalance +outbalanced +outbalances +outbalancing +outbid +outbidding +outbids +outbound +outbreak +outbreak's +outbreaks +outbuilding +outbuilding's +outbuildings +outburst +outburst's +outbursts +outcast +outcast's +outcasts +outclass +outclassed +outclasses +outclassing +outcome +outcome's +outcomes +outcries +outcrop +outcrop's +outcropped +outcropping +outcropping's +outcroppings +outcrops +outcry +outcry's +outdated +outdid +outdistance +outdistanced +outdistances +outdistancing +outdo +outdoes +outdoing +outdone +outdoor +outdoors +outdoors's +outed +outer +outermost +outfield +outfield's +outfielder +outfielder's +outfielders +outfields +outfit +outfit's +outfits +outfitted +outfitter +outfitter's +outfitters +outfitting +outflank +outflanked +outflanking +outflanks +outfox +outfoxed +outfoxes +outfoxing +outgo +outgo's +outgoes +outgoing +outgrew +outgrow +outgrowing +outgrown +outgrows +outgrowth +outgrowth's +outgrowths +outhouse +outhouse's +outhouses +outing +outing's +outings +outlaid +outlandish +outlandishly +outlast +outlasted +outlasting +outlasts +outlaw +outlaw's +outlawed +outlawing +outlaws +outlay +outlay's +outlaying +outlays +outlet +outlet's +outlets +outline +outline's +outlined +outlines +outlining +outlive +outlived +outlives +outliving +outlook +outlook's +outlooks +outlying +outmaneuver +outmaneuvered +outmaneuvering +outmaneuvers +outmanoeuvre +outmanoeuvred +outmanoeuvres +outmanoeuvring +outmoded +outnumber +outnumbered +outnumbering +outnumbers +outpatient +outpatient's +outpatients +outperform +outperformed +outperforming +outperforms +outplacement +outplacement's +outplay +outplayed +outplaying +outplays +outpost +outpost's +outposts +outpouring +outpouring's +outpourings +output +output's +outputs +outputted +outputting +outrage +outrage's +outraged +outrageous +outrageously +outrages +outraging +outran +outrank +outranked +outranking +outranks +outreach +outreach's +outreached +outreaches +outreaching +outrider +outrider's +outriders +outrigger +outrigger's +outriggers +outright +outrun +outrunning +outruns +outré +outs +outsell +outselling +outsells +outset +outset's +outsets +outshine +outshined +outshines +outshining +outshone +outside +outside's +outsider +outsider's +outsiders +outsides +outsize +outsize's +outsized +outsizes +outskirt +outskirt's +outskirts +outsmart +outsmarted +outsmarting +outsmarts +outsold +outsource +outsourced +outsources +outsourcing +outsourcing's +outspoken +outspokenly +outspokenness +outspokenness's +outspread +outspreading +outspreads +outstanding +outstandingly +outstation +outstation's +outstations +outstay +outstayed +outstaying +outstays +outstretch +outstretched +outstretches +outstretching +outstrip +outstripped +outstripping +outstrips +outstript +outtake +outtake's +outtakes +outvote +outvoted +outvotes +outvoting +outward +outwardly +outwards +outwear +outwearing +outwears +outweigh +outweighed +outweighing +outweighs +outwit +outwits +outwitted +outwitting +outwore +outworn +ova +oval +oval's +ovals +ovarian +ovaries +ovary +ovary's +ovation +ovation's +ovations +oven +oven's +ovens +over +over's +overabundance +overabundance's +overabundant +overachieve +overachieved +overachiever +overachiever's +overachievers +overachieves +overachieving +overact +overacted +overacting +overactive +overacts +overage +overage's +overages +overall +overall's +overalls +overalls's +overambitious +overanxious +overate +overawe +overawed +overawes +overawing +overbalance +overbalance's +overbalanced +overbalances +overbalancing +overbear +overbearing +overbears +overbite +overbite's +overbites +overblown +overboard +overbook +overbooked +overbooking +overbooks +overbore +overborne +overburden +overburdened +overburdening +overburdens +overcame +overcast +overcast's +overcasting +overcasts +overcautious +overcharge +overcharge's +overcharged +overcharges +overcharging +overcoat +overcoat's +overcoats +overcome +overcomes +overcoming +overcompensate +overcompensated +overcompensates +overcompensating +overcompensation +overcompensation's +overconfident +overcook +overcooked +overcooking +overcooks +overcrowd +overcrowded +overcrowding +overcrowds +overdid +overdo +overdoes +overdoing +overdone +overdose +overdose's +overdosed +overdoses +overdosing +overdraft +overdraft's +overdrafts +overdraw +overdrawing +overdrawn +overdraws +overdress +overdress's +overdressed +overdresses +overdressing +overdrew +overdrive +overdrive's +overdue +overeager +overeat +overeaten +overeating +overeats +overemphasize +overemphasized +overemphasizes +overemphasizing +overenthusiastic +overestimate +overestimate's +overestimated +overestimates +overestimating +overexpose +overexposed +overexposes +overexposing +overexposure +overexposure's +overextend +overextended +overextending +overextends +overflow +overflow's +overflowed +overflowing +overflows +overfull +overgenerous +overgrew +overgrow +overgrowing +overgrown +overgrows +overgrowth +overgrowth's +overhand +overhand's +overhands +overhang +overhang's +overhanging +overhangs +overhaul +overhaul's +overhauled +overhauling +overhauls +overhead +overhead's +overheads +overhear +overheard +overhearing +overhears +overheat +overheated +overheating +overheats +overhung +overindulge +overindulged +overindulgence +overindulgence's +overindulges +overindulging +overjoy +overjoyed +overjoying +overjoys +overkill +overkill's +overlaid +overlain +overland +overlap +overlap's +overlapped +overlapping +overlaps +overlay +overlay's +overlaying +overlays +overlie +overlies +overload +overload's +overloaded +overloading +overloads +overlong +overlook +overlook's +overlooked +overlooking +overlooks +overlord +overlord's +overlords +overly +overlying +overmuch +overmuches +overnight +overnight's +overnights +overpaid +overpass +overpass's +overpasses +overpay +overpaying +overpays +overplay +overplayed +overplaying +overplays +overpopulate +overpopulated +overpopulates +overpopulating +overpopulation +overpopulation's +overpower +overpowered +overpowering +overpowers +overprice +overpriced +overprices +overpricing +overprint +overprinted +overprinting +overprints +overproduce +overproduced +overproduces +overproducing +overproduction +overproduction's +overprotective +overqualified +overran +overrate +overrated +overrates +overrating +overreach +overreached +overreaches +overreaching +overreact +overreacted +overreacting +overreaction +overreaction's +overreactions +overreacts +overridden +override +override's +overrides +overriding +overripe +overripe's +overrode +overrule +overruled +overrules +overruling +overrun +overrun's +overrunning +overruns +overs +oversampling +oversaw +overseas +oversee +overseeing +overseen +overseer +overseer's +overseers +oversees +oversell +overselling +oversells +oversensitive +oversexed +overshadow +overshadowed +overshadowing +overshadows +overshare +overshared +overshares +oversharing +overshoe +overshoe's +overshoes +overshoot +overshooting +overshoots +overshot +oversight +oversight's +oversights +oversimplification +oversimplification's +oversimplifications +oversimplified +oversimplifies +oversimplify +oversimplifying +oversize +oversized +oversleep +oversleeping +oversleeps +overslept +oversold +overspecialize +overspecialized +overspecializes +overspecializing +overspend +overspending +overspends +overspent +overspill +overspread +overspreading +overspreads +overstate +overstated +overstatement +overstatement's +overstatements +overstates +overstating +overstay +overstayed +overstaying +overstays +overstep +overstepped +overstepping +oversteps +overstock +overstocked +overstocking +overstocks +overstuffed +oversupplied +oversupplies +oversupply +oversupplying +overt +overtake +overtaken +overtakes +overtaking +overtax +overtaxed +overtaxes +overtaxing +overthink +overthinking +overthinks +overthought +overthrew +overthrow +overthrow's +overthrowing +overthrown +overthrows +overtime +overtime's +overtimes +overtly +overtone +overtone's +overtones +overtook +overture +overture's +overtures +overturn +overturned +overturning +overturns +overuse +overuse's +overused +overuses +overusing +overview +overview's +overviews +overweening +overweight +overweight's +overwhelm +overwhelmed +overwhelming +overwhelmingly +overwhelms +overwork +overwork's +overworked +overworking +overworks +overwrite +overwrites +overwriting +overwritten +overwrought +overzealous +oviduct +oviduct's +oviducts +oviparous +ovoid +ovoid's +ovoids +ovulate +ovulated +ovulates +ovulating +ovulation +ovulation's +ovule +ovule's +ovules +ovum +ovum's +ow +owe +owed +owes +owing +owl +owl's +owlet +owlet's +owlets +owlish +owls +own +owned +owner +owner's +owners +ownership +ownership's +owning +owns +ox +ox's +oxbow +oxbow's +oxbows +oxen +oxford +oxford's +oxfords +oxidation +oxidation's +oxide +oxide's +oxides +oxidize +oxidized +oxidizer +oxidizer's +oxidizers +oxidizes +oxidizing +oxyacetylene +oxyacetylene's +oxygen +oxygen's +oxygenate +oxygenated +oxygenates +oxygenating +oxygenation +oxygenation's +oxymora +oxymoron +oxymoron's +oxymorons +oyster +oyster's +oysters +ozone +ozone's +p +pH +pa +pa's +pace +pace's +paced +pacemaker +pacemaker's +pacemakers +paces +pacesetter +pacesetter's +pacesetters +pachyderm +pachyderm's +pachyderms +pacific +pacifically +pacification +pacification's +pacified +pacifier +pacifier's +pacifiers +pacifies +pacifism +pacifism's +pacifist +pacifist's +pacifists +pacify +pacifying +pacing +pack +pack's +package +package's +packaged +packages +packaging +packaging's +packed +packer +packer's +packers +packet +packet's +packets +packing +packing's +packs +pact +pact's +pacts +pad +pad's +padded +paddies +padding +padding's +paddle +paddle's +paddled +paddles +paddling +paddock +paddock's +paddocked +paddocking +paddocks +paddy +paddy's +padlock +padlock's +padlocked +padlocking +padlocks +padre +padre's +padres +pads +paean +paean's +paeans +pagan +pagan's +paganism +paganism's +pagans +page +page's +pageant +pageant's +pageantry +pageantry's +pageants +paged +pager +pager's +pagers +pages +paginate +paginated +paginates +paginating +pagination +pagination's +paging +pagoda +pagoda's +pagodas +paid +pail +pail's +pailful +pailful's +pailfuls +pails +pailsful +pain +pain's +pained +painful +painfuller +painfullest +painfully +paining +painkiller +painkiller's +painkillers +painless +painlessly +pains +painstaking +painstaking's +painstakingly +paint +paint's +paintbrush +paintbrush's +paintbrushes +painted +painter +painter's +painters +painting +painting's +paintings +paints +paintwork +pair +pair's +paired +pairing +pairs +pairwise +paisley +paisley's +paisleys +pajamas +pajamas's +pal +pal's +palace +palace's +palaces +palatable +palatal +palatal's +palatals +palate +palate's +palates +palatial +palaver +palaver's +palavered +palavering +palavers +palazzi +palazzo +pale +pale's +paled +paleface +paleface's +palefaces +paleness +paleness's +paleontologist +paleontologist's +paleontologists +paleontology +paleontology's +paler +pales +palest +palette +palette's +palettes +palimony +palimony's +palimpsest +palimpsest's +palimpsests +palindrome +palindrome's +palindromes +palindromic +paling +paling's +palings +palisade +palisade's +palisades +pall +pall's +palladium +palladium's +pallbearer +pallbearer's +pallbearers +palled +pallet +pallet's +pallets +palliate +palliated +palliates +palliating +palliation +palliation's +palliative +palliative's +palliatives +pallid +palling +pallor +pallor's +palls +palm +palm's +palmed +palmetto +palmetto's +palmettoes +palmettos +palmier +palmiest +palming +palmist +palmist's +palmistry +palmistry's +palmists +palms +palmy +palomino +palomino's +palominos +palpable +palpably +palpate +palpated +palpates +palpating +palpation +palpation's +palpitate +palpitated +palpitates +palpitating +palpitation +palpitation's +palpitations +pals +palsied +palsies +palsy +palsy's +palsying +paltrier +paltriest +paltriness +paltriness's +paltry +pampas +pampas's +pamper +pampered +pampering +pampers +pamphlet +pamphlet's +pamphleteer +pamphleteer's +pamphleteers +pamphlets +pan +pan's +panacea +panacea's +panaceas +panache +panache's +pancake +pancake's +pancaked +pancakes +pancaking +panchromatic +pancreas +pancreas's +pancreases +pancreatic +panda +panda's +pandas +pandemic +pandemic's +pandemics +pandemonium +pandemonium's +pander +pander's +pandered +panderer +panderer's +panderers +pandering +panders +pane +pane's +panegyric +panegyric's +panegyrics +panel +panel's +paneled +paneling +paneling's +panelings +panelist +panelist's +panelists +panelled +panelling +panelling's +panellings +panels +panes +pang +pang's +pangs +panhandle +panhandle's +panhandled +panhandler +panhandler's +panhandlers +panhandles +panhandling +panic +panic's +panicked +panicking +panicky +panics +panier +panier's +paniers +panned +pannier +pannier's +panniers +panning +panoplies +panoply +panoply's +panorama +panorama's +panoramas +panoramic +pans +pansies +pansy +pansy's +pant +pant's +pantaloons +pantaloons's +panted +pantheism +pantheism's +pantheist +pantheist's +pantheistic +pantheists +pantheon +pantheon's +pantheons +panther +panther's +panthers +pantie +pantie's +panties +panting +pantomime +pantomime's +pantomimed +pantomimes +pantomiming +pantries +pantry +pantry's +pants +pantsuit +pantsuit's +pantsuits +panty +panty's +pantyhose +pantyhose's +pap +pap's +papa +papa's +papacies +papacy +papacy's +papal +papas +papaw +papaw's +papaws +papaya +papaya's +papayas +paper +paper's +paperback +paperback's +paperbacks +paperboy +paperboy's +paperboys +papered +papergirl +papergirl's +papergirls +paperhanger +paperhanger's +paperhangers +papering +papers +paperweight +paperweight's +paperweights +paperwork +paperwork's +papery +papilla +papilla's +papillae +papoose +papoose's +papooses +paprika +paprika's +paps +papyri +papyrus +papyrus's +papyruses +par +par's +parable +parable's +parables +parabola +parabola's +parabolas +parabolic +parachute +parachute's +parachuted +parachutes +parachuting +parachutist +parachutist's +parachutists +parade +parade's +paraded +parades +paradigm +paradigm's +paradigmatic +paradigms +parading +paradise +paradise's +paradises +paradox +paradox's +paradoxes +paradoxical +paradoxically +paraffin +paraffin's +paragliding +paragon +paragon's +paragons +paragraph +paragraph's +paragraphed +paragraphing +paragraphs +parakeet +parakeet's +parakeets +paralegal +paralegal's +paralegals +parallax +parallax's +parallaxes +parallel +parallel's +paralleled +paralleling +parallelism +parallelism's +parallelisms +parallelled +parallelling +parallelogram +parallelogram's +parallelograms +parallels +paralyses +paralysis +paralysis's +paralytic +paralytic's +paralytics +paralyze +paralyzed +paralyzes +paralyzing +paramecia +paramecium +paramecium's +parameciums +paramedic +paramedic's +paramedical +paramedical's +paramedicals +paramedics +parameter +parameter's +parameters +paramilitaries +paramilitary +paramilitary's +paramount +paramour +paramour's +paramours +paranoia +paranoia's +paranoid +paranoid's +paranoids +paranormal +parapet +parapet's +parapets +paraphernalia +paraphernalia's +paraphrase +paraphrase's +paraphrased +paraphrases +paraphrasing +paraplegia +paraplegia's +paraplegic +paraplegic's +paraplegics +paraprofessional +paraprofessional's +paraprofessionals +parapsychology +parapsychology's +parasailing +parasite +parasite's +parasites +parasitic +parasol +parasol's +parasols +paratrooper +paratrooper's +paratroopers +paratroops +paratroops's +parboil +parboiled +parboiling +parboils +parcel +parcel's +parceled +parceling +parcelled +parcelling +parcels +parch +parched +parches +parching +parchment +parchment's +parchments +pardon +pardon's +pardonable +pardoned +pardoning +pardons +pare +pared +parent +parent's +parentage +parentage's +parental +parented +parentheses +parenthesis +parenthesis's +parenthesize +parenthesized +parenthesizes +parenthesizing +parenthetic +parenthetical +parenthetically +parenthood +parenthood's +parenting +parenting's +parents +pares +parfait +parfait's +parfaits +pariah +pariah's +pariahs +paring +paring's +parings +parish +parish's +parishes +parishioner +parishioner's +parishioners +parity +parity's +park +park's +parka +parka's +parkas +parked +parking +parking's +parkour +parks +parkway +parkway's +parkways +parlance +parlance's +parlay +parlay's +parlayed +parlaying +parlays +parley +parley's +parleyed +parleying +parleys +parliament +parliament's +parliamentarian +parliamentarian's +parliamentarians +parliamentary +parliaments +parlor +parlor's +parlors +parochial +parochialism +parochialism's +parodied +parodies +parody +parody's +parodying +parole +parole's +paroled +parolee +parolee's +parolees +paroles +paroling +paroxysm +paroxysm's +paroxysms +parquet +parquet's +parqueted +parqueting +parquetry +parquetry's +parquets +parrakeet +parrakeet's +parrakeets +parred +parricide +parricide's +parricides +parried +parries +parring +parrot +parrot's +parroted +parroting +parrots +parry +parry's +parrying +pars +parse +parsec +parsec's +parsecs +parsed +parser +parses +parsimonious +parsimony +parsimony's +parsing +parsley +parsley's +parsnip +parsnip's +parsnips +parson +parson's +parsonage +parsonage's +parsonages +parsons +part +part's +partake +partaken +partaker +partaker's +partakers +partakes +partaking +parted +parterre +parterre's +parterres +parthenogenesis +parthenogenesis's +partial +partial's +partiality +partiality's +partially +partials +participant +participant's +participants +participate +participated +participates +participating +participation +participation's +participator +participator's +participators +participatory +participial +participial's +participle +participle's +participles +particle +particle's +particles +particular +particular's +particularities +particularity +particularity's +particularization +particularization's +particularize +particularized +particularizes +particularizing +particularly +particulars +particulate +particulate's +particulates +partied +parties +parting +parting's +partings +partisan +partisan's +partisans +partisanship +partisanship's +partition +partition's +partitioned +partitioning +partitions +partizan +partizan's +partizans +partly +partner +partner's +partnered +partnering +partners +partnership +partnership's +partnerships +partook +partridge +partridge's +partridges +parts +parturition +parturition's +partway +party +party's +partying +parvenu +parvenu's +parvenus +pas +paschal +pasha +pasha's +pashas +pass +pass's +passable +passably +passage +passage's +passages +passageway +passageway's +passageways +passbook +passbook's +passbooks +passed +passel +passel's +passels +passenger +passenger's +passengers +passer +passerby +passerby's +passersby +passes +passing +passing's +passion +passion's +passionate +passionately +passionless +passions +passive +passive's +passively +passives +passivity +passivity's +passkey +passkey's +passkeys +passport +passport's +passports +password +password's +passwords +passé +past +past's +pasta +pasta's +pastas +paste +paste's +pasteboard +pasteboard's +pasted +pastel +pastel's +pastels +pastern +pastern's +pasterns +pastes +pasteurization +pasteurization's +pasteurize +pasteurized +pasteurizes +pasteurizing +pastiche +pastiche's +pastiches +pastier +pasties +pastiest +pastime +pastime's +pastimes +pasting +pastor +pastor's +pastoral +pastoral's +pastorals +pastorate +pastorate's +pastorates +pastors +pastrami +pastrami's +pastries +pastry +pastry's +pasts +pasturage +pasturage's +pasture +pasture's +pastured +pastures +pasturing +pasty +pasty's +pat +pat's +patch +patch's +patched +patches +patchier +patchiest +patchiness +patchiness's +patching +patchwork +patchwork's +patchworks +patchy +pate +pate's +patella +patella's +patellae +patellas +patent +patent's +patented +patenting +patently +patents +paternal +paternalism +paternalism's +paternalistic +paternally +paternity +paternity's +pates +path +path's +pathetic +pathetically +pathogen +pathogen's +pathogenic +pathogens +pathological +pathologically +pathologist +pathologist's +pathologists +pathology +pathology's +pathos +pathos's +paths +pathway +pathway's +pathways +patience +patience's +patient +patient's +patienter +patientest +patiently +patients +patina +patina's +patinae +patinas +patine +patio +patio's +patios +patois +patois's +patriarch +patriarch's +patriarchal +patriarchies +patriarchs +patriarchy +patriarchy's +patrician +patrician's +patricians +patricide +patricide's +patricides +patrimonial +patrimonies +patrimony +patrimony's +patriot +patriot's +patriotic +patriotically +patriotism +patriotism's +patriots +patrol +patrol's +patrolled +patrolling +patrolman +patrolman's +patrolmen +patrols +patrolwoman +patrolwoman's +patrolwomen +patron +patron's +patronage +patronage's +patronages +patronize +patronized +patronizes +patronizing +patronizingly +patrons +patronymic +patronymic's +patronymics +pats +patsies +patsy +patsy's +patted +patter +patter's +pattered +pattering +pattern +pattern's +patterned +patterning +patterns +patters +patties +patting +patty +patty's +paucity +paucity's +paunch +paunch's +paunches +paunchier +paunchiest +paunchy +pauper +pauper's +pauperism +pauperism's +pauperize +pauperized +pauperizes +pauperizing +paupers +pause +pause's +paused +pauses +pausing +pave +paved +pavement +pavement's +pavements +paves +pavilion +pavilion's +pavilions +paving +paving's +pavings +paw +paw's +pawed +pawing +pawl +pawl's +pawls +pawn +pawn's +pawnbroker +pawnbroker's +pawnbrokers +pawned +pawning +pawns +pawnshop +pawnshop's +pawnshops +pawpaw +pawpaw's +pawpaws +paws +pay +pay's +payable +paycheck +paycheck's +paychecks +payday +payday's +paydays +payed +payee +payee's +payees +payer +payer's +payers +paying +payload +payload's +payloads +paymaster +paymaster's +paymasters +payment +payment's +payments +payoff +payoff's +payoffs +payroll +payroll's +payrolls +pays +paywall +paywall's +paywalls +pea +pea's +peace +peace's +peaceable +peaceably +peaceful +peacefully +peacefulness +peacefulness's +peacekeeping +peacekeeping's +peacemaker +peacemaker's +peacemakers +peaces +peacetime +peacetime's +peach +peach's +peaches +peacock +peacock's +peacocks +peafowl +peafowl's +peafowls +peahen +peahen's +peahens +peak +peak's +peaked +peaking +peaks +peal +peal's +pealed +pealing +peals +peanut +peanut's +peanuts +pear +pear's +pearl +pearl's +pearled +pearlier +pearliest +pearling +pearls +pearly +pears +peas +peasant +peasant's +peasantry +peasantry's +peasants +pease +peat +peat's +pebble +pebble's +pebbled +pebbles +pebbling +pebbly +pecan +pecan's +pecans +peccadillo +peccadillo's +peccadilloes +peccadillos +peccaries +peccary +peccary's +peck +peck's +pecked +pecking +pecks +pecs +pectin +pectin's +pectoral +pectoral's +pectorals +peculiar +peculiarities +peculiarity +peculiarity's +peculiarly +pecuniary +pedagog +pedagog's +pedagogic +pedagogical +pedagogs +pedagogue +pedagogue's +pedagogues +pedagogy +pedagogy's +pedal +pedal's +pedaled +pedaling +pedalled +pedalling +pedals +pedant +pedant's +pedantic +pedantically +pedantry +pedantry's +pedants +peddle +peddled +peddler +peddler's +peddlers +peddles +peddling +pederast +pederast's +pederasts +pederasty +pederasty's +pedestal +pedestal's +pedestals +pedestrian +pedestrian's +pedestrianize +pedestrianized +pedestrianizes +pedestrianizing +pedestrians +pediatric +pediatrician +pediatrician's +pediatricians +pediatrics +pediatrics's +pediatrist +pediatrist's +pediatrists +pedicure +pedicure's +pedicured +pedicures +pedicuring +pedigree +pedigree's +pedigreed +pedigrees +pediment +pediment's +pediments +pedlar +pedlar's +pedlars +pedometer +pedometer's +pedometers +pee +pee's +peed +peeing +peek +peek's +peekaboo +peekaboo's +peeked +peeking +peeks +peel +peel's +peeled +peeling +peeling's +peelings +peels +peep +peep's +peeped +peeper +peeper's +peepers +peephole +peephole's +peepholes +peeping +peeps +peer +peer's +peerage +peerage's +peerages +peered +peering +peerless +peers +pees +peeve +peeve's +peeved +peeves +peeving +peevish +peevishly +peevishness +peevishness's +peewee +peewee's +peewees +peg +peg's +pegged +pegging +pegs +pejorative +pejorative's +pejoratives +pekoe +pekoe's +pelagic +pelican +pelican's +pelicans +pellagra +pellagra's +pellet +pellet's +pelleted +pelleting +pellets +pellucid +pelt +pelt's +pelted +pelting +pelts +pelves +pelvic +pelvis +pelvis's +pelvises +pen +pen's +penal +penalize +penalized +penalizes +penalizing +penalties +penalty +penalty's +penance +penance's +penances +pence +penchant +penchant's +penchants +pencil +pencil's +penciled +penciling +pencilled +pencilling +pencils +pendant +pendant's +pendants +pended +pendent +pendent's +pendents +pending +pends +pendulous +pendulum +pendulum's +pendulums +penes +penetrable +penetrate +penetrated +penetrates +penetrating +penetration +penetration's +penetrations +penetrative +penguin +penguin's +penguins +penicillin +penicillin's +penile +peninsula +peninsula's +peninsular +peninsulas +penis +penis's +penises +penitence +penitence's +penitent +penitent's +penitential +penitentiaries +penitentiary +penitentiary's +penitently +penitents +penknife +penknife's +penknives +penlight +penlight's +penlights +penlite +penlite's +penlites +penmanship +penmanship's +pennant +pennant's +pennants +penned +pennies +penniless +penning +pennon +pennon's +pennons +penny +penny's +pennyweight +pennyweight's +pennyweights +penologist +penologist's +penologists +penology +penology's +pens +pension +pension's +pensioned +pensioner +pensioner's +pensioners +pensioning +pensions +pensive +pensively +pensiveness +pensiveness's +pent +pentagon +pentagon's +pentagonal +pentagons +pentameter +pentameter's +pentameters +pentathlon +pentathlon's +pentathlons +penthouse +penthouse's +penthouses +penultimate +penultimate's +penultimates +penurious +penury +penury's +peon +peon's +peonage +peonage's +peonies +peons +peony +peony's +people +people's +peopled +peoples +peopling +pep +pep's +pepped +pepper +pepper's +peppercorn +peppercorn's +peppercorns +peppered +peppering +peppermint +peppermint's +peppermints +pepperoni +pepperoni's +pepperonis +peppers +peppery +peppier +peppiest +pepping +peppy +peps +pepsin +pepsin's +peptic +peptic's +peptics +per +perambulate +perambulated +perambulates +perambulating +perambulator +perambulator's +perambulators +percale +percale's +percales +perceivable +perceive +perceived +perceives +perceiving +percent +percent's +percentage +percentage's +percentages +percentile +percentile's +percentiles +percents +perceptible +perceptibly +perception +perception's +perceptions +perceptive +perceptively +perceptiveness +perceptiveness's +perceptual +perch +perch's +perchance +perched +perches +perching +percolate +percolated +percolates +percolating +percolation +percolation's +percolator +percolator's +percolators +percussion +percussion's +percussionist +percussionist's +percussionists +perdition +perdition's +peregrination +peregrination's +peregrinations +peremptorily +peremptory +perennial +perennial's +perennially +perennials +perfect +perfect's +perfected +perfecter +perfectest +perfectible +perfecting +perfection +perfection's +perfectionism +perfectionism's +perfectionist +perfectionist's +perfectionists +perfections +perfectly +perfects +perfidies +perfidious +perfidy +perfidy's +perforate +perforated +perforates +perforating +perforation +perforation's +perforations +perforce +perform +performance +performance's +performances +performed +performer +performer's +performers +performing +performs +perfume +perfume's +perfumed +perfumeries +perfumery +perfumery's +perfumes +perfuming +perfunctorily +perfunctory +perhaps +pericardia +pericardium +pericardium's +pericardiums +perigee +perigee's +perigees +perihelia +perihelion +perihelion's +perihelions +peril +peril's +periled +periling +perilled +perilling +perilous +perilously +perils +perimeter +perimeter's +perimeters +period +period's +periodic +periodical +periodical's +periodically +periodicals +periodicity +periodontal +periods +peripatetic +peripatetic's +peripatetics +peripheral +peripheral's +peripherals +peripheries +periphery +periphery's +periphrases +periphrasis +periphrasis's +periscope +periscope's +periscopes +perish +perishable +perishable's +perishables +perished +perishes +perishing +peritonea +peritoneum +peritoneum's +peritoneums +peritonitis +peritonitis's +periwig +periwig's +periwigs +periwinkle +periwinkle's +periwinkles +perjure +perjured +perjurer +perjurer's +perjurers +perjures +perjuries +perjuring +perjury +perjury's +perk +perk's +perked +perkier +perkiest +perkiness +perkiness's +perking +perks +perky +perm +perm's +permafrost +permafrost's +permanence +permanence's +permanent +permanent's +permanently +permanents +permeability +permeability's +permeable +permeate +permeated +permeates +permeating +permed +perming +permissible +permissibly +permission +permission's +permissions +permissive +permissively +permissiveness +permissiveness's +permit +permit's +permits +permitted +permitting +perms +permutation +permutation's +permutations +permute +permuted +permutes +permuting +pernicious +perniciously +peroration +peroration's +perorations +peroxide +peroxide's +peroxided +peroxides +peroxiding +perpendicular +perpendicular's +perpendiculars +perpetrate +perpetrated +perpetrates +perpetrating +perpetration +perpetration's +perpetrator +perpetrator's +perpetrators +perpetual +perpetual's +perpetually +perpetuals +perpetuate +perpetuated +perpetuates +perpetuating +perpetuation +perpetuation's +perpetuity +perpetuity's +perplex +perplexed +perplexes +perplexing +perplexities +perplexity +perplexity's +perquisite +perquisite's +perquisites +persecute +persecuted +persecutes +persecuting +persecution +persecution's +persecutions +persecutor +persecutor's +persecutors +perseverance +perseverance's +persevere +persevered +perseveres +persevering +persiflage +persiflage's +persimmon +persimmon's +persimmons +persist +persisted +persistence +persistence's +persistent +persistently +persisting +persists +persnickety +person +person's +persona +persona's +personable +personae +personage +personage's +personages +personal +personal's +personalities +personality +personality's +personalize +personalized +personalizes +personalizing +personally +personals +personification +personification's +personifications +personified +personifies +personify +personifying +personnel +personnel's +persons +perspective +perspective's +perspectives +perspicacious +perspicacity +perspicacity's +perspicuity +perspicuity's +perspicuous +perspiration +perspiration's +perspire +perspired +perspires +perspiring +persuade +persuaded +persuades +persuading +persuasion +persuasion's +persuasions +persuasive +persuasively +persuasiveness +persuasiveness's +pert +pertain +pertained +pertaining +pertains +perter +pertest +pertinacious +pertinacity +pertinacity's +pertinence +pertinence's +pertinent +pertly +pertness +pertness's +perturb +perturbation +perturbation's +perturbations +perturbed +perturbing +perturbs +perusal +perusal's +perusals +peruse +perused +peruses +perusing +pervade +pervaded +pervades +pervading +pervasive +perverse +perversely +perverseness +perverseness's +perversion +perversion's +perversions +perversity +perversity's +pervert +pervert's +perverted +perverting +perverts +peseta +peseta's +pesetas +peskier +peskiest +pesky +peso +peso's +pesos +pessimism +pessimism's +pessimist +pessimist's +pessimistic +pessimistically +pessimists +pest +pest's +pester +pestered +pestering +pesters +pesticide +pesticide's +pesticides +pestilence +pestilence's +pestilences +pestilent +pestle +pestle's +pestled +pestles +pestling +pests +pet +pet's +petal +petal's +petals +petard +petard's +petards +peter +peter's +petered +petering +peters +petiole +petiole's +petioles +petite +petite's +petites +petition +petition's +petitioned +petitioner +petitioner's +petitioners +petitioning +petitions +petrel +petrel's +petrels +petrifaction +petrifaction's +petrified +petrifies +petrify +petrifying +petrochemical +petrochemical's +petrochemicals +petrol +petrol's +petrolatum +petrolatum's +petroleum +petroleum's +pets +petted +petticoat +petticoat's +petticoats +pettier +pettiest +pettifog +pettifogged +pettifogger +pettifogger's +pettifoggers +pettifogging +pettifogs +pettily +pettiness +pettiness's +petting +petty +petulance +petulance's +petulant +petulantly +petunia +petunia's +petunias +pew +pew's +pewee +pewee's +pewees +pews +pewter +pewter's +pewters +peyote +peyote's +phalanges +phalanx +phalanx's +phalanxes +phalli +phallic +phallus +phallus's +phalluses +phantasied +phantasies +phantasm +phantasm's +phantasmagoria +phantasmagoria's +phantasmagorias +phantasms +phantasy +phantasy's +phantasying +phantom +phantom's +phantoms +pharaoh +pharaoh's +pharaohs +pharmaceutical +pharmaceutical's +pharmaceuticals +pharmacies +pharmacist +pharmacist's +pharmacists +pharmacologist +pharmacologist's +pharmacologists +pharmacology +pharmacology's +pharmacopeia +pharmacopeia's +pharmacopeias +pharmacopoeia +pharmacopoeia's +pharmacopoeias +pharmacy +pharmacy's +pharyngeal +pharynges +pharynx +pharynx's +pharynxes +phase +phase's +phased +phases +phasing +pheasant +pheasant's +pheasants +phenobarbital +phenobarbital's +phenomena +phenomenal +phenomenally +phenomenon +phenomenon's +phenomenons +phenotype +pheromone +pheromone's +pheromones +phial +phial's +phials +philander +philandered +philanderer +philanderer's +philanderers +philandering +philanders +philanthropic +philanthropically +philanthropies +philanthropist +philanthropist's +philanthropists +philanthropy +philanthropy's +philatelic +philatelist +philatelist's +philatelists +philately +philately's +philharmonic +philharmonic's +philharmonics +philippic +philippic's +philippics +philistine +philistine's +philistines +philodendra +philodendron +philodendron's +philodendrons +philological +philologist +philologist's +philologists +philology +philology's +philosopher +philosopher's +philosophers +philosophic +philosophical +philosophically +philosophies +philosophize +philosophized +philosophizes +philosophizing +philosophy +philosophy's +philter +philter's +philters +phish +phished +phisher +phisher's +phishers +phishing +phlebitis +phlebitis's +phlegm +phlegm's +phlegmatic +phlegmatically +phloem +phloem's +phlox +phlox's +phloxes +phobia +phobia's +phobias +phobic +phobic's +phobics +phoebe +phoebe's +phoebes +phoenix +phoenix's +phoenixes +phone +phone's +phoned +phoneme +phoneme's +phonemes +phonemic +phones +phonetic +phonetically +phonetician +phonetician's +phoneticians +phonetics +phonetics's +phoney +phoney's +phoneyed +phoneying +phoneys +phonic +phonically +phonics +phonics's +phonied +phonier +phonies +phoniest +phoniness +phoniness's +phoning +phonograph +phonograph's +phonographs +phonological +phonologist +phonologist's +phonologists +phonology +phonology's +phony +phony's +phonying +phooey +phosphate +phosphate's +phosphates +phosphor +phosphor's +phosphorescence +phosphorescence's +phosphorescent +phosphoric +phosphors +phosphorus +phosphorus's +photo +photo's +photocopied +photocopier +photocopier's +photocopiers +photocopies +photocopy +photocopy's +photocopying +photoed +photoelectric +photogenic +photograph +photograph's +photographed +photographer +photographer's +photographers +photographic +photographically +photographing +photographs +photography +photography's +photoing +photojournalism +photojournalism's +photojournalist +photojournalist's +photojournalists +photon +photon's +photons +photos +photosensitive +photosynthesis +photosynthesis's +phototypesetter +phototypesetting +phrasal +phrase +phrase's +phrased +phraseology +phraseology's +phrases +phrasing +phrasing's +phrasings +phrenology +phrenology's +phyla +phylum +phylum's +physic +physic's +physical +physical's +physically +physicals +physician +physician's +physicians +physicist +physicist's +physicists +physicked +physicking +physics +physics's +physiognomies +physiognomy +physiognomy's +physiological +physiologist +physiologist's +physiologists +physiology +physiology's +physiotherapist +physiotherapist's +physiotherapists +physiotherapy +physiotherapy's +physique +physique's +physiques +pi +pi's +pianissimi +pianissimo +pianissimo's +pianissimos +pianist +pianist's +pianists +piano +piano's +pianoforte +pianoforte's +pianofortes +pianos +piazza +piazza's +piazzas +piazze +pica +pica's +picante +picaresque +picayune +piccalilli +piccalilli's +piccolo +piccolo's +piccolos +pick +pick's +pickaback +pickaback's +pickabacked +pickabacking +pickabacks +pickax +pickax's +pickaxe +pickaxe's +pickaxed +pickaxes +pickaxing +picked +picker +picker's +pickerel +pickerel's +pickerels +pickers +picket +picket's +picketed +picketing +pickets +pickier +pickiest +picking +pickings +pickings's +pickle +pickle's +pickled +pickles +pickling +pickpocket +pickpocket's +pickpockets +picks +pickup +pickup's +pickups +picky +picnic +picnic's +picnicked +picnicker +picnicker's +picnickers +picnicking +picnics +pictograph +pictograph's +pictographs +pictorial +pictorial's +pictorially +pictorials +picture +picture's +pictured +pictures +picturesque +picturing +piddle +piddle's +piddled +piddles +piddling +pidgin +pidgin's +pidgins +pie +pie's +piebald +piebald's +piebalds +piece +piece's +pieced +piecemeal +pieces +piecework +piecework's +piecing +pied +pieing +pier +pier's +pierce +pierced +pierces +piercing +piercing's +piercingly +piercings +piers +pies +piety +piety's +piffle +piffle's +pig +pig's +pigeon +pigeon's +pigeonhole +pigeonhole's +pigeonholed +pigeonholes +pigeonholing +pigeons +pigged +piggier +piggies +piggiest +pigging +piggish +piggishness +piggishness's +piggy +piggy's +piggyback +piggyback's +piggybacked +piggybacking +piggybacks +pigheaded +piglet +piglet's +piglets +pigment +pigment's +pigmentation +pigmentation's +pigments +pigmies +pigmy +pigmy's +pigpen +pigpen's +pigpens +pigs +pigskin +pigskin's +pigskins +pigsties +pigsty +pigsty's +pigtail +pigtail's +pigtails +piing +pike +pike's +piked +piker +piker's +pikers +pikes +piking +pilaf +pilaf's +pilaff +pilaff's +pilaffs +pilafs +pilaster +pilaster's +pilasters +pilau +pilau's +pilaus +pilaw +pilaw's +pilaws +pilchard +pilchard's +pilchards +pile +pile's +piled +piles +pileup +pileup's +pileups +pilfer +pilfered +pilferer +pilferer's +pilferers +pilfering +pilfers +pilgrim +pilgrim's +pilgrimage +pilgrimage's +pilgrimages +pilgrims +piling +piling's +pilings +pill +pill's +pillage +pillage's +pillaged +pillages +pillaging +pillar +pillar's +pillars +pillbox +pillbox's +pillboxes +pilled +pilling +pillion +pillion's +pillions +pilloried +pillories +pillory +pillory's +pillorying +pillow +pillow's +pillowcase +pillowcase's +pillowcases +pillowed +pillowing +pillows +pills +pilot +pilot's +piloted +pilothouse +pilothouse's +pilothouses +piloting +pilots +pimento +pimento's +pimentos +pimiento +pimiento's +pimientos +pimp +pimp's +pimped +pimpernel +pimpernel's +pimpernels +pimping +pimple +pimple's +pimples +pimplier +pimpliest +pimply +pimps +pin +pin's +pinafore +pinafore's +pinafores +pinball +pinball's +pincer +pincer's +pincers +pinch +pinch's +pinched +pinches +pinching +pincushion +pincushion's +pincushions +pine +pine's +pineapple +pineapple's +pineapples +pined +pines +pinfeather +pinfeather's +pinfeathers +ping +ping's +pinged +pinging +pings +pinhead +pinhead's +pinheads +pinhole +pinhole's +pinholes +pining +pinion +pinion's +pinioned +pinioning +pinions +pink +pink's +pinked +pinker +pinkest +pinkeye +pinkeye's +pinkie +pinkie's +pinkies +pinking +pinkish +pinks +pinky +pinky's +pinnacle +pinnacle's +pinnacles +pinnate +pinned +pinning +pinochle +pinochle's +pinpoint +pinpoint's +pinpointed +pinpointing +pinpoints +pinprick +pinprick's +pinpricks +pins +pinstripe +pinstripe's +pinstriped +pinstripes +pint +pint's +pinto +pinto's +pintoes +pintos +pints +pinup +pinup's +pinups +pinwheel +pinwheel's +pinwheeled +pinwheeling +pinwheels +pioneer +pioneer's +pioneered +pioneering +pioneers +pious +piously +pip +pip's +pipe +pipe's +piped +pipeline +pipeline's +pipelines +piper +piper's +pipers +pipes +piping +piping's +pipit +pipit's +pipits +pipped +pippin +pippin's +pipping +pippins +pips +pipsqueak +pipsqueak's +pipsqueaks +piquancy +piquancy's +piquant +pique +pique's +piqued +piques +piquing +piracy +piracy's +piranha +piranha's +piranhas +pirate +pirate's +pirated +pirates +piratical +pirating +pirouette +pirouette's +pirouetted +pirouettes +pirouetting +pis +piscatorial +piss +piss's +pissed +pisses +pissing +pistachio +pistachio's +pistachios +pistil +pistil's +pistillate +pistils +pistol +pistol's +pistols +piston +piston's +pistons +pit +pit's +pita +pita's +pitch +pitch's +pitchblende +pitchblende's +pitched +pitcher +pitcher's +pitchers +pitches +pitchfork +pitchfork's +pitchforked +pitchforking +pitchforks +pitching +pitchman +pitchman's +pitchmen +piteous +piteously +pitfall +pitfall's +pitfalls +pith +pith's +pithier +pithiest +pithily +pithy +pitiable +pitiably +pitied +pities +pitiful +pitifully +pitiless +pitilessly +piton +piton's +pitons +pits +pittance +pittance's +pittances +pitted +pitting +pituitaries +pituitary +pituitary's +pity +pity's +pitying +pivot +pivot's +pivotal +pivoted +pivoting +pivots +pixel +pixel's +pixels +pixie +pixie's +pixies +pixy +pixy's +pizazz +pizazz's +pizza +pizza's +pizzas +pizzazz +pizzazz's +pizzeria +pizzeria's +pizzerias +pizzicati +pizzicato +pizzicato's +pizzicatos +pj's +placard +placard's +placarded +placarding +placards +placate +placated +placates +placating +placation +placation's +place +place's +placebo +placebo's +placebos +placed +placeholder +placement +placement's +placements +placenta +placenta's +placentae +placental +placentals +placentas +placer +placer's +placers +places +placid +placidity +placidity's +placidly +placing +placket +placket's +plackets +plagiarism +plagiarism's +plagiarisms +plagiarist +plagiarist's +plagiarists +plagiarize +plagiarized +plagiarizes +plagiarizing +plague +plague's +plagued +plagues +plaguing +plaice +plaid +plaid's +plaids +plain +plain's +plainclothes +plainclothesman +plainclothesman's +plainclothesmen +plainer +plainest +plainly +plainness +plainness's +plains +plaint +plaint's +plaintiff +plaintiff's +plaintiffs +plaintive +plaintively +plaints +plait +plait's +plaited +plaiting +plaits +plan +plan's +planar +plane +plane's +planed +planes +planet +planet's +planetaria +planetarium +planetarium's +planetariums +planetary +planets +plangent +planing +plank +plank's +planked +planking +planking's +planks +plankton +plankton's +planned +planner +planner's +planners +planning +plannings +plans +plant +plant's +plantain +plantain's +plantains +plantation +plantation's +plantations +planted +planter +planter's +planters +planting +planting's +plantings +plants +plaque +plaque's +plaques +plasma +plasma's +plaster +plaster's +plasterboard +plasterboard's +plastered +plasterer +plasterer's +plasterers +plastering +plasters +plastic +plastic's +plasticity +plasticity's +plastics +plastique +plate +plate's +plateau +plateau's +plateaued +plateauing +plateaus +plateaux +plated +plateful +plateful's +platefuls +platelet +platelet's +platelets +platen +platen's +platens +plates +platform +platform's +platformed +platforming +platforms +plating +plating's +platinum +platinum's +platitude +platitude's +platitudes +platitudinous +platonic +platoon +platoon's +platooned +platooning +platoons +platter +platter's +platters +platypi +platypus +platypus's +platypuses +plaudit +plaudit's +plaudits +plausibility +plausibility's +plausible +plausibly +play +play's +playable +playact +playacted +playacting +playacting's +playacts +playback +playback's +playbacks +playbill +playbill's +playbills +playboy +playboy's +playboys +played +player +player's +players +playful +playfully +playfulness +playfulness's +playgoer +playgoer's +playgoers +playground +playground's +playgrounds +playhouse +playhouse's +playhouses +playing +playlist +playlist's +playlists +playmate +playmate's +playmates +playoff +playoff's +playoffs +playpen +playpen's +playpens +playroom +playroom's +playrooms +plays +plaything +plaything's +playthings +playwright +playwright's +playwrights +plaza +plaza's +plazas +plea +plea's +plead +pleaded +pleader +pleader's +pleaders +pleading +pleads +pleas +pleasant +pleasanter +pleasantest +pleasantly +pleasantness +pleasantness's +pleasantries +pleasantry +pleasantry's +please +pleased +pleases +pleasing +pleasingly +pleasings +pleasurable +pleasurably +pleasure +pleasure's +pleasured +pleasures +pleasuring +pleat +pleat's +pleated +pleating +pleats +plebeian +plebeian's +plebeians +plebiscite +plebiscite's +plebiscites +plectra +plectrum +plectrum's +plectrums +pled +pledge +pledge's +pledged +pledges +pledging +plenaries +plenary +plenary's +plenipotentiaries +plenipotentiary +plenipotentiary's +plenitude +plenitude's +plenitudes +plenteous +plentiful +plentifully +plenty +plenty's +plethora +plethora's +pleurisy +pleurisy's +plexus +plexus's +plexuses +pliability +pliability's +pliable +pliancy +pliancy's +pliant +plied +pliers +pliers's +plies +plight +plight's +plighted +plighting +plights +plinth +plinth's +plinths +plod +plodded +plodder +plodder's +plodders +plodding +ploddings +plods +plop +plop's +plopped +plopping +plops +plot +plot's +plots +plotted +plotter +plotter's +plotters +plotting +plough +plough's +ploughed +ploughing +ploughs +ploughshare +ploughshare's +ploughshares +plover +plover's +plovers +plow +plow's +plowed +plowing +plowman +plowman's +plowmen +plows +plowshare +plowshare's +plowshares +ploy +ploy's +ploys +pluck +pluck's +plucked +pluckier +pluckiest +pluckiness +pluckiness's +plucking +plucks +plucky +plug +plug's +plugged +plugging +plugin +plugin's +plugins +plugs +plum +plum's +plumage +plumage's +plumb +plumb's +plumbed +plumber +plumber's +plumbers +plumbing +plumbing's +plumbs +plume +plume's +plumed +plumes +pluming +plummet +plummet's +plummeted +plummeting +plummets +plump +plump's +plumped +plumper +plumpest +plumping +plumpness +plumpness's +plumps +plums +plunder +plunder's +plundered +plunderer +plunderer's +plunderers +plundering +plunders +plunge +plunge's +plunged +plunger +plunger's +plungers +plunges +plunging +plunk +plunk's +plunked +plunking +plunks +pluperfect +pluperfect's +pluperfects +plural +plural's +pluralism +pluralism's +pluralistic +pluralities +plurality +plurality's +pluralize +pluralized +pluralizes +pluralizing +plurals +plus +plus's +pluses +plush +plush's +plusher +plushest +plushier +plushiest +plushy +plusses +plutocracies +plutocracy +plutocracy's +plutocrat +plutocrat's +plutocratic +plutocrats +plutonium +plutonium's +ply +ply's +plying +plywood +plywood's +pneumatic +pneumatically +pneumonia +pneumonia's +poach +poached +poacher +poacher's +poachers +poaches +poaching +pock +pock's +pocked +pocket +pocket's +pocketbook +pocketbook's +pocketbooks +pocketed +pocketful +pocketful's +pocketfuls +pocketing +pocketknife +pocketknife's +pocketknives +pockets +pocking +pockmark +pockmark's +pockmarked +pockmarking +pockmarks +pocks +pod +pod's +podcast +podcast's +podcasting +podcasts +podded +podding +podia +podiatrist +podiatrist's +podiatrists +podiatry +podiatry's +podium +podium's +podiums +pods +poem +poem's +poems +poesy +poesy's +poet +poet's +poetess +poetess's +poetesses +poetic +poetical +poetically +poetry +poetry's +poets +pogrom +pogrom's +pogroms +poi +poi's +poignancy +poignancy's +poignant +poignantly +poinsettia +poinsettia's +poinsettias +point +point's +pointed +pointedly +pointer +pointer's +pointers +pointier +pointiest +pointillism +pointillism's +pointillist +pointillist's +pointillists +pointing +pointless +pointlessly +pointlessness +pointlessness's +points +pointy +poise +poise's +poised +poises +poising +poison +poison's +poisoned +poisoner +poisoner's +poisoners +poisoning +poisoning's +poisonings +poisonous +poisonously +poisons +poke +poke's +poked +poker +poker's +pokers +pokes +pokey +pokey's +pokeys +pokier +pokiest +poking +poky +pol +pol's +polar +polarities +polarity +polarity's +polarization +polarization's +polarize +polarized +polarizes +polarizing +pole +pole's +polecat +polecat's +polecats +poled +polemic +polemic's +polemical +polemics +poles +polestar +polestar's +polestars +police +police's +policed +policeman +policeman's +policemen +polices +policewoman +policewoman's +policewomen +policies +policing +policy +policy's +policyholder +policyholder's +policyholders +poling +polio +polio's +poliomyelitis +poliomyelitis's +polios +polish +polish's +polished +polisher +polisher's +polishers +polishes +polishing +polite +politely +politeness +politeness's +politer +politesse +politesse's +politest +politic +political +politically +politician +politician's +politicians +politicize +politicized +politicizes +politicizing +politico +politico's +politicoes +politicos +politics +politics's +polities +polity +polity's +polka +polka's +polkaed +polkaing +polkas +poll +poll's +polled +pollen +pollen's +pollinate +pollinated +pollinates +pollinating +pollination +pollination's +polling +polliwog +polliwog's +polliwogs +polls +pollster +pollster's +pollsters +pollutant +pollutant's +pollutants +pollute +polluted +polluter +polluter's +polluters +pollutes +polluting +pollution +pollution's +pollywog +pollywog's +pollywogs +polo +polo's +polonaise +polonaise's +polonaises +polonium +polonium's +pols +poltergeist +poltergeist's +poltergeists +poltroon +poltroon's +poltroons +polyamories +polyamory +polyester +polyester's +polyesters +polyethylene +polyethylene's +polygamist +polygamist's +polygamists +polygamous +polygamy +polygamy's +polyglot +polyglot's +polyglots +polygon +polygon's +polygonal +polygons +polygraph +polygraph's +polygraphed +polygraphing +polygraphs +polyhedra +polyhedron +polyhedron's +polyhedrons +polymath +polymath's +polymaths +polymer +polymer's +polymeric +polymerization +polymerization's +polymers +polymorphic +polynomial +polynomial's +polynomials +polyp +polyp's +polyphonic +polyphony +polyphony's +polyps +polystyrene +polystyrene's +polysyllabic +polysyllable +polysyllable's +polysyllables +polytechnic +polytechnic's +polytechnics +polytheism +polytheism's +polytheist +polytheist's +polytheistic +polytheists +polythene +polyunsaturated +pomade +pomade's +pomaded +pomades +pomading +pomegranate +pomegranate's +pomegranates +pommel +pommel's +pommeled +pommeling +pommelled +pommelling +pommels +pomp +pomp's +pompadour +pompadour's +pompadoured +pompadours +pompom +pompom's +pompoms +pompon +pompon's +pompons +pomposity +pomposity's +pompous +pompously +pompousness +pompousness's +poncho +poncho's +ponchos +pond +pond's +ponder +pondered +pondering +ponderous +ponderously +ponders +ponds +pone +pone's +pones +poniard +poniard's +poniards +ponies +pontiff +pontiff's +pontiffs +pontifical +pontificate +pontificate's +pontificated +pontificates +pontificating +pontoon +pontoon's +pontoons +pony +pony's +ponytail +ponytail's +ponytails +pooch +pooch's +pooched +pooches +pooching +poodle +poodle's +poodles +pooh +pooh's +poohed +poohing +poohs +pool +pool's +pooled +pooling +pools +poop +poop's +pooped +pooping +poops +poor +poorer +poorest +poorhouse +poorhouse's +poorhouses +poorly +pop +pop's +popcorn +popcorn's +pope +pope's +popes +popgun +popgun's +popguns +popinjay +popinjay's +popinjays +poplar +poplar's +poplars +poplin +poplin's +popover +popover's +popovers +poppa +poppa's +poppas +popped +poppies +popping +poppy +poppy's +poppycock +poppycock's +pops +populace +populace's +populaces +popular +popularity +popularity's +popularization +popularization's +popularize +popularized +popularizes +popularizing +popularly +populate +populated +populates +populating +population +population's +populations +populism +populism's +populist +populist's +populists +populous +porcelain +porcelain's +porch +porch's +porches +porcine +porcupine +porcupine's +porcupines +pore +pore's +pored +pores +poring +pork +pork's +porn +porn's +porno +porno's +pornographer +pornographer's +pornographers +pornographic +pornography +pornography's +porosity +porosity's +porous +porphyry +porphyry's +porpoise +porpoise's +porpoised +porpoises +porpoising +porridge +porridge's +porringer +porringer's +porringers +port +port's +portability +portability's +portable +portable's +portables +portage +portage's +portaged +portages +portaging +portal +portal's +portals +portcullis +portcullis's +portcullises +ported +portend +portended +portending +portends +portent +portent's +portentous +portentously +portents +porter +porter's +porterhouse +porterhouse's +porterhouses +porters +portfolio +portfolio's +portfolios +porthole +porthole's +portholes +portico +portico's +porticoes +porticos +porting +portion +portion's +portioned +portioning +portions +portlier +portliest +portliness +portliness's +portly +portmanteau +portmanteau's +portmanteaus +portmanteaux +portrait +portrait's +portraitist +portraitist's +portraitists +portraits +portraiture +portraiture's +portray +portrayal +portrayal's +portrayals +portrayed +portraying +portrays +ports +pose +pose's +posed +poser +poser's +posers +poses +poseur +poseur's +poseurs +posh +posher +poshest +posies +posing +posit +posited +positing +position +position's +positional +positioned +positioning +positions +positive +positive's +positively +positives +positivism +positron +positron's +positrons +posits +posse +posse's +posses +possess +possessed +possesses +possessing +possession +possession's +possessions +possessive +possessive's +possessively +possessiveness +possessiveness's +possessives +possessor +possessor's +possessors +possibilities +possibility +possibility's +possible +possible's +possibles +possibly +possum +possum's +possums +post +post's +postage +postage's +postal +postbox +postcard +postcard's +postcards +postcode +postcodes +postdate +postdated +postdates +postdating +postdoc +postdoctoral +posted +poster +poster's +posterior +posterior's +posteriors +posterity +posterity's +posters +postgraduate +postgraduate's +postgraduates +posthaste +posthumous +posthumously +posting +postlude +postlude's +postludes +postman +postman's +postmark +postmark's +postmarked +postmarking +postmarks +postmaster +postmaster's +postmasters +postmen +postmistress +postmistress's +postmistresses +postmodern +postmortem +postmortem's +postmortems +postnatal +postoperative +postpaid +postpartum +postpone +postponed +postponement +postponement's +postponements +postpones +postponing +posts +postscript +postscript's +postscripts +postulate +postulate's +postulated +postulates +postulating +posture +posture's +postured +postures +posturing +postwar +posy +posy's +pot +pot's +potable +potable's +potables +potash +potash's +potassium +potassium's +potato +potato's +potatoes +potbellied +potbellies +potbelly +potbelly's +potboiler +potboiler's +potboilers +potency +potency's +potent +potentate +potentate's +potentates +potential +potential's +potentialities +potentiality +potentiality's +potentially +potentials +potful +potful's +potfuls +potholder +potholder's +potholders +pothole +pothole's +potholes +pothook +pothook's +pothooks +potion +potion's +potions +potluck +potluck's +potlucks +potpie +potpie's +potpies +potpourri +potpourri's +potpourris +pots +potsherd +potsherd's +potsherds +potshot +potshot's +potshots +pottage +pottage's +potted +potter +potter's +pottered +potteries +pottering +potters +pottery +pottery's +pottier +potties +pottiest +potting +potty +potty's +pouch +pouch's +pouched +pouches +pouching +poultice +poultice's +poulticed +poultices +poulticing +poultry +poultry's +pounce +pounce's +pounced +pounces +pouncing +pound +pound's +pounded +pounding +pounds +pour +poured +pouring +pours +pout +pout's +pouted +pouting +pouts +poverty +poverty's +powder +powder's +powdered +powdering +powders +powdery +power +power's +powerboat +powerboat's +powerboats +powered +powerful +powerfully +powerhouse +powerhouse's +powerhouses +powering +powerless +powerlessly +powerlessness +powerlessness's +powers +powwow +powwow's +powwowed +powwowing +powwows +pox +pox's +poxes +practicability +practicability's +practicable +practicably +practical +practical's +practicalities +practicality +practicality's +practically +practicals +practice +practice's +practiced +practices +practicing +practise +practise's +practised +practises +practising +practitioner +practitioner's +practitioners +pragmatic +pragmatic's +pragmatically +pragmatics +pragmatism +pragmatism's +pragmatist +pragmatist's +pragmatists +prairie +prairie's +prairies +praise +praise's +praised +praises +praiseworthiness +praiseworthiness's +praiseworthy +praising +praline +praline's +pralines +pram +prance +prance's +pranced +prancer +prancer's +prancers +prances +prancing +prank +prank's +pranks +prankster +prankster's +pranksters +prate +prate's +prated +prates +pratfall +pratfall's +pratfalls +prating +prattle +prattle's +prattled +prattles +prattling +prawn +prawn's +prawned +prawning +prawns +pray +prayed +prayer +prayer's +prayers +praying +prays +preach +preached +preacher +preacher's +preachers +preaches +preachier +preachiest +preaching +preachy +preamble +preamble's +preambled +preambles +preambling +prearrange +prearranged +prearrangement +prearrangement's +prearranges +prearranging +precarious +precariously +precaution +precaution's +precautionary +precautions +precede +preceded +precedence +precedence's +precedent +precedent's +precedents +precedes +preceding +precept +precept's +preceptor +preceptor's +preceptors +precepts +precinct +precinct's +precincts +preciosity +preciosity's +precious +preciously +preciousness +preciousness's +precipice +precipice's +precipices +precipitant +precipitant's +precipitants +precipitate +precipitate's +precipitated +precipitately +precipitates +precipitating +precipitation +precipitation's +precipitations +precipitous +precipitously +precise +precisely +preciseness +preciseness's +preciser +precises +precisest +precision +precision's +preclude +precluded +precludes +precluding +preclusion +preclusion's +precocious +precociously +precociousness +precociousness's +precocity +precocity's +precognition +preconceive +preconceived +preconceives +preconceiving +preconception +preconception's +preconceptions +precondition +precondition's +preconditioned +preconditioning +preconditions +precursor +precursor's +precursors +predate +predated +predates +predating +predator +predator's +predators +predatory +predecease +predeceased +predeceases +predeceasing +predecessor +predecessor's +predecessors +predefined +predestination +predestination's +predestine +predestined +predestines +predestining +predetermination +predetermination's +predetermine +predetermined +predetermines +predetermining +predicament +predicament's +predicaments +predicate +predicate's +predicated +predicates +predicating +predication +predication's +predicative +predict +predictability +predictable +predictably +predicted +predicting +prediction +prediction's +predictions +predictive +predictor +predicts +predilection +predilection's +predilections +predispose +predisposed +predisposes +predisposing +predisposition +predisposition's +predispositions +predominance +predominance's +predominant +predominantly +predominate +predominated +predominates +predominating +preeminence +preeminence's +preeminent +preeminently +preempt +preempted +preempting +preemption +preemption's +preemptive +preemptively +preempts +preen +preened +preening +preens +preexist +preexisted +preexisting +preexists +prefab +prefab's +prefabbed +prefabbing +prefabricate +prefabricated +prefabricates +prefabricating +prefabrication +prefabrication's +prefabs +preface +preface's +prefaced +prefaces +prefacing +prefatory +prefect +prefect's +prefects +prefecture +prefecture's +prefectures +prefer +preferable +preferably +preference +preference's +preferences +preferential +preferentially +preferment +preferment's +preferred +preferring +prefers +prefigure +prefigured +prefigures +prefiguring +prefix +prefix's +prefixed +prefixes +prefixing +pregnancies +pregnancy +pregnancy's +pregnant +preheat +preheated +preheating +preheats +prehensile +prehistoric +prehistory +prehistory's +prejudge +prejudged +prejudges +prejudging +prejudgment +prejudgment's +prejudgments +prejudice +prejudice's +prejudiced +prejudices +prejudicial +prejudicing +prelate +prelate's +prelates +preliminaries +preliminary +preliminary's +prelude +prelude's +preludes +premarital +premature +prematurely +premeditate +premeditated +premeditates +premeditating +premeditation +premeditation's +premenstrual +premier +premier's +premiere +premiere's +premiered +premieres +premiering +premiers +premise +premise's +premised +premises +premising +premiss +premiss's +premisses +premium +premium's +premiums +premonition +premonition's +premonitions +premonitory +prenatal +prenup +prenup's +prenups +preoccupation +preoccupation's +preoccupations +preoccupied +preoccupies +preoccupy +preoccupying +preordain +preordained +preordaining +preordains +prep +prep's +prepackage +prepackaged +prepackages +prepackaging +prepaid +preparation +preparation's +preparations +preparatory +prepare +prepared +preparedness +preparedness's +prepares +preparing +prepay +prepaying +prepayment +prepayment's +prepayments +prepays +preponderance +preponderance's +preponderances +preponderant +preponderate +preponderated +preponderates +preponderating +preposition +preposition's +prepositional +prepositions +prepossess +prepossessed +prepossesses +prepossessing +preposterous +preposterously +prepped +preppie +preppie's +preppier +preppies +preppiest +prepping +preppy +preppy's +preps +prequel +prequel's +prequels +prerecord +prerecorded +prerecording +prerecords +preregister +preregistered +preregistering +preregisters +preregistration +preregistration's +prerequisite +prerequisite's +prerequisites +prerogative +prerogative's +prerogatives +presage +presage's +presaged +presages +presaging +preschool +preschool's +preschooler +preschooler's +preschoolers +preschools +prescience +prescience's +prescient +prescribe +prescribed +prescribes +prescribing +prescription +prescription's +prescriptions +prescriptive +presence +presence's +presences +present +present's +presentable +presentation +presentation's +presentations +presented +presenter +presentiment +presentiment's +presentiments +presenting +presently +presents +preservation +preservation's +preservative +preservative's +preservatives +preserve +preserve's +preserved +preserver +preserver's +preservers +preserves +preserving +preset +presets +presetting +preshrank +preshrink +preshrinking +preshrinks +preshrunk +preshrunken +preside +presided +presidencies +presidency +presidency's +president +president's +presidential +presidents +presides +presiding +press +press's +pressed +presses +pressing +pressing's +pressings +pressman +pressman's +pressmen +pressure +pressure's +pressured +pressures +pressuring +pressurization +pressurization's +pressurize +pressurized +pressurizes +pressurizing +prestige +prestige's +prestigious +presto +presto's +prestos +presumable +presumably +presume +presumed +presumes +presuming +presumption +presumption's +presumptions +presumptive +presumptuous +presumptuously +presumptuousness +presumptuousness's +presuppose +presupposed +presupposes +presupposing +presupposition +presupposition's +presuppositions +preteen +preteen's +preteens +pretence +pretence's +pretences +pretend +pretended +pretender +pretender's +pretenders +pretending +pretends +pretense +pretense's +pretenses +pretension +pretension's +pretensions +pretentious +pretentiously +pretentiousness +pretentiousness's +preterit +preterit's +preterite +preterite's +preterites +preterits +preternatural +pretext +pretext's +pretexts +prettied +prettier +pretties +prettiest +prettified +prettifies +prettify +prettifying +prettily +prettiness +prettiness's +pretty +pretty's +prettying +pretzel +pretzel's +pretzels +prevail +prevailed +prevailing +prevails +prevalence +prevalence's +prevalent +prevaricate +prevaricated +prevaricates +prevaricating +prevarication +prevarication's +prevarications +prevaricator +prevaricator's +prevaricators +prevent +preventable +preventative +preventative's +preventatives +prevented +preventible +preventing +prevention +prevention's +preventive +preventive's +preventives +prevents +preview +preview's +previewed +previewer +previewers +previewing +previews +previous +previously +prevue +prevue's +prevues +prewar +prey +prey's +preyed +preying +preys +price +price's +priced +priceless +prices +pricey +pricier +priciest +pricing +prick +prick's +pricked +pricking +prickle +prickle's +prickled +prickles +pricklier +prickliest +prickling +prickly +pricks +pricy +pride +pride's +prided +prides +priding +pried +pries +priest +priest's +priestess +priestess's +priestesses +priesthood +priesthood's +priesthoods +priestlier +priestliest +priestly +priests +prig +prig's +priggish +prigs +prim +primacy +primacy's +primaeval +primal +primaries +primarily +primary +primary's +primate +primate's +primates +prime +prime's +primed +primer +primer's +primers +primes +primeval +priming +primitive +primitive's +primitively +primitives +primly +primmer +primmest +primness +primness's +primogeniture +primogeniture's +primordial +primp +primped +primping +primps +primrose +primrose's +primroses +prince +prince's +princelier +princeliest +princely +princes +princess +princess's +princesses +principal +principal's +principalities +principality +principality's +principally +principals +principle +principle's +principled +principles +print +print's +printable +printed +printer +printer's +printers +printing +printing's +printings +printout +printout's +printouts +prints +prior +prior's +prioress +prioress's +prioresses +priories +priorities +prioritize +prioritized +prioritizes +prioritizing +priority +priority's +priors +priory +priory's +prism +prism's +prismatic +prisms +prison +prison's +prisoner +prisoner's +prisoners +prisons +prissier +prissiest +prissiness +prissiness's +prissy +pristine +prithee +privacy +privacy's +private +private's +privateer +privateer's +privateers +privately +privater +privates +privatest +privation +privation's +privations +privatization +privatization's +privatizations +privatize +privatized +privatizes +privatizing +privet +privet's +privets +privier +privies +priviest +privilege +privilege's +privileged +privileges +privileging +privy +privy's +prize +prize's +prized +prizefight +prizefight's +prizefighter +prizefighter's +prizefighters +prizefighting +prizefights +prizes +prizing +pro +pro's +proactive +probabilistic +probabilities +probability +probability's +probable +probable's +probables +probably +probate +probate's +probated +probates +probating +probation +probation's +probationary +probationer +probationer's +probationers +probe +probe's +probed +probes +probing +probity +probity's +problem +problem's +problematic +problematical +problematically +problems +proboscides +proboscis +proboscis's +proboscises +procedural +procedure +procedure's +procedures +proceed +proceeded +proceeding +proceeding's +proceedings +proceeds +proceeds's +process +process's +processed +processes +processing +procession +procession's +processional +processional's +processionals +processioned +processioning +processions +processor +processor's +processors +proclaim +proclaimed +proclaiming +proclaims +proclamation +proclamation's +proclamations +proclivities +proclivity +proclivity's +procrastinate +procrastinated +procrastinates +procrastinating +procrastination +procrastination's +procrastinator +procrastinator's +procrastinators +procreate +procreated +procreates +procreating +procreation +procreation's +procreative +proctor +proctor's +proctored +proctoring +proctors +procurator +procurator's +procurators +procure +procured +procurement +procurement's +procurer +procurer's +procurers +procures +procuring +prod +prod's +prodded +prodding +prodigal +prodigal's +prodigality +prodigality's +prodigals +prodigies +prodigious +prodigiously +prodigy +prodigy's +prods +produce +produce's +produced +producer +producer's +producers +produces +producing +product +product's +production +production's +productions +productive +productively +productiveness +productiveness's +productivity +productivity's +products +prof +prof's +profanation +profanation's +profanations +profane +profaned +profanely +profanes +profaning +profanities +profanity +profanity's +profess +professed +professes +professing +profession +profession's +professional +professional's +professionalism +professionalism's +professionally +professionals +professions +professor +professor's +professorial +professors +professorship +professorship's +professorships +proffer +proffer's +proffered +proffering +proffers +proficiency +proficiency's +proficient +proficient's +proficiently +proficients +profile +profile's +profiled +profiles +profiling +profit +profit's +profitability +profitability's +profitable +profitably +profited +profiteer +profiteer's +profiteered +profiteering +profiteers +profiting +profits +profligacy +profligacy's +profligate +profligate's +profligates +proforma +profound +profounder +profoundest +profoundly +profs +profundities +profundity +profundity's +profuse +profusely +profusion +profusion's +profusions +progenitor +progenitor's +progenitors +progeny +progeny's +progesterone +progesterone's +prognoses +prognosis +prognosis's +prognostic +prognostic's +prognosticate +prognosticated +prognosticates +prognosticating +prognostication +prognostication's +prognostications +prognosticator +prognosticator's +prognosticators +prognostics +program +program's +programed +programer +programer's +programers +programing +programmable +programmable's +programmables +programme +programmed +programmer +programmer's +programmers +programmes +programming +programming's +programs +progress +progress's +progressed +progresses +progressing +progression +progression's +progressions +progressive +progressive's +progressively +progressives +prohibit +prohibited +prohibiting +prohibition +prohibition's +prohibitionist +prohibitionist's +prohibitionists +prohibitions +prohibitive +prohibitively +prohibitory +prohibits +project +project's +projected +projectile +projectile's +projectiles +projecting +projection +projection's +projectionist +projectionist's +projectionists +projections +projector +projector's +projectors +projects +proletarian +proletarian's +proletarians +proletariat +proletariat's +proliferate +proliferated +proliferates +proliferating +proliferation +proliferation's +prolific +prolifically +prolix +prolixity +prolixity's +prolog +prolog's +prologs +prologue +prologue's +prologues +prolong +prolongation +prolongation's +prolongations +prolonged +prolonging +prolongs +prom +prom's +promenade +promenade's +promenaded +promenades +promenading +prominence +prominence's +prominent +prominently +promiscuity +promiscuity's +promiscuous +promiscuously +promise +promise's +promised +promises +promising +promisingly +promissory +promo +promo's +promontories +promontory +promontory's +promos +promote +promoted +promoter +promoter's +promoters +promotes +promoting +promotion +promotion's +promotional +promotions +prompt +prompt's +prompted +prompter +prompter's +prompters +promptest +prompting +prompting's +promptings +promptly +promptness +promptness's +prompts +proms +promulgate +promulgated +promulgates +promulgating +promulgation +promulgation's +prone +proneness +proneness's +prong +prong's +pronged +pronghorn +pronghorn's +pronghorns +prongs +pronoun +pronoun's +pronounce +pronounceable +pronounced +pronouncement +pronouncement's +pronouncements +pronounces +pronouncing +pronouns +pronto +pronunciation +pronunciation's +pronunciations +proof +proof's +proofed +proofing +proofread +proofreader +proofreader's +proofreaders +proofreading +proofreads +proofs +prop +prop's +propaganda +propaganda's +propagandist +propagandist's +propagandists +propagandize +propagandized +propagandizes +propagandizing +propagate +propagated +propagates +propagating +propagation +propagation's +propane +propane's +propel +propellant +propellant's +propellants +propelled +propellent +propellent's +propellents +propeller +propeller's +propellers +propelling +propels +propensities +propensity +propensity's +proper +proper's +properer +properest +properly +propertied +properties +property +property's +prophecies +prophecy +prophecy's +prophesied +prophesies +prophesy +prophesy's +prophesying +prophet +prophet's +prophetess +prophetess's +prophetesses +prophetic +prophetically +prophets +prophylactic +prophylactic's +prophylactics +prophylaxis +prophylaxis's +propinquity +propinquity's +propitiate +propitiated +propitiates +propitiating +propitiation +propitiation's +propitiatory +propitious +proponent +proponent's +proponents +proportion +proportion's +proportional +proportionality +proportionally +proportionals +proportionate +proportionately +proportioned +proportioning +proportions +proposal +proposal's +proposals +propose +proposed +proposer +proposes +proposing +proposition +proposition's +propositional +propositioned +propositioning +propositions +propound +propounded +propounding +propounds +propped +propping +proprietaries +proprietary +proprietary's +proprietor +proprietor's +proprietors +proprietorship +proprietorship's +proprietress +proprietress's +proprietresses +propriety +propriety's +props +propulsion +propulsion's +propulsive +prorate +prorated +prorates +prorating +pros +prosaic +prosaically +proscenia +proscenium +proscenium's +prosceniums +proscribe +proscribed +proscribes +proscribing +proscription +proscription's +proscriptions +prose +prose's +prosecute +prosecuted +prosecutes +prosecuting +prosecution +prosecution's +prosecutions +prosecutor +prosecutor's +prosecutors +proselyte +proselyte's +proselyted +proselytes +proselyting +proselytize +proselytized +proselytizes +proselytizing +prosier +prosiest +prosodies +prosody +prosody's +prospect +prospect's +prospected +prospecting +prospective +prospector +prospector's +prospectors +prospects +prospectus +prospectus's +prospectuses +prosper +prospered +prospering +prosperity +prosperity's +prosperous +prosperously +prospers +prostate +prostate's +prostates +prostheses +prosthesis +prosthesis's +prosthetic +prostitute +prostitute's +prostituted +prostitutes +prostituting +prostitution +prostitution's +prostrate +prostrated +prostrates +prostrating +prostration +prostration's +prostrations +prosy +protagonist +protagonist's +protagonists +protean +protect +protected +protecting +protection +protection's +protections +protective +protectively +protectiveness +protectiveness's +protector +protector's +protectorate +protectorate's +protectorates +protectors +protects +protein +protein's +proteins +protest +protest's +protestant +protestants +protestation +protestation's +protestations +protested +protester +protester's +protesters +protesting +protestor +protestor's +protestors +protests +protocol +protocol's +protocols +proton +proton's +protons +protoplasm +protoplasm's +protoplasmic +prototype +prototype's +prototypes +prototyping +protozoa +protozoan +protozoan's +protozoans +protozoon +protozoon's +protract +protracted +protracting +protraction +protraction's +protractor +protractor's +protractors +protracts +protrude +protruded +protrudes +protruding +protrusion +protrusion's +protrusions +protuberance +protuberance's +protuberances +protuberant +protégé +protégé's +protégés +proud +prouder +proudest +proudly +provable +provably +prove +proved +proven +provenance +provenance's +provender +provender's +proverb +proverb's +proverbial +proverbially +proverbs +proves +provide +provided +providence +providence's +provident +providential +providentially +providently +provider +provider's +providers +provides +providing +province +province's +provinces +provincial +provincial's +provincialism +provincialism's +provincials +proving +provision +provision's +provisional +provisionally +provisioned +provisioning +provisions +proviso +proviso's +provisoes +provisos +provocation +provocation's +provocations +provocative +provocatively +provoke +provoked +provokes +provoking +provost +provost's +provosts +prow +prow's +prowess +prowess's +prowl +prowl's +prowled +prowler +prowler's +prowlers +prowling +prowls +prows +proxies +proximity +proximity's +proxy +proxy's +prude +prude's +prudence +prudence's +prudent +prudential +prudently +prudery +prudery's +prudes +prudish +prudishly +prune +prune's +pruned +prunes +pruning +prurience +prurience's +prurient +pry +pry's +prying +précis +précis's +précised +précising +psalm +psalm's +psalmist +psalmist's +psalmists +psalms +pseudo +pseudonym +pseudonym's +pseudonyms +pshaw +pshaw's +pshaws +psoriasis +psoriasis's +psst +psych +psych's +psyche +psyche's +psyched +psychedelic +psychedelic's +psychedelics +psyches +psychiatric +psychiatrist +psychiatrist's +psychiatrists +psychiatry +psychiatry's +psychic +psychic's +psychical +psychically +psychics +psyching +psycho +psycho's +psychoanalysis +psychoanalysis's +psychoanalyst +psychoanalyst's +psychoanalysts +psychoanalyze +psychoanalyzed +psychoanalyzes +psychoanalyzing +psychobabble +psychobabble's +psychogenic +psychokinesis +psychological +psychologically +psychologies +psychologist +psychologist's +psychologists +psychology +psychology's +psychopath +psychopath's +psychopathic +psychopaths +psychos +psychoses +psychosis +psychosis's +psychosomatic +psychotherapies +psychotherapist +psychotherapist's +psychotherapists +psychotherapy +psychotherapy's +psychotic +psychotic's +psychotics +psychs +ptarmigan +ptarmigan's +ptarmigans +pterodactyl +pterodactyl's +pterodactyls +ptomaine +ptomaine's +ptomaines +pub +pub's +puberty +puberty's +pubescence +pubescence's +pubescent +pubic +public +public's +publican +publican's +publicans +publication +publication's +publications +publicist +publicist's +publicists +publicity +publicity's +publicize +publicized +publicizes +publicizing +publicly +publish +publishable +published +publisher +publisher's +publishers +publishes +publishing +publishing's +pubs +puck +puck's +pucker +pucker's +puckered +puckering +puckers +puckish +pucks +pudding +pudding's +puddings +puddle +puddle's +puddled +puddles +puddling +pudgier +pudgiest +pudgy +pueblo +pueblo's +pueblos +puerile +puerility +puerility's +puff +puff's +puffball +puffball's +puffballs +puffed +puffer +puffier +puffiest +puffin +puffin's +puffiness +puffiness's +puffing +puffins +puffs +puffy +pug +pug's +pugilism +pugilism's +pugilist +pugilist's +pugilistic +pugilists +pugnacious +pugnaciously +pugnacity +pugnacity's +pugs +puke +puke's +puked +pukes +puking +pulchritude +pulchritude's +pull +pull's +pullback +pullback's +pullbacks +pulled +puller +puller's +pullers +pullet +pullet's +pullets +pulley +pulley's +pulleys +pulling +pullout +pullout's +pullouts +pullover +pullover's +pullovers +pulls +pulmonary +pulp +pulp's +pulped +pulpier +pulpiest +pulping +pulpit +pulpit's +pulpits +pulps +pulpy +pulsar +pulsar's +pulsars +pulsate +pulsated +pulsates +pulsating +pulsation +pulsation's +pulsations +pulse +pulse's +pulsed +pulses +pulsing +pulverization +pulverization's +pulverize +pulverized +pulverizes +pulverizing +puma +puma's +pumas +pumice +pumice's +pumices +pummel +pummeled +pummeling +pummelled +pummelling +pummels +pump +pump's +pumped +pumper +pumper's +pumpernickel +pumpernickel's +pumpers +pumping +pumpkin +pumpkin's +pumpkins +pumps +pun +pun's +punch +punch's +punched +punches +punchier +punchiest +punching +punchline +punchy +punctilious +punctiliously +punctual +punctuality +punctuality's +punctually +punctuate +punctuated +punctuates +punctuating +punctuation +punctuation's +puncture +puncture's +punctured +punctures +puncturing +pundit +pundit's +pundits +pungency +pungency's +pungent +pungently +punier +puniest +punish +punishable +punished +punishes +punishing +punishment +punishment's +punishments +punitive +punk +punk's +punker +punkest +punks +punned +punning +puns +punster +punster's +punsters +punt +punt's +punted +punter +punter's +punters +punting +punts +puny +pup +pup's +pupa +pupa's +pupae +pupal +pupas +pupil +pupil's +pupils +pupped +puppet +puppet's +puppeteer +puppeteer's +puppeteers +puppetry +puppetry's +puppets +puppies +pupping +puppy +puppy's +pups +purblind +purchasable +purchase +purchase's +purchased +purchaser +purchaser's +purchasers +purchases +purchasing +pure +purebred +purebred's +purebreds +puree +puree's +pureed +pureeing +purees +purely +pureness +pureness's +purer +purest +purgative +purgative's +purgatives +purgatorial +purgatories +purgatory +purgatory's +purge +purge's +purged +purges +purging +purification +purification's +purified +purifier +purifier's +purifiers +purifies +purify +purifying +purism +purism's +purist +purist's +purists +puritan +puritan's +puritanical +puritanically +puritanism +puritanism's +puritans +purity +purity's +purl +purl's +purled +purling +purloin +purloined +purloining +purloins +purls +purple +purple's +purpler +purples +purplest +purplish +purport +purport's +purported +purportedly +purporting +purports +purpose +purpose's +purposed +purposeful +purposefully +purposeless +purposely +purposes +purposing +purr +purr's +purred +purring +purrs +purse +purse's +pursed +purser +purser's +pursers +purses +pursing +pursuance +pursuance's +pursuant +pursue +pursued +pursuer +pursuer's +pursuers +pursues +pursuing +pursuit +pursuit's +pursuits +purulence +purulence's +purulent +purvey +purveyed +purveying +purveyor +purveyor's +purveyors +purveys +purview +purview's +pus +pus's +push +push's +pushcart +pushcart's +pushcarts +pushed +pusher +pusher's +pushers +pushes +pushier +pushiest +pushiness +pushiness's +pushing +pushover +pushover's +pushovers +pushup +pushup's +pushups +pushy +pusillanimity +pusillanimity's +pusillanimous +puss +puss's +pusses +pussier +pussies +pussiest +pussy +pussy's +pussycat +pussycat's +pussycats +pussyfoot +pussyfooted +pussyfooting +pussyfoots +pustule +pustule's +pustules +put +put's +putative +putrefaction +putrefaction's +putrefied +putrefies +putrefy +putrefying +putrescence +putrescence's +putrescent +putrid +puts +putsch +putsch's +putsches +putt +putt's +putted +putter +putter's +puttered +puttering +putters +puttied +putties +putting +putts +putty +putty's +puttying +puzzle +puzzle's +puzzled +puzzlement +puzzlement's +puzzler +puzzler's +puzzlers +puzzles +puzzling +pwn +pwned +pwning +pwns +pygmies +pygmy +pygmy's +pylon +pylon's +pylons +pyorrhea +pyorrhea's +pyramid +pyramid's +pyramidal +pyramided +pyramiding +pyramids +pyre +pyre's +pyres +pyrite +pyrite's +pyromania +pyromania's +pyromaniac +pyromaniac's +pyromaniacs +pyrotechnic +pyrotechnics +pyrotechnics's +python +python's +pythons +pyx +pyx's +pyxes +q +qua +quack +quack's +quacked +quackery +quackery's +quacking +quacks +quad +quad's +quadrangle +quadrangle's +quadrangles +quadrangular +quadrant +quadrant's +quadrants +quadraphonic +quadratic +quadrature +quadrennial +quadriceps +quadriceps's +quadricepses +quadrilateral +quadrilateral's +quadrilaterals +quadrille +quadrille's +quadrilles +quadriphonic +quadriplegia +quadriplegia's +quadriplegic +quadriplegic's +quadriplegics +quadruped +quadruped's +quadrupeds +quadruple +quadruple's +quadrupled +quadruples +quadruplet +quadruplet's +quadruplets +quadruplicate +quadruplicate's +quadruplicated +quadruplicates +quadruplicating +quadrupling +quads +quaff +quaff's +quaffed +quaffing +quaffs +quagmire +quagmire's +quagmires +quahaug +quahaug's +quahaugs +quahog +quahog's +quahogs +quail +quail's +quailed +quailing +quails +quaint +quainter +quaintest +quaintly +quaintness +quaintness's +quake +quake's +quaked +quakes +quaking +qualification +qualification's +qualifications +qualified +qualifier +qualifier's +qualifiers +qualifies +qualify +qualifying +qualitative +qualitatively +qualities +quality +quality's +qualm +qualm's +qualms +quandaries +quandary +quandary's +quanta +quantified +quantifier +quantifier's +quantifiers +quantifies +quantify +quantifying +quantitative +quantities +quantity +quantity's +quantum +quantum's +quarantine +quarantine's +quarantined +quarantines +quarantining +quark +quark's +quarks +quarrel +quarrel's +quarreled +quarreling +quarrelled +quarrelling +quarrels +quarrelsome +quarried +quarries +quarry +quarry's +quarrying +quart +quart's +quarter +quarter's +quarterback +quarterback's +quarterbacked +quarterbacking +quarterbacks +quarterdeck +quarterdeck's +quarterdecks +quartered +quarterfinal +quarterfinal's +quarterfinals +quartering +quarterlies +quarterly +quarterly's +quartermaster +quartermaster's +quartermasters +quarters +quartet +quartet's +quartets +quartette +quartette's +quartettes +quarto +quarto's +quartos +quarts +quartz +quartz's +quasar +quasar's +quasars +quash +quashed +quashes +quashing +quasi +quatrain +quatrain's +quatrains +quaver +quaver's +quavered +quavering +quavers +quavery +quay +quay's +quays +queasier +queasiest +queasily +queasiness +queasiness's +queasy +queen +queen's +queened +queening +queenlier +queenliest +queenly +queens +queer +queer's +queered +queerer +queerest +queering +queerly +queerness +queerness's +queers +quell +quelled +quelling +quells +quench +quenched +quenches +quenching +queried +queries +querulous +querulously +query +query's +querying +quesadilla +quesadilla's +quesadillas +quest +quest's +quested +questing +question +question's +questionable +questionably +questioned +questioner +questioner's +questioners +questioning +questioningly +questionnaire +questionnaire's +questionnaires +questions +quests +queue +queue's +queued +queues +queuing +quibble +quibble's +quibbled +quibbler +quibbler's +quibblers +quibbles +quibbling +quiche +quiche's +quiches +quick +quick's +quicken +quickened +quickening +quickens +quicker +quickest +quickie +quickie's +quickies +quicklime +quicklime's +quickly +quickness +quickness's +quicksand +quicksand's +quicksands +quicksilver +quicksilver's +quid +quid's +quids +quiescence +quiescence's +quiescent +quiet +quiet's +quieted +quieter +quietest +quieting +quietly +quietness +quietness's +quiets +quietude +quietude's +quietus +quietus's +quietuses +quill +quill's +quills +quilt +quilt's +quilted +quilter +quilter's +quilters +quilting +quilting's +quilts +quince +quince's +quinces +quinine +quinine's +quintessence +quintessence's +quintessences +quintessential +quintet +quintet's +quintets +quintuple +quintuple's +quintupled +quintuples +quintuplet +quintuplet's +quintuplets +quintupling +quip +quip's +quipped +quipping +quips +quire +quire's +quires +quirk +quirk's +quirked +quirkier +quirkiest +quirking +quirks +quirky +quisling +quisling's +quislings +quit +quite +quits +quitted +quitter +quitter's +quitters +quitting +quiver +quiver's +quivered +quivering +quivers +quixotic +quiz +quiz's +quizzed +quizzes +quizzical +quizzically +quizzing +quoit +quoit's +quoited +quoiting +quoits +quondam +quorum +quorum's +quorums +quota +quota's +quotable +quotas +quotation +quotation's +quotations +quote +quote's +quoted +quotes +quoth +quotidian +quotient +quotient's +quotients +quoting +r +rabbi +rabbi's +rabbinate +rabbinate's +rabbinical +rabbis +rabbit +rabbit's +rabbited +rabbiting +rabbits +rabble +rabble's +rabbles +rabid +rabies +rabies's +raccoon +raccoon's +raccoons +race +race's +racecourse +racecourse's +racecourses +raced +racehorse +racehorse's +racehorses +raceme +raceme's +racemes +racer +racer's +racers +races +racetrack +racetrack's +racetracks +raceway +raceway's +raceways +racial +racially +racier +raciest +racily +raciness +raciness's +racing +racing's +racism +racism's +racist +racist's +racists +rack +rack's +racked +racket +racket's +racketed +racketeer +racketeer's +racketeered +racketeering +racketeering's +racketeers +racketing +rackets +racking +racks +raconteur +raconteur's +raconteurs +racoon +racoon's +racoons +racquet +racquet's +racquetball +racquetball's +racquetballs +racquets +racy +radar +radar's +radars +radial +radial's +radially +radials +radiance +radiance's +radiant +radiantly +radiate +radiated +radiates +radiating +radiation +radiation's +radiations +radiator +radiator's +radiators +radical +radical's +radicalism +radicalism's +radically +radicals +radii +radio +radio's +radioactive +radioactivity +radioactivity's +radioed +radiogram +radiogram's +radiograms +radioing +radioisotope +radioisotope's +radioisotopes +radiologist +radiologist's +radiologists +radiology +radiology's +radios +radiotelephone +radiotelephone's +radiotelephones +radiotherapist +radiotherapist's +radiotherapists +radiotherapy +radiotherapy's +radish +radish's +radishes +radium +radium's +radius +radius's +radiuses +radon +radon's +raffia +raffia's +raffish +raffle +raffle's +raffled +raffles +raffling +raft +raft's +rafted +rafter +rafter's +rafters +rafting +rafts +rag +rag's +raga +raga's +ragamuffin +ragamuffin's +ragamuffins +ragas +rage +rage's +raged +rages +ragged +raggeder +raggedest +raggedier +raggediest +raggedly +raggedness +raggedness's +raggedy +ragging +raging +raglan +raglan's +raglans +ragout +ragout's +ragouts +rags +ragtag +ragtags +ragtime +ragtime's +ragweed +ragweed's +raid +raid's +raided +raider +raider's +raiders +raiding +raids +rail +rail's +railed +railing +railing's +railings +railleries +raillery +raillery's +railroad +railroad's +railroaded +railroading +railroads +rails +railway +railway's +railways +raiment +raiment's +rain +rain's +rainbow +rainbow's +rainbows +raincoat +raincoat's +raincoats +raindrop +raindrop's +raindrops +rained +rainfall +rainfall's +rainfalls +rainforest +rainier +rainiest +raining +rainmaker +rainmaker's +rainmakers +rains +rainstorm +rainstorm's +rainstorms +rainwater +rainwater's +rainy +raise +raise's +raised +raises +raisin +raisin's +raising +raisins +raja +raja's +rajah +rajah's +rajahs +rajas +rake +rake's +raked +rakes +raking +rakish +rakishly +rakishness +rakishness's +rallied +rallies +rally +rally's +rallying +ram +ram's +ramble +ramble's +rambled +rambler +rambler's +ramblers +rambles +rambling +rambunctious +rambunctiousness +rambunctiousness's +ramification +ramification's +ramifications +ramified +ramifies +ramify +ramifying +rammed +ramming +ramp +ramp's +rampage +rampage's +rampaged +rampages +rampaging +rampant +rampantly +rampart +rampart's +ramparts +ramps +ramrod +ramrod's +ramrodded +ramrodding +ramrods +rams +ramshackle +ran +ranch +ranch's +ranched +rancher +rancher's +ranchers +ranches +ranching +ranching's +rancid +rancidity +rancidity's +rancor +rancor's +rancorous +rancorously +randier +randiest +random +randomize +randomized +randomizes +randomizing +randomly +randomness +randomness's +randy +rang +range +range's +ranged +ranger +ranger's +rangers +ranges +rangier +rangiest +ranginess +ranginess's +ranging +rangy +rank +rank's +ranked +ranker +rankest +ranking +ranking's +rankings +rankle +rankled +rankles +rankling +rankness +rankness's +ranks +ransack +ransacked +ransacking +ransacks +ransom +ransom's +ransomed +ransoming +ransoms +rant +rant's +ranted +ranter +ranting +rants +rap +rap's +rapacious +rapaciously +rapaciousness +rapaciousness's +rapacity +rapacity's +rape +rape's +raped +rapes +rapid +rapid's +rapider +rapidest +rapidity +rapidity's +rapidly +rapids +rapier +rapier's +rapiers +rapine +rapine's +raping +rapist +rapist's +rapists +rapped +rapper +rapper's +rappers +rapping +rapport +rapport's +rapports +rapprochement +rapprochement's +rapprochements +raps +rapscallion +rapscallion's +rapscallions +rapt +rapture +rapture's +raptures +rapturous +rare +rared +rarefied +rarefies +rarefy +rarefying +rarely +rareness +rareness's +rarer +rares +rarest +raring +rarities +rarity +rarity's +rascal +rascal's +rascally +rascals +rash +rash's +rasher +rasher's +rashers +rashes +rashest +rashly +rashness +rashness's +rasp +rasp's +raspberries +raspberry +raspberry's +rasped +raspier +raspiest +rasping +rasps +raspy +raster +rat +rat's +ratchet +ratchet's +ratcheted +ratcheting +ratchets +rate +rate's +rated +rates +rather +rathskeller +rathskeller's +rathskellers +ratification +ratification's +ratified +ratifies +ratify +ratifying +rating +rating's +ratings +ratio +ratio's +ration +ration's +rational +rational's +rationale +rationale's +rationales +rationalism +rationalism's +rationalist +rationalist's +rationalistic +rationalists +rationality +rationality's +rationalization +rationalization's +rationalizations +rationalize +rationalized +rationalizes +rationalizing +rationally +rationals +rationed +rationing +rations +ratios +rats +rattan +rattan's +rattans +ratted +rattier +rattiest +ratting +rattle +rattle's +rattled +rattler +rattler's +rattlers +rattles +rattlesnake +rattlesnake's +rattlesnakes +rattletrap +rattletrap's +rattletraps +rattling +rattlings +rattrap +rattrap's +rattraps +ratty +raucous +raucously +raucousness +raucousness's +raunchier +raunchiest +raunchiness +raunchiness's +raunchy +ravage +ravage's +ravaged +ravages +ravaging +rave +rave's +raved +ravel +ravel's +raveled +raveling +ravelled +ravelling +ravels +raven +raven's +ravened +ravening +ravenous +ravenously +ravens +raves +ravine +ravine's +ravines +raving +raving's +ravings +ravioli +ravioli's +raviolis +ravish +ravished +ravishes +ravishing +ravishingly +ravishment +ravishment's +raw +raw's +rawboned +rawer +rawest +rawhide +rawhide's +rawness +rawness's +ray +ray's +rayon +rayon's +rays +raze +razed +razes +razing +razor +razor's +razors +razz +razz's +razzed +razzes +razzing +re +re's +reach +reach's +reachable +reached +reaches +reaching +react +reacted +reacting +reaction +reaction's +reactionaries +reactionary +reactionary's +reactions +reactivate +reactivated +reactivates +reactivating +reactivation +reactivation's +reactive +reactor +reactor's +reactors +reacts +read +read's +readabilities +readability +readability's +readable +reader +reader's +readers +readership +readership's +readerships +readied +readier +readies +readiest +readily +readiness +readiness's +reading +reading's +readings +readjust +readjusted +readjusting +readjustment +readjustment's +readjustments +readjusts +readmit +readmits +readmitted +readmitting +readout +readout's +readouts +reads +ready +readying +reaffirm +reaffirmed +reaffirming +reaffirms +reagent +reagent's +reagents +real +real's +realer +reales +realest +realign +realism +realism's +realist +realist's +realistic +realistically +realists +realities +reality +reality's +realizable +realization +realization's +realize +realized +realizes +realizing +reallocate +reallocated +reallocates +reallocating +reallocation +really +realm +realm's +realms +reals +realtor +realtor's +realtors +realty +realty's +ream +ream's +reamed +reamer +reamer's +reamers +reaming +reams +reanimate +reanimated +reanimates +reanimating +reap +reaped +reaper +reaper's +reapers +reaping +reappear +reappearance +reappearance's +reappearances +reappeared +reappearing +reappears +reapplied +reapplies +reapply +reapplying +reappoint +reappointed +reappointing +reappointment +reappointment's +reappoints +reapportion +reapportioned +reapportioning +reapportionment +reapportionment's +reapportions +reappraisal +reappraisal's +reappraisals +reappraise +reappraised +reappraises +reappraising +reaps +rear +rear's +reared +rearing +rearm +rearmament +rearmament's +rearmed +rearming +rearmost +rearms +rearrange +rearranged +rearrangement +rearrangement's +rearrangements +rearranges +rearranging +rears +rearward +rearwards +reason +reason's +reasonable +reasonableness +reasonableness's +reasonably +reasoned +reasoning +reasoning's +reasons +reassemble +reassembled +reassembles +reassembling +reassert +reasserted +reasserting +reasserts +reassess +reassessed +reassesses +reassessing +reassessment +reassessment's +reassessments +reassign +reassigned +reassigning +reassigns +reassurance +reassurance's +reassurances +reassure +reassured +reassures +reassuring +reassuringly +reawaken +reawakened +reawakening +reawakens +rebate +rebate's +rebated +rebates +rebating +rebel +rebel's +rebelled +rebelling +rebellion +rebellion's +rebellions +rebellious +rebelliously +rebelliousness +rebelliousness's +rebels +rebind +rebinding +rebinds +rebirth +rebirth's +rebirths +reborn +rebound +rebound's +rebounded +rebounding +rebounds +rebroadcast +rebroadcast's +rebroadcasted +rebroadcasting +rebroadcasts +rebuff +rebuff's +rebuffed +rebuffing +rebuffs +rebuild +rebuilding +rebuilds +rebuilt +rebuke +rebuke's +rebuked +rebukes +rebuking +rebus +rebus's +rebuses +rebut +rebuts +rebuttal +rebuttal's +rebuttals +rebutted +rebutting +recalcitrance +recalcitrance's +recalcitrant +recall +recall's +recalled +recalling +recalls +recant +recantation +recantation's +recantations +recanted +recanting +recants +recap +recap's +recapitulate +recapitulated +recapitulates +recapitulating +recapitulation +recapitulation's +recapitulations +recapped +recapping +recaps +recapture +recapture's +recaptured +recaptures +recapturing +recast +recast's +recasting +recasts +recede +receded +recedes +receding +receipt +receipt's +receipted +receipting +receipts +receivable +receive +received +receiver +receiver's +receivers +receivership +receivership's +receives +receiving +recent +recenter +recentest +recently +receptacle +receptacle's +receptacles +reception +reception's +receptionist +receptionist's +receptionists +receptions +receptive +receptively +receptiveness +receptiveness's +receptivity +receptivity's +receptor +receptor's +receptors +recess +recess's +recessed +recesses +recessing +recession +recession's +recessional +recessional's +recessionals +recessions +recessive +recessive's +recessives +recharge +recharge's +rechargeable +recharged +recharges +recharging +recheck +recheck's +rechecked +rechecking +rechecks +recherché +recidivism +recidivism's +recidivist +recidivist's +recidivists +recipe +recipe's +recipes +recipient +recipient's +recipients +reciprocal +reciprocal's +reciprocally +reciprocals +reciprocate +reciprocated +reciprocates +reciprocating +reciprocation +reciprocation's +reciprocity +reciprocity's +recital +recital's +recitals +recitation +recitation's +recitations +recitative +recitative's +recitatives +recite +recited +recites +reciting +reckless +recklessly +recklessness +recklessness's +reckon +reckoned +reckoning +reckoning's +reckonings +reckons +reclaim +reclaimed +reclaiming +reclaims +reclamation +reclamation's +reclassified +reclassifies +reclassify +reclassifying +recline +reclined +recliner +recliner's +recliners +reclines +reclining +recluse +recluse's +recluses +reclusive +recognition +recognition's +recognizable +recognizably +recognizance +recognizance's +recognize +recognized +recognizer +recognizes +recognizing +recoil +recoil's +recoiled +recoiling +recoils +recollect +recollected +recollecting +recollection +recollection's +recollections +recollects +recombination +recombine +recombined +recombines +recombining +recommence +recommenced +recommences +recommencing +recommend +recommendation +recommendation's +recommendations +recommended +recommending +recommends +recompense +recompense's +recompensed +recompenses +recompensing +recompilation +recompile +recompiled +recompiling +reconcilable +reconcile +reconciled +reconciles +reconciliation +reconciliation's +reconciliations +reconciling +recondite +recondition +reconditioned +reconditioning +reconditions +reconfiguration +reconfigure +reconfigured +reconnaissance +reconnaissance's +reconnaissances +reconnect +reconnected +reconnecting +reconnects +reconnoiter +reconnoitered +reconnoitering +reconnoiters +reconquer +reconquered +reconquering +reconquers +reconsider +reconsideration +reconsideration's +reconsidered +reconsidering +reconsiders +reconstitute +reconstituted +reconstitutes +reconstituting +reconstruct +reconstructed +reconstructing +reconstruction +reconstruction's +reconstructions +reconstructs +reconvene +reconvened +reconvenes +reconvening +recopied +recopies +recopy +recopying +record +record's +recorded +recorder +recorder's +recorders +recording +recording's +recordings +records +recount +recount's +recounted +recounting +recounts +recoup +recouped +recouping +recoups +recourse +recourse's +recover +recoverable +recovered +recoveries +recovering +recovers +recovery +recovery's +recreant +recreant's +recreants +recreate +recreated +recreates +recreating +recreation +recreation's +recreational +recreations +recriminate +recriminated +recriminates +recriminating +recrimination +recrimination's +recriminations +recrudescence +recrudescence's +recruit +recruit's +recruited +recruiter +recruiter's +recruiters +recruiting +recruitment +recruitment's +recruits +recta +rectal +rectangle +rectangle's +rectangles +rectangular +rectifiable +rectification +rectification's +rectifications +rectified +rectifier +rectifier's +rectifiers +rectifies +rectify +rectifying +rectilinear +rectitude +rectitude's +rector +rector's +rectories +rectors +rectory +rectory's +rectum +rectum's +rectums +recumbent +recuperate +recuperated +recuperates +recuperating +recuperation +recuperation's +recuperative +recur +recurred +recurrence +recurrence's +recurrences +recurrent +recurring +recurs +recursion +recursive +recursively +recyclable +recyclable's +recyclables +recycle +recycle's +recycled +recycles +recycling +recycling's +red +red's +redbreast +redbreast's +redbreasts +redcap +redcap's +redcaps +redcoat +redcoat's +redcoats +redden +reddened +reddening +reddens +redder +reddest +reddish +redecorate +redecorated +redecorates +redecorating +rededicate +rededicated +rededicates +rededicating +redeem +redeemable +redeemed +redeemer +redeemer's +redeemers +redeeming +redeems +redefine +redefined +redefines +redefining +redefinition +redemption +redemption's +redeploy +redeployed +redeploying +redeployment +redeployment's +redeploys +redesign +redesigned +redesigning +redesigns +redevelop +redeveloped +redeveloping +redevelopment +redevelopment's +redevelopments +redevelops +redhead +redhead's +redheaded +redheads +redid +redirect +redirected +redirecting +redirection +redirects +rediscover +rediscovered +rediscovering +rediscovers +rediscovery +rediscovery's +redistribute +redistributed +redistributes +redistributing +redistribution +redistribution's +redistributor +redistributors +redistrict +redistricted +redistricting +redistricts +redneck +redneck's +rednecks +redness +redness's +redo +redoes +redoing +redolence +redolence's +redolent +redone +redouble +redoubled +redoubles +redoubling +redoubt +redoubt's +redoubtable +redoubts +redound +redounded +redounding +redounds +redraft +redrafted +redrafting +redrafts +redraw +redrawing +redrawn +redraws +redress +redress's +redressed +redresses +redressing +redrew +reds +redskin +redskin's +redskins +reduce +reduced +reduces +reducing +reduction +reduction's +reductions +redundancies +redundancy +redundancy's +redundant +redundantly +redwood +redwood's +redwoods +reed +reed's +reedier +reediest +reeds +reeducate +reeducated +reeducates +reeducating +reeducation +reeducation's +reedy +reef +reef's +reefed +reefer +reefer's +reefers +reefing +reefs +reek +reek's +reeked +reeking +reeks +reel +reel's +reelect +reelected +reelecting +reelection +reelection's +reelections +reelects +reeled +reeling +reels +reemerge +reemerged +reemerges +reemerging +reemphasize +reemphasized +reemphasizes +reemphasizing +reenact +reenacted +reenacting +reenactment +reenactment's +reenactments +reenacts +reenforce +reenforced +reenforces +reenforcing +reenlist +reenlisted +reenlisting +reenlists +reenter +reentered +reentering +reenters +reentries +reentry +reentry's +reestablish +reestablished +reestablishes +reestablishing +reevaluate +reevaluated +reevaluates +reevaluating +reeve +reeved +reeves +reeving +reexamine +reexamined +reexamines +reexamining +ref +ref's +refashion +refashioned +refashioning +refashions +refectories +refectory +refectory's +refer +referee +referee's +refereed +refereeing +referees +reference +reference's +referenced +references +referencing +referenda +referendum +referendum's +referendums +referent +referential +referral +referral's +referrals +referred +referring +refers +reffed +reffing +refile +refiled +refiles +refiling +refill +refill's +refillable +refilled +refilling +refills +refinance +refinanced +refinances +refinancing +refine +refined +refinement +refinement's +refinements +refiner +refiner's +refineries +refiners +refinery +refinery's +refines +refining +refinish +refinished +refinishes +refinishing +refit +refit's +refits +refitted +refitting +reflect +reflected +reflecting +reflection +reflection's +reflections +reflective +reflector +reflector's +reflectors +reflects +reflex +reflex's +reflexes +reflexive +reflexive's +reflexively +reflexives +refocus +refocused +refocuses +refocusing +refocussed +refocusses +refocussing +reforest +reforestation +reforestation's +reforested +reforesting +reforests +reform +reform's +reformat +reformation +reformation's +reformations +reformatories +reformatory +reformatory's +reformatted +reformatting +reformed +reformer +reformer's +reformers +reforming +reforms +reformulate +reformulated +reformulates +reformulating +refract +refracted +refracting +refraction +refraction's +refractories +refractory +refractory's +refracts +refrain +refrain's +refrained +refraining +refrains +refresh +refreshed +refresher +refresher's +refreshers +refreshes +refreshing +refreshingly +refreshment +refreshment's +refreshments +refreshments's +refrigerant +refrigerant's +refrigerants +refrigerate +refrigerated +refrigerates +refrigerating +refrigeration +refrigeration's +refrigerator +refrigerator's +refrigerators +refs +refuel +refueled +refueling +refuelled +refuelling +refuels +refuge +refuge's +refugee +refugee's +refugees +refuges +refulgence +refulgence's +refulgent +refund +refund's +refundable +refunded +refunding +refunds +refurbish +refurbished +refurbishes +refurbishing +refurbishment +refurbishment's +refurbishments +refurnish +refurnished +refurnishes +refurnishing +refusal +refusal's +refusals +refuse +refuse's +refused +refuses +refusing +refutation +refutation's +refutations +refute +refuted +refutes +refuting +regain +regained +regaining +regains +regal +regale +regaled +regales +regalia +regalia's +regaling +regally +regard +regard's +regarded +regarding +regardless +regards +regards's +regatta +regatta's +regattas +regencies +regency +regency's +regenerate +regenerated +regenerates +regenerating +regeneration +regeneration's +regenerative +regent +regent's +regents +reggae +reggae's +regicide +regicide's +regicides +regime +regime's +regimen +regimen's +regimens +regiment +regiment's +regimental +regimentation +regimentation's +regimented +regimenting +regiments +regimes +region +region's +regional +regionalism +regionalism's +regionalisms +regionally +regions +register +register's +registered +registering +registers +registrant +registrant's +registrants +registrar +registrar's +registrars +registration +registration's +registrations +registries +registry +registry's +regress +regress's +regressed +regresses +regressing +regression +regression's +regressions +regressive +regret +regret's +regretful +regretfully +regrets +regrettable +regrettably +regretted +regretting +regroup +regrouped +regrouping +regroups +regular +regular's +regularity +regularity's +regularize +regularized +regularizes +regularizing +regularly +regulars +regulate +regulated +regulates +regulating +regulation +regulation's +regulations +regulator +regulator's +regulators +regulatory +regurgitate +regurgitated +regurgitates +regurgitating +regurgitation +regurgitation's +rehab +rehab's +rehabbed +rehabbing +rehabilitate +rehabilitated +rehabilitates +rehabilitating +rehabilitation +rehabilitation's +rehabs +rehash +rehash's +rehashed +rehashes +rehashing +rehearsal +rehearsal's +rehearsals +rehearse +rehearsed +rehearses +rehearsing +reheat +reheated +reheating +reheats +rehire +rehired +rehires +rehiring +reign +reign's +reigned +reigning +reigns +reimburse +reimbursed +reimbursement +reimbursement's +reimbursements +reimburses +reimbursing +reimpose +reimposed +reimposes +reimposing +rein +rein's +reincarnate +reincarnated +reincarnates +reincarnating +reincarnation +reincarnation's +reincarnations +reindeer +reindeer's +reindeers +reined +reinforce +reinforced +reinforcement +reinforcement's +reinforcements +reinforces +reinforcing +reining +reinitialize +reinitialized +reins +reinsert +reinserted +reinserting +reinserts +reinstall +reinstalled +reinstalling +reinstate +reinstated +reinstatement +reinstatement's +reinstates +reinstating +reinterpret +reinterpretation +reinterpretation's +reinterpretations +reinterpreted +reinterpreting +reinterprets +reinvent +reinvented +reinventing +reinvents +reinvest +reinvested +reinvesting +reinvests +reis +reissue +reissue's +reissued +reissues +reissuing +reiterate +reiterated +reiterates +reiterating +reiteration +reiteration's +reiterations +reject +reject's +rejected +rejecting +rejection +rejection's +rejections +rejects +rejoice +rejoiced +rejoices +rejoicing +rejoicing's +rejoicings +rejoin +rejoinder +rejoinder's +rejoinders +rejoined +rejoining +rejoins +rejuvenate +rejuvenated +rejuvenates +rejuvenating +rejuvenation +rejuvenation's +rekindle +rekindled +rekindles +rekindling +relabel +relabeled +relabeling +relabelled +relabelling +relabels +relaid +relapse +relapse's +relapsed +relapses +relapsing +relate +related +relates +relating +relation +relation's +relational +relations +relationship +relationship's +relationships +relative +relative's +relatively +relatives +relativistic +relativity +relativity's +relax +relaxant +relaxant's +relaxants +relaxation +relaxation's +relaxations +relaxed +relaxes +relaxing +relay +relay's +relayed +relaying +relays +relearn +relearned +relearning +relearns +releasable +release +release's +released +releases +releasing +relegate +relegated +relegates +relegating +relegation +relegation's +relent +relented +relenting +relentless +relentlessly +relentlessness +relentlessness's +relents +relevance +relevance's +relevancy +relevancy's +relevant +relevantly +reliability +reliability's +reliable +reliably +reliance +reliance's +reliant +relic +relic's +relics +relied +relief +relief's +reliefs +relies +relieve +relieved +relieves +relieving +religion +religion's +religions +religious +religious's +religiously +relinquish +relinquished +relinquishes +relinquishing +relinquishment +relinquishment's +relish +relish's +relished +relishes +relishing +relive +relived +relives +reliving +reload +reloaded +reloading +reloads +relocatable +relocate +relocated +relocates +relocating +relocation +relocation's +reluctance +reluctance's +reluctant +reluctantly +rely +relying +remade +remain +remainder +remainder's +remaindered +remainders +remained +remaining +remains +remake +remake's +remakes +remaking +remand +remanded +remanding +remands +remark +remark's +remarkable +remarkably +remarked +remarking +remarks +remarriage +remarriage's +remarriages +remarried +remarries +remarry +remarrying +rematch +rematch's +rematches +remediable +remedial +remedied +remedies +remedy +remedy's +remedying +remember +remembered +remembering +remembers +remembrance +remembrance's +remembrances +remind +reminded +reminder +reminder's +reminders +reminding +reminds +reminisce +reminisced +reminiscence +reminiscence's +reminiscences +reminiscent +reminisces +reminiscing +remiss +remission +remission's +remissions +remissness +remissness's +remit +remits +remittance +remittance's +remittances +remitted +remitting +remnant +remnant's +remnants +remodel +remodeled +remodeling +remodelled +remodelling +remodels +remonstrance +remonstrance's +remonstrances +remonstrate +remonstrated +remonstrates +remonstrating +remorse +remorse's +remorseful +remorsefully +remorseless +remorselessly +remortgage +remortgaged +remortgages +remortgaging +remote +remote's +remotely +remoteness +remoteness's +remoter +remotes +remotest +remount +remount's +remounted +remounting +remounts +removable +removal +removal's +removals +remove +remove's +removed +remover +remover's +removers +removes +removing +remunerate +remunerated +remunerates +remunerating +remuneration +remuneration's +remunerations +remunerative +renaissance +renaissance's +renaissances +renal +rename +renamed +renames +renaming +renascence +renascence's +renascences +renascent +rend +render +render's +rendered +rendering +rendering's +renderings +renders +rendezvous +rendezvous's +rendezvoused +rendezvouses +rendezvousing +rending +rendition +rendition's +renditions +rends +renegade +renegade's +renegaded +renegades +renegading +renege +reneged +reneges +reneging +renegotiate +renegotiated +renegotiates +renegotiating +renew +renewable +renewal +renewal's +renewals +renewed +renewing +renews +rennet +rennet's +renounce +renounced +renounces +renouncing +renovate +renovated +renovates +renovating +renovation +renovation's +renovations +renovator +renovator's +renovators +renown +renown's +renowned +rent +rent's +rental +rental's +rentals +rented +renter +renter's +renters +renting +rents +renumber +renumbered +renumbering +renumbers +renunciation +renunciation's +renunciations +reoccupied +reoccupies +reoccupy +reoccupying +reoccur +reoccurred +reoccurring +reoccurs +reopen +reopened +reopening +reopens +reorder +reorder's +reordered +reordering +reorders +reorg +reorg's +reorganization +reorganization's +reorganizations +reorganize +reorganized +reorganizes +reorganizing +reorged +reorging +reorgs +rep +rep's +repackage +repackaged +repackages +repackaging +repaid +repaint +repainted +repainting +repaints +repair +repair's +repairable +repaired +repairing +repairman +repairman's +repairmen +repairs +reparation +reparation's +reparations +reparations's +repartee +repartee's +repast +repast's +repasts +repatriate +repatriate's +repatriated +repatriates +repatriating +repatriation +repatriation's +repay +repayable +repaying +repayment +repayment's +repayments +repays +repeal +repeal's +repealed +repealing +repeals +repeat +repeat's +repeatable +repeatably +repeated +repeatedly +repeater +repeater's +repeaters +repeating +repeats +repel +repellant +repellant's +repellants +repelled +repellent +repellent's +repellents +repelling +repels +repent +repentance +repentance's +repentant +repented +repenting +repents +repercussion +repercussion's +repercussions +repertoire +repertoire's +repertoires +repertories +repertory +repertory's +repetition +repetition's +repetitions +repetitious +repetitive +rephrase +rephrased +rephrases +rephrasing +replace +replaceable +replaced +replacement +replacement's +replacements +replaces +replacing +replay +replay's +replayed +replaying +replays +replenish +replenished +replenishes +replenishing +replenishment +replenishment's +replete +repleted +repletes +repleting +repletion +repletion's +replica +replica's +replicas +replicate +replicated +replicates +replicating +replication +replication's +replications +replied +replies +reply +reply's +replying +report +report's +reportage +reportage's +reported +reportedly +reporter +reporter's +reporters +reporting +reports +repose +repose's +reposed +reposeful +reposes +reposing +repositories +repository +repository's +repossess +repossessed +repossesses +repossessing +repossession +repossession's +repossessions +reprehend +reprehended +reprehending +reprehends +reprehensible +reprehensibly +represent +representation +representation's +representational +representations +representative +representative's +representatives +represented +representing +represents +repress +repressed +represses +repressing +repression +repression's +repressions +repressive +reprieve +reprieve's +reprieved +reprieves +reprieving +reprimand +reprimand's +reprimanded +reprimanding +reprimands +reprint +reprint's +reprinted +reprinting +reprints +reprisal +reprisal's +reprisals +reprise +reprise's +reprises +reprising +reprized +reproach +reproach's +reproached +reproaches +reproachful +reproachfully +reproaching +reprobate +reprobate's +reprobates +reprocess +reprocessed +reprocesses +reprocessing +reproduce +reproduced +reproduces +reproducible +reproducing +reproduction +reproduction's +reproductions +reproductive +reprogram +reprogramed +reprograming +reprogrammed +reprogramming +reprograms +reproof +reproof's +reproofed +reproofing +reproofs +reprove +reproved +reproves +reproving +reps +reptile +reptile's +reptiles +reptilian +reptilian's +reptilians +republic +republic's +republican +republican's +republicanism +republicanism's +republicans +republics +republish +republished +republishes +republishing +repudiate +repudiated +repudiates +repudiating +repudiation +repudiation's +repudiations +repugnance +repugnance's +repugnant +repulse +repulse's +repulsed +repulses +repulsing +repulsion +repulsion's +repulsive +repulsively +repulsiveness +repulsiveness's +reputable +reputably +reputation +reputation's +reputations +repute +repute's +reputed +reputedly +reputes +reputing +request +request's +requested +requester +requesting +requests +requiem +requiem's +requiems +require +required +requirement +requirement's +requirements +requires +requiring +requisite +requisite's +requisites +requisition +requisition's +requisitioned +requisitioning +requisitions +requital +requital's +requite +requited +requites +requiting +reran +reread +rereading +rereads +reroute +rerouted +reroutes +rerouting +rerun +rerun's +rerunning +reruns +resale +resale's +resales +reschedule +rescheduled +reschedules +rescheduling +rescind +rescinded +rescinding +rescinds +rescission +rescission's +rescue +rescue's +rescued +rescuer +rescuer's +rescuers +rescues +rescuing +research +research's +researched +researcher +researcher's +researchers +researches +researching +resell +reselling +resells +resemblance +resemblance's +resemblances +resemble +resembled +resembles +resembling +resend +resent +resented +resentful +resentfully +resenting +resentment +resentment's +resentments +resents +reservation +reservation's +reservations +reserve +reserve's +reserved +reservedly +reserves +reserving +reservist +reservist's +reservists +reservoir +reservoir's +reservoirs +reset +reset's +resets +resetting +resettle +resettled +resettles +resettling +reshuffle +reshuffle's +reshuffled +reshuffles +reshuffling +reside +resided +residence +residence's +residences +residencies +residency +residency's +resident +resident's +residential +residents +resides +residing +residual +residual's +residuals +residue +residue's +residues +resign +resignation +resignation's +resignations +resigned +resignedly +resigning +resigns +resilience +resilience's +resiliency +resiliency's +resilient +resin +resin's +resinous +resins +resist +resist's +resistance +resistance's +resistances +resistant +resisted +resister +resister's +resisters +resisting +resistor +resistor's +resistors +resists +resold +resolute +resolutely +resoluteness +resoluteness's +resolution +resolution's +resolutions +resolve +resolve's +resolved +resolver +resolves +resolving +resonance +resonance's +resonances +resonant +resonantly +resonate +resonated +resonates +resonating +resonator +resonator's +resonators +resort +resort's +resorted +resorting +resorts +resound +resounded +resounding +resoundingly +resounds +resource +resource's +resourceful +resourcefully +resourcefulness +resourcefulness's +resources +respect +respect's +respectability +respectability's +respectable +respectably +respected +respectful +respectfully +respecting +respective +respectively +respects +respell +respelled +respelling +respells +respelt +respiration +respiration's +respirator +respirator's +respirators +respiratory +respire +respired +respires +respiring +respite +respite's +respites +resplendence +resplendence's +resplendent +resplendently +respond +responded +respondent +respondent's +respondents +responding +responds +response +response's +responses +responsibilities +responsibility +responsibility's +responsible +responsibly +responsive +responsively +responsiveness +responsiveness's +rest +rest's +restart +restart's +restarted +restarting +restarts +restate +restated +restatement +restatement's +restatements +restates +restating +restaurant +restaurant's +restauranteur +restauranteur's +restauranteurs +restaurants +restaurateur +restaurateur's +restaurateurs +rested +restful +restfuller +restfullest +restfully +restfulness +restfulness's +resting +restitution +restitution's +restive +restively +restiveness +restiveness's +restless +restlessly +restlessness +restlessness's +restock +restocked +restocking +restocks +restoration +restoration's +restorations +restorative +restorative's +restoratives +restore +restored +restorer +restorer's +restorers +restores +restoring +restrain +restrained +restraining +restrains +restraint +restraint's +restraints +restrict +restricted +restricting +restriction +restriction's +restrictions +restrictive +restrictively +restricts +restroom +restroom's +restrooms +restructure +restructured +restructures +restructuring +restructuring's +restructurings +rests +restudied +restudies +restudy +restudying +resubmit +resubmits +resubmitted +resubmitting +result +result's +resultant +resultant's +resultants +resulted +resulting +results +resume +resume's +resumed +resumes +resuming +resumption +resumption's +resumptions +resupplied +resupplies +resupply +resupplying +resurface +resurfaced +resurfaces +resurfacing +resurgence +resurgence's +resurgences +resurgent +resurrect +resurrected +resurrecting +resurrection +resurrection's +resurrections +resurrects +resuscitate +resuscitated +resuscitates +resuscitating +resuscitation +resuscitation's +resuscitator +resuscitator's +resuscitators +retail +retail's +retailed +retailer +retailer's +retailers +retailing +retails +retain +retained +retainer +retainer's +retainers +retaining +retains +retake +retake's +retaken +retakes +retaking +retaliate +retaliated +retaliates +retaliating +retaliation +retaliation's +retaliations +retaliatory +retard +retard's +retardant +retardant's +retardants +retardation +retardation's +retarded +retarding +retards +retch +retched +retches +retching +retell +retelling +retells +retention +retention's +retentive +retentiveness +retentiveness's +rethink +rethink's +rethinking +rethinks +rethought +reticence +reticence's +reticent +retina +retina's +retinae +retinal +retinas +retinue +retinue's +retinues +retire +retired +retiree +retiree's +retirees +retirement +retirement's +retirements +retires +retiring +retold +retook +retool +retooled +retooling +retools +retort +retort's +retorted +retorting +retorts +retouch +retouch's +retouched +retouches +retouching +retrace +retraced +retraces +retracing +retract +retractable +retracted +retracting +retraction +retraction's +retractions +retracts +retrain +retrained +retraining +retrains +retread +retread's +retreaded +retreading +retreads +retreat +retreat's +retreated +retreating +retreats +retrench +retrenched +retrenches +retrenching +retrenchment +retrenchment's +retrenchments +retrial +retrial's +retrials +retribution +retribution's +retributions +retributive +retried +retries +retrievable +retrieval +retrieval's +retrievals +retrieve +retrieve's +retrieved +retriever +retriever's +retrievers +retrieves +retrieving +retroactive +retroactively +retrod +retrodden +retrofit +retrofit's +retrofits +retrofitted +retrofitting +retrograde +retrograded +retrogrades +retrograding +retrogress +retrogressed +retrogresses +retrogressing +retrogression +retrogression's +retrogressive +retrorocket +retrorocket's +retrorockets +retrospect +retrospect's +retrospected +retrospecting +retrospection +retrospection's +retrospective +retrospective's +retrospectively +retrospectives +retrospects +retry +retrying +return +return's +returnable +returnable's +returnables +returned +returnee +returnee's +returnees +returning +returns +retweet +retweeted +retweeting +retweets +retype +retyped +retypes +retyping +reunification +reunification's +reunified +reunifies +reunify +reunifying +reunion +reunion's +reunions +reunite +reunited +reunites +reuniting +reupholster +reupholstered +reupholstering +reupholsters +reusable +reuse +reuse's +reused +reuses +reusing +rev +rev's +revaluation +revaluation's +revaluations +revalue +revalued +revalues +revaluing +revamp +revamp's +revamped +revamping +revamps +reveal +revealed +revealing +revealings +reveals +reveille +reveille's +revel +revel's +revelation +revelation's +revelations +reveled +reveler +reveler's +revelers +reveling +revelled +reveller +reveller's +revellers +revelling +revelries +revelry +revelry's +revels +revenge +revenge's +revenged +revengeful +revenges +revenging +revenue +revenue's +revenues +reverberate +reverberated +reverberates +reverberating +reverberation +reverberation's +reverberations +revere +revered +reverence +reverence's +reverenced +reverences +reverencing +reverend +reverend's +reverends +reverent +reverential +reverently +reveres +reverie +reverie's +reveries +revering +reversal +reversal's +reversals +reverse +reverse's +reversed +reverses +reversible +reversing +reversion +reversion's +revert +reverted +reverting +reverts +revery +revery's +review +review's +reviewed +reviewer +reviewer's +reviewers +reviewing +reviews +revile +reviled +revilement +revilement's +reviler +reviler's +revilers +reviles +reviling +revise +revise's +revised +revises +revising +revision +revision's +revisions +revisit +revisited +revisiting +revisits +revitalization +revitalization's +revitalize +revitalized +revitalizes +revitalizing +revival +revival's +revivalist +revivalist's +revivalists +revivals +revive +revived +revives +revivification +revivification's +revivified +revivifies +revivify +revivifying +reviving +revocable +revocation +revocation's +revocations +revokable +revoke +revoked +revokes +revoking +revolt +revolt's +revolted +revolting +revoltingly +revolts +revolution +revolution's +revolutionaries +revolutionary +revolutionary's +revolutionist +revolutionist's +revolutionists +revolutionize +revolutionized +revolutionizes +revolutionizing +revolutions +revolve +revolved +revolver +revolver's +revolvers +revolves +revolving +revs +revue +revue's +revues +revulsion +revulsion's +revved +revving +reward +reward's +rewarded +rewarding +rewards +rewind +rewind's +rewindable +rewinding +rewinds +rewire +rewired +rewires +rewiring +reword +reworded +rewording +rewords +rework +reworked +reworking +reworks +rewound +rewrite +rewrite's +rewrites +rewriting +rewritten +rewrote +rhapsodic +rhapsodies +rhapsodize +rhapsodized +rhapsodizes +rhapsodizing +rhapsody +rhapsody's +rhea +rhea's +rheas +rheostat +rheostat's +rheostats +rhetoric +rhetoric's +rhetorical +rhetorically +rhetorician +rhetorician's +rhetoricians +rheum +rheum's +rheumatic +rheumatic's +rheumatics +rheumatism +rheumatism's +rheumy +rhinestone +rhinestone's +rhinestones +rhino +rhino's +rhinoceri +rhinoceros +rhinoceros's +rhinoceroses +rhinos +rhizome +rhizome's +rhizomes +rho +rhodium +rhodium's +rhododendron +rhododendron's +rhododendrons +rhombi +rhomboid +rhomboid's +rhomboids +rhombus +rhombus's +rhombuses +rhubarb +rhubarb's +rhubarbs +rhyme +rhyme's +rhymed +rhymes +rhyming +rhythm +rhythm's +rhythmic +rhythmical +rhythmically +rhythms +rib +rib's +ribald +ribaldry +ribaldry's +ribbed +ribbing +ribbon +ribbon's +ribbons +riboflavin +riboflavin's +ribs +rice +rice's +riced +rices +rich +rich's +richer +riches +richest +richly +richness +richness's +ricing +rick +rick's +ricked +ricketier +ricketiest +rickets +rickets's +rickety +ricking +ricks +ricksha +ricksha's +rickshas +rickshaw +rickshaw's +rickshaws +ricochet +ricochet's +ricocheted +ricocheting +ricochets +ricochetted +ricochetting +ricotta +ricotta's +rid +riddance +riddance's +ridded +ridden +ridding +riddle +riddle's +riddled +riddles +riddling +ride +ride's +rider +rider's +riders +rides +ridge +ridge's +ridged +ridgepole +ridgepole's +ridgepoles +ridges +ridging +ridicule +ridicule's +ridiculed +ridicules +ridiculing +ridiculous +ridiculously +ridiculousness +ridiculousness's +riding +riding's +rids +rife +rifer +rifest +riff +riff's +riffed +riffing +riffle +riffle's +riffled +riffles +riffling +riffraff +riffraff's +riffs +rifle +rifle's +rifled +rifleman +rifleman's +riflemen +rifles +rifling +rift +rift's +rifted +rifting +rifts +rig +rig's +rigamarole +rigamarole's +rigamaroles +rigged +rigging +rigging's +right +right's +righted +righteous +righteously +righteousness +righteousness's +righter +rightest +rightful +rightfully +rightfulness +rightfulness's +righting +rightist +rightist's +rightists +rightly +rightmost +rightness +rightness's +rights +rigid +rigidity +rigidity's +rigidly +rigidness +rigidness's +rigmarole +rigmarole's +rigmaroles +rigor +rigor's +rigorous +rigorously +rigors +rigs +rile +riled +riles +riling +rill +rill's +rills +rim +rim's +rime +rime's +rimed +rimes +riming +rimmed +rimming +rims +rind +rind's +rinds +ring +ring's +ringed +ringer +ringer's +ringers +ringing +ringleader +ringleader's +ringleaders +ringlet +ringlet's +ringlets +ringmaster +ringmaster's +ringmasters +rings +ringside +ringside's +ringtone +ringtone's +ringtones +ringworm +ringworm's +rink +rink's +rinks +rinse +rinse's +rinsed +rinses +rinsing +riot +riot's +rioted +rioter +rioter's +rioters +rioting +rioting's +riotous +riots +rip +rip's +ripe +ripely +ripen +ripened +ripeness +ripeness's +ripening +ripens +riper +ripest +riposte +riposte's +riposted +ripostes +riposting +ripped +ripper +ripper's +rippers +ripping +ripple +ripple's +rippled +ripples +rippling +rips +ripsaw +ripsaw's +ripsaws +rise +rise's +risen +riser +riser's +risers +rises +risible +rising +risk +risk's +risked +riskier +riskiest +riskiness +riskiness's +risking +risks +risky +risqué +rite +rite's +rites +ritual +ritual's +ritualism +ritualism's +ritualistic +ritually +rituals +ritzier +ritziest +ritzy +rival +rival's +rivaled +rivaling +rivalled +rivalling +rivalries +rivalry +rivalry's +rivals +riven +river +river's +riverbed +riverbed's +riverbeds +riverfront +rivers +riverside +riverside's +riversides +rivet +rivet's +riveted +riveter +riveter's +riveters +riveting +rivets +rivetted +rivetting +rivulet +rivulet's +rivulets +roach +roach's +roaches +road +road's +roadbed +roadbed's +roadbeds +roadblock +roadblock's +roadblocked +roadblocking +roadblocks +roadhouse +roadhouse's +roadhouses +roadkill +roadkill's +roadrunner +roadrunner's +roadrunners +roads +roadshow +roadside +roadside's +roadsides +roadster +roadster's +roadsters +roadway +roadway's +roadways +roadwork +roadwork's +roadworthy +roam +roamed +roamer +roamer's +roamers +roaming +roams +roan +roan's +roans +roar +roar's +roared +roaring +roaring's +roars +roast +roast's +roasted +roaster +roaster's +roasters +roasting +roasts +rob +robbed +robber +robber's +robberies +robbers +robbery +robbery's +robbing +robe +robe's +robed +robes +robin +robin's +robing +robins +robocall +robocall's +robocalled +robocalling +robocalls +robot +robot's +robotic +robotics +robotics's +robots +robs +robust +robuster +robustest +robustly +robustness +robustness's +rock +rock's +rocked +rocker +rocker's +rockers +rocket +rocket's +rocketed +rocketing +rocketry +rocketry's +rockets +rockier +rockiest +rockiness +rockiness's +rocking +rocks +rocky +rococo +rococo's +rod +rod's +rode +rodent +rodent's +rodents +rodeo +rodeo's +rodeos +rods +roe +roe's +roebuck +roebuck's +roebucks +roentgen +roentgen's +roentgens +roes +roger +rogered +rogering +rogers +rogue +rogue's +roguery +roguery's +rogues +roguish +roguishly +roil +roiled +roiling +roils +roister +roistered +roisterer +roisterer's +roisterers +roistering +roisters +role +role's +roles +roll +roll's +rollback +rollback's +rollbacks +rolled +roller +roller's +rollers +rollerskating +rollerskating's +rollick +rollicked +rollicking +rollicking's +rollicks +rolling +rolls +romaine +romaine's +roman +romance +romance's +romanced +romances +romancing +romantic +romantic's +romantically +romanticism +romanticism's +romanticist +romanticist's +romanticists +romanticize +romanticized +romanticizes +romanticizing +romantics +romp +romp's +romped +romper +romper's +rompers +romping +romps +rood +rood's +roods +roof +roof's +roofed +roofer +roofer's +roofers +roofing +roofing's +roofs +rooftop +rooftop's +rooftops +rook +rook's +rooked +rookeries +rookery +rookery's +rookie +rookie's +rookies +rooking +rooks +room +room's +roomed +roomer +roomer's +roomers +roomful +roomful's +roomfuls +roomier +roomiest +roominess +roominess's +rooming +roommate +roommate's +roommates +rooms +roomy +roost +roost's +roosted +rooster +rooster's +roosters +roosting +roosts +root +root's +rooted +rooter +rooting +rootless +roots +rope +rope's +roped +ropes +roping +rosaries +rosary +rosary's +rose +rose's +roseate +rosebud +rosebud's +rosebuds +rosebush +rosebush's +rosebushes +rosemary +rosemary's +roses +rosette +rosette's +rosettes +rosewood +rosewood's +rosewoods +rosier +rosiest +rosily +rosin +rosin's +rosined +rosiness +rosiness's +rosining +rosins +roster +roster's +rosters +rostra +rostrum +rostrum's +rostrums +rosy +rot +rot's +rotaries +rotary +rotary's +rotate +rotated +rotates +rotating +rotation +rotation's +rotational +rotations +rote +rote's +rotisserie +rotisserie's +rotisseries +rotogravure +rotogravure's +rotogravures +rotor +rotor's +rotors +rots +rotted +rotten +rottener +rottenest +rottenness +rottenness's +rotting +rotund +rotunda +rotunda's +rotundas +rotundity +rotundity's +rotundness +rotundness's +rouge +rouge's +rouged +rouges +rough +rough's +roughage +roughage's +roughed +roughen +roughened +roughening +roughens +rougher +roughest +roughhouse +roughhouse's +roughhoused +roughhouses +roughhousing +roughing +roughly +roughneck +roughneck's +roughnecked +roughnecking +roughnecks +roughness +roughness's +roughs +roughshod +rouging +roulette +roulette's +round +round's +roundabout +roundabout's +roundabouts +rounded +roundelay +roundelay's +roundelays +rounder +roundest +roundhouse +roundhouse's +roundhouses +rounding +roundish +roundly +roundness +roundness's +rounds +roundup +roundup's +roundups +roundworm +roundworm's +roundworms +rouse +roused +rouses +rousing +roustabout +roustabout's +roustabouts +rout +rout's +route +route's +routed +routeing +router +routes +routine +routine's +routinely +routines +routing +routinize +routinized +routinizes +routinizing +routs +roué +roué's +roués +rove +roved +rover +rover's +rovers +roves +roving +row +row's +rowboat +rowboat's +rowboats +rowdier +rowdies +rowdiest +rowdiness +rowdiness's +rowdy +rowdy's +rowdyism +rowdyism's +rowed +rowel +rowel's +roweled +roweling +rowelled +rowelling +rowels +rower +rower's +rowers +rowing +rowing's +rows +royal +royal's +royalist +royalist's +royalists +royally +royals +royalties +royalties's +royalty +royalty's +rs +rub +rub's +rubbed +rubber +rubber's +rubberize +rubberized +rubberizes +rubberizing +rubberneck +rubberneck's +rubbernecked +rubbernecking +rubbernecks +rubbers +rubbery +rubbing +rubbish +rubbish's +rubbished +rubbishes +rubbishing +rubbishy +rubble +rubble's +rubdown +rubdown's +rubdowns +rube +rube's +rubella +rubella's +rubes +rubicund +rubier +rubies +rubiest +ruble +ruble's +rubles +rubric +rubric's +rubrics +rubs +ruby +ruby's +rucksack +rucksack's +rucksacks +ruckus +ruckus's +ruckuses +rudder +rudder's +rudders +ruddier +ruddiest +ruddiness +ruddiness's +ruddy +rude +rudely +rudeness +rudeness's +ruder +rudest +rudiment +rudiment's +rudimentary +rudiments +rue +rue's +rued +rueful +ruefully +rues +ruff +ruff's +ruffed +ruffian +ruffian's +ruffians +ruffing +ruffle +ruffle's +ruffled +ruffles +ruffling +ruffs +rug +rug's +rugby +rugby's +rugged +ruggeder +ruggedest +ruggedly +ruggedness +ruggedness's +rugrat +rugrat's +rugrats +rugs +ruin +ruin's +ruination +ruination's +ruined +ruing +ruining +ruinous +ruinously +ruins +rule +rule's +ruled +ruler +ruler's +rulers +rules +ruling +ruling's +rulings +rum +rum's +rumba +rumba's +rumbaed +rumbaing +rumbas +rumble +rumble's +rumbled +rumbles +rumbling +rumbling's +rumblings +ruminant +ruminant's +ruminants +ruminate +ruminated +ruminates +ruminating +rumination +rumination's +ruminations +rummage +rummage's +rummaged +rummages +rummaging +rummer +rummest +rummy +rummy's +rumor +rumor's +rumored +rumoring +rumors +rump +rump's +rumple +rumple's +rumpled +rumples +rumpling +rumps +rumpus +rumpus's +rumpuses +rums +run +run's +runabout +runabout's +runabouts +runaround +runaround's +runarounds +runaway +runaway's +runaways +rundown +rundown's +rundowns +rune +rune's +runes +rung +rung's +rungs +runnel +runnel's +runnels +runner +runner's +runners +runnier +runniest +running +running's +runny +runoff +runoff's +runoffs +runs +runt +runt's +runts +runway +runway's +runways +rupee +rupee's +rupees +rupture +rupture's +ruptured +ruptures +rupturing +rural +ruse +ruse's +ruses +rush +rush's +rushed +rushes +rushing +rusk +rusk's +rusks +russet +russet's +russets +rust +rust's +rusted +rustic +rustic's +rustically +rusticity +rusticity's +rustics +rustier +rustiest +rustiness +rustiness's +rusting +rustle +rustle's +rustled +rustler +rustler's +rustlers +rustles +rustling +rustproof +rustproofed +rustproofing +rustproofs +rusts +rusty +rut +rut's +rutabaga +rutabaga's +rutabagas +ruthless +ruthlessly +ruthlessness +ruthlessness's +ruts +rutted +rutting +rye +rye's +s +sabbatical +sabbatical's +sabbaticals +saber +saber's +sabers +sable +sable's +sables +sabotage +sabotage's +sabotaged +sabotages +sabotaging +saboteur +saboteur's +saboteurs +sabre +sabre's +sabres +sac +sac's +saccharin +saccharin's +saccharine +sacerdotal +sachem +sachem's +sachems +sachet +sachet's +sachets +sack +sack's +sackcloth +sackcloth's +sacked +sackful +sackful's +sackfuls +sacking +sacking's +sacks +sacrament +sacrament's +sacramental +sacraments +sacred +sacredly +sacredness +sacredness's +sacrifice +sacrifice's +sacrificed +sacrifices +sacrificial +sacrificing +sacrilege +sacrilege's +sacrileges +sacrilegious +sacristan +sacristan's +sacristans +sacristies +sacristy +sacristy's +sacrosanct +sacs +sad +sadden +saddened +saddening +saddens +sadder +saddest +saddle +saddle's +saddlebag +saddlebag's +saddlebags +saddled +saddles +saddling +sades +sadism +sadism's +sadist +sadist's +sadistic +sadistically +sadists +sadly +sadness +sadness's +safari +safari's +safaried +safariing +safaris +safe +safe's +safeguard +safeguard's +safeguarded +safeguarding +safeguards +safekeeping +safekeeping's +safely +safeness +safeness's +safer +safes +safest +safeties +safety +safety's +safflower +safflower's +safflowers +saffron +saffron's +saffrons +sag +sag's +saga +saga's +sagacious +sagacity +sagacity's +sagas +sage +sage's +sagebrush +sagebrush's +sager +sages +sagest +sagged +sagging +sago +sago's +sags +saguaro +saguaro's +saguaros +sahib +sahib's +sahibs +said +sail +sail's +sailboard +sailboard's +sailboards +sailboat +sailboat's +sailboats +sailcloth +sailcloth's +sailed +sailfish +sailfish's +sailfishes +sailing +sailing's +sailings +sailor +sailor's +sailors +sails +saint +saint's +sainthood +sainthood's +saintlier +saintliest +saintliness +saintliness's +saintly +saints +saith +sake +sake's +saki +saki's +salaam +salaam's +salaamed +salaaming +salaams +salable +salacious +salaciously +salaciousness +salaciousness's +salad +salad's +salads +salamander +salamander's +salamanders +salami +salami's +salamis +salaried +salaries +salary +salary's +sale +sale's +saleable +sales +salesclerk +salesclerk's +salesclerks +salesgirl +salesgirl's +salesgirls +salesman +salesman's +salesmanship +salesmanship's +salesmen +salespeople +salespeople's +salesperson +salesperson's +salespersons +saleswoman +saleswoman's +saleswomen +salience +salience's +salient +salient's +salients +saline +saline's +salines +salinity +salinity's +saliva +saliva's +salivary +salivate +salivated +salivates +salivating +salivation +salivation's +sallied +sallies +sallow +sallower +sallowest +sally +sally's +sallying +salmon +salmon's +salmonella +salmonella's +salmonellae +salmonellas +salmons +salon +salon's +salons +saloon +saloon's +saloons +salsa +salsa's +salsas +salt +salt's +saltcellar +saltcellar's +saltcellars +salted +salter +saltest +saltier +saltiest +saltine +saltine's +saltines +saltiness +saltiness's +salting +saltpeter +saltpeter's +saltpetre +saltpetre's +salts +saltshaker +saltshaker's +saltshakers +saltwater +saltwater's +salty +salubrious +salutary +salutation +salutation's +salutations +salute +salute's +saluted +salutes +saluting +salvage +salvage's +salvageable +salvaged +salvages +salvaging +salvation +salvation's +salve +salve's +salved +salver +salver's +salvers +salves +salving +salvo +salvo's +salvoes +salvos +samba +samba's +sambaed +sambaing +sambas +same +sameness +sameness's +sames +samovar +samovar's +samovars +sampan +sampan's +sampans +sample +sample's +sampled +sampler +sampler's +samplers +samples +sampling +sampling's +samplings +samurai +samurai's +sanatoria +sanatorium +sanatorium's +sanatoriums +sancta +sanctification +sanctification's +sanctified +sanctifies +sanctify +sanctifying +sanctimonious +sanctimoniously +sanction +sanction's +sanctioned +sanctioning +sanctions +sanctity +sanctity's +sanctuaries +sanctuary +sanctuary's +sanctum +sanctum's +sanctums +sand +sand's +sandal +sandal's +sandals +sandalwood +sandalwood's +sandbag +sandbag's +sandbagged +sandbagging +sandbags +sandbank +sandbank's +sandbanks +sandbar +sandbar's +sandbars +sandblast +sandblast's +sandblasted +sandblaster +sandblaster's +sandblasters +sandblasting +sandblasts +sandbox +sandbox's +sandboxes +sandcastle +sandcastle's +sandcastles +sanded +sander +sander's +sanders +sandhog +sandhog's +sandhogs +sandier +sandiest +sandiness +sandiness's +sanding +sandlot +sandlot's +sandlots +sandman +sandman's +sandmen +sandpaper +sandpaper's +sandpapered +sandpapering +sandpapers +sandpiper +sandpiper's +sandpipers +sands +sandstone +sandstone's +sandstorm +sandstorm's +sandstorms +sandwich +sandwich's +sandwiched +sandwiches +sandwiching +sandy +sane +sanely +saner +sanest +sang +sangfroid +sangfroid's +sanguinary +sanguine +sanitaria +sanitarium +sanitarium's +sanitariums +sanitary +sanitation +sanitation's +sanitize +sanitized +sanitizes +sanitizing +sanity +sanity's +sank +sans +sanserif +sap +sap's +sapience +sapience's +sapient +sapling +sapling's +saplings +sapped +sapphire +sapphire's +sapphires +sappier +sappiest +sapping +sappy +saprophyte +saprophyte's +saprophytes +saps +sapsucker +sapsucker's +sapsuckers +sarape +sarape's +sarapes +sarcasm +sarcasm's +sarcasms +sarcastic +sarcastically +sarcoma +sarcoma's +sarcomas +sarcomata +sarcophagi +sarcophagus +sarcophagus's +sarcophaguses +sardine +sardine's +sardines +sardonic +sardonically +saree +saree's +sarees +sari +sari's +saris +sarong +sarong's +sarongs +sarsaparilla +sarsaparilla's +sarsaparillas +sartorial +sartorially +sash +sash's +sashay +sashay's +sashayed +sashaying +sashays +sashes +sass +sass's +sassafras +sassafras's +sassafrases +sassed +sasses +sassier +sassiest +sassing +sassy +sat +satanic +satanically +satanism +satanism's +satay +satchel +satchel's +satchels +sate +sated +sateen +sateen's +satellite +satellite's +satellited +satellites +satelliting +sates +satiate +satiated +satiates +satiating +satiety +satiety's +satin +satin's +sating +satinwood +satinwood's +satinwoods +satiny +satire +satire's +satires +satirical +satirically +satirist +satirist's +satirists +satirize +satirized +satirizes +satirizing +satisfaction +satisfaction's +satisfactions +satisfactorily +satisfactory +satisfied +satisfies +satisfy +satisfying +satrap +satrap's +satraps +saturate +saturated +saturates +saturating +saturation +saturation's +saturnine +satyr +satyr's +satyrs +sauce +sauce's +sauced +saucepan +saucepan's +saucepans +saucer +saucer's +saucers +sauces +saucier +sauciest +saucily +sauciness +sauciness's +saucing +saucy +sauerkraut +sauerkraut's +sauna +sauna's +saunaed +saunaing +saunas +saunter +saunter's +sauntered +sauntering +saunters +sausage +sausage's +sausages +sauted +sauté +sauté's +sautéed +sautéing +sautés +savage +savage's +savaged +savagely +savageness +savageness's +savager +savageries +savagery +savagery's +savages +savagest +savaging +savanna +savanna's +savannah +savannah's +savannahes +savannahs +savannas +savant +savant's +savants +save +save's +saved +saver +saver's +savers +saves +saving +saving's +savings +savings's +savior +savior's +saviors +saviour +saviour's +saviours +savor +savor's +savored +savorier +savories +savoriest +savoring +savors +savory +savory's +savvied +savvier +savvies +savviest +savvy +savvy's +savvying +saw +saw's +sawdust +sawdust's +sawed +sawhorse +sawhorse's +sawhorses +sawing +sawmill +sawmill's +sawmills +sawn +saws +sawyer +sawyer's +sawyers +sax +sax's +saxes +saxophone +saxophone's +saxophones +saxophonist +saxophonist's +saxophonists +say +say's +saying +saying's +sayings +says +scab +scab's +scabbard +scabbard's +scabbards +scabbed +scabbier +scabbiest +scabbing +scabby +scabies +scabies's +scabrous +scabs +scad +scad's +scads +scaffold +scaffold's +scaffolding +scaffolding's +scaffolds +scalar +scalars +scalawag +scalawag's +scalawags +scald +scald's +scalded +scalding +scalds +scale +scale's +scaled +scalene +scales +scalier +scaliest +scaling +scallion +scallion's +scallions +scallop +scallop's +scalloped +scalloping +scallops +scallywag +scallywag's +scallywags +scalp +scalp's +scalped +scalpel +scalpel's +scalpels +scalper +scalper's +scalpers +scalping +scalps +scaly +scam +scam's +scammed +scammer +scammers +scamming +scamp +scamp's +scamper +scamper's +scampered +scampering +scampers +scampi +scampi's +scampies +scamps +scams +scan +scan's +scandal +scandal's +scandalize +scandalized +scandalizes +scandalizing +scandalmonger +scandalmonger's +scandalmongers +scandalous +scandalously +scandals +scanned +scanner +scanner's +scanners +scanning +scans +scansion +scansion's +scant +scanted +scanter +scantest +scantier +scanties +scantiest +scantily +scantiness +scantiness's +scanting +scants +scanty +scapegoat +scapegoat's +scapegoated +scapegoating +scapegoats +scapula +scapula's +scapulae +scapulas +scar +scar's +scarab +scarab's +scarabs +scarce +scarcely +scarceness +scarceness's +scarcer +scarcest +scarcity +scarcity's +scare +scare's +scarecrow +scarecrow's +scarecrows +scared +scares +scarf +scarf's +scarfed +scarfing +scarfs +scarier +scariest +scarified +scarifies +scarify +scarifying +scaring +scarlet +scarlet's +scarred +scarring +scars +scarves +scary +scat +scat's +scathing +scathingly +scatological +scats +scatted +scatter +scatter's +scatterbrain +scatterbrain's +scatterbrained +scatterbrains +scattered +scattering +scatters +scatting +scavenge +scavenged +scavenger +scavenger's +scavengers +scavenges +scavenging +scenario +scenario's +scenarios +scene +scene's +scenery +scenery's +scenes +scenic +scenically +scent +scent's +scented +scenting +scents +scepter +scepter's +scepters +schedule +schedule's +scheduled +scheduler +schedulers +schedules +scheduling +schema +schematic +schematic's +schematically +schematics +scheme +scheme's +schemed +schemer +schemer's +schemers +schemes +scheming +scherzi +scherzo +scherzo's +scherzos +schism +schism's +schismatic +schismatic's +schismatics +schisms +schist +schist's +schizoid +schizoid's +schizoids +schizophrenia +schizophrenia's +schizophrenic +schizophrenic's +schizophrenics +schlemiel +schlemiel's +schlemiels +schlep +schlep's +schlepp +schlepp's +schlepped +schlepping +schlepps +schleps +schlock +schlock's +schlocky +schmaltz +schmaltz's +schmaltzier +schmaltziest +schmaltzy +schmalz +schmalz's +schmalzy +schmooze +schmoozed +schmoozes +schmoozing +schmuck +schmuck's +schmucks +schnapps +schnapps's +schnauzer +schnauzer's +schnauzers +scholar +scholar's +scholarly +scholars +scholarship +scholarship's +scholarships +scholastic +scholastically +school +school's +schoolbook +schoolbook's +schoolbooks +schoolboy +schoolboy's +schoolboys +schoolchild +schoolchild's +schoolchildren +schoolchildren's +schooldays +schooled +schoolgirl +schoolgirl's +schoolgirls +schoolhouse +schoolhouse's +schoolhouses +schooling +schooling's +schoolmarm +schoolmarm's +schoolmarms +schoolmaster +schoolmaster's +schoolmasters +schoolmate +schoolmate's +schoolmates +schoolmistress +schoolmistress's +schoolmistresses +schoolroom +schoolroom's +schoolrooms +schools +schoolteacher +schoolteacher's +schoolteachers +schoolwork +schoolwork's +schoolyard +schoolyard's +schoolyards +schooner +schooner's +schooners +schrod +schrod's +schrods +schtick +schtick's +schticks +schuss +schuss's +schussed +schusses +schussing +schwa +schwa's +schwas +sciatic +sciatica +sciatica's +science +science's +sciences +scientific +scientifically +scientist +scientist's +scientists +scimitar +scimitar's +scimitars +scintilla +scintilla's +scintillas +scintillate +scintillated +scintillates +scintillating +scintillation +scintillation's +scion +scion's +scions +scissor +scissors +sclerosis +sclerosis's +sclerotic +scoff +scoff's +scoffed +scoffing +scofflaw +scofflaw's +scofflaws +scoffs +scold +scold's +scolded +scolding +scolding's +scoldings +scolds +scoliosis +scoliosis's +scollop +scollop's +scolloped +scolloping +scollops +sconce +sconce's +sconces +scone +scone's +scones +scoop +scoop's +scooped +scooping +scoops +scoot +scooted +scooter +scooter's +scooters +scooting +scoots +scope +scope's +scoped +scopes +scoping +scorch +scorch's +scorched +scorcher +scorcher's +scorchers +scorches +scorching +score +score's +scoreboard +scoreboard's +scoreboards +scorecard +scorecard's +scorecards +scored +scoreless +scorer +scorer's +scorers +scores +scoring +scorn +scorn's +scorned +scornful +scornfully +scorning +scorns +scorpion +scorpion's +scorpions +scotch +scotch's +scotched +scotches +scotching +scotchs +scoundrel +scoundrel's +scoundrels +scour +scoured +scourge +scourge's +scourged +scourges +scourging +scouring +scours +scout +scout's +scouted +scouting +scouting's +scoutmaster +scoutmaster's +scoutmasters +scouts +scow +scow's +scowl +scowl's +scowled +scowling +scowls +scows +scrabble +scrabble's +scrabbled +scrabbles +scrabbling +scragglier +scraggliest +scraggly +scram +scramble +scramble's +scrambled +scrambler +scrambler's +scramblers +scrambles +scrambling +scrammed +scramming +scrams +scrap +scrap's +scrapbook +scrapbook's +scrapbooks +scrape +scrape's +scraped +scraper +scraper's +scrapers +scrapes +scraping +scrapped +scrappier +scrappiest +scrapping +scrappy +scraps +scratch +scratch's +scratched +scratches +scratchier +scratchiest +scratchiness +scratchiness's +scratching +scratchy +scrawl +scrawl's +scrawled +scrawling +scrawls +scrawnier +scrawniest +scrawny +scream +scream's +screamed +screaming +screams +screech +screech's +screeched +screeches +screechier +screechiest +screeching +screechy +screen +screen's +screened +screening +screening's +screenings +screenplay +screenplay's +screenplays +screens +screenshot +screenshots +screenwriter +screenwriter's +screenwriters +screw +screw's +screwball +screwball's +screwballs +screwdriver +screwdriver's +screwdrivers +screwed +screwier +screwiest +screwing +screws +screwy +scribble +scribble's +scribbled +scribbler +scribbler's +scribblers +scribbles +scribbling +scribe +scribe's +scribes +scrimmage +scrimmage's +scrimmaged +scrimmages +scrimmaging +scrimp +scrimped +scrimping +scrimps +scrimshaw +scrimshaw's +scrimshawed +scrimshawing +scrimshaws +scrip +scrip's +scrips +script +script's +scripted +scripting +scripts +scriptural +scripture +scripture's +scriptures +scriptwriter +scriptwriter's +scriptwriters +scrod +scrod's +scrods +scrofula +scrofula's +scroll +scroll's +scrolled +scrolling +scrolls +scrooge +scrooge's +scrooges +scrota +scrotum +scrotum's +scrotums +scrounge +scrounged +scrounger +scrounger's +scroungers +scrounges +scrounging +scrub +scrub's +scrubbed +scrubber +scrubber's +scrubbers +scrubbier +scrubbiest +scrubbing +scrubby +scrubs +scruff +scruff's +scruffier +scruffiest +scruffs +scruffy +scrumptious +scrunch +scrunch's +scrunched +scrunches +scrunchie +scrunchie's +scrunchies +scrunching +scrunchy +scrunchy's +scruple +scruple's +scrupled +scruples +scrupling +scrupulous +scrupulously +scrutinize +scrutinized +scrutinizes +scrutinizing +scrutiny +scrutiny's +scuba +scuba's +scubaed +scubaing +scubas +scud +scud's +scudded +scudding +scuds +scuff +scuff's +scuffed +scuffing +scuffle +scuffle's +scuffled +scuffles +scuffling +scuffs +scull +scull's +sculled +sculleries +scullery +scullery's +sculling +scullion +scullion's +scullions +sculls +sculpt +sculpted +sculpting +sculptor +sculptor's +sculptors +sculpts +sculptural +sculpture +sculpture's +sculptured +sculptures +sculpturing +scum +scum's +scumbag +scumbag's +scumbags +scummed +scummier +scummiest +scumming +scummy +scums +scupper +scupper's +scuppered +scuppering +scuppers +scurf +scurf's +scurfy +scurried +scurries +scurrilous +scurrilously +scurry +scurry's +scurrying +scurvier +scurviest +scurvy +scurvy's +scuttle +scuttle's +scuttlebutt +scuttlebutt's +scuttled +scuttles +scuttling +scuzzier +scuzziest +scuzzy +scythe +scythe's +scythed +scythes +scything +sea +sea's +seabed +seabed's +seabeds +seabird +seabird's +seabirds +seaboard +seaboard's +seaboards +seacoast +seacoast's +seacoasts +seafarer +seafarer's +seafarers +seafaring +seafaring's +seafood +seafood's +seagoing +seal +seal's +sealant +sealant's +sealants +sealed +sealer +sealer's +sealers +sealing +seals +sealskin +sealskin's +seam +seam's +seaman +seaman's +seamanship +seamanship's +seamed +seamen +seamier +seamiest +seaming +seamless +seams +seamstress +seamstress's +seamstresses +seamy +seaplane +seaplane's +seaplanes +seaport +seaport's +seaports +sear +sear's +search +search's +searched +searcher +searcher's +searchers +searches +searching +searchingly +searchlight +searchlight's +searchlights +seared +searing +sears +seas +seascape +seascape's +seascapes +seashell +seashell's +seashells +seashore +seashore's +seashores +seasick +seasickness +seasickness's +seaside +seaside's +seasides +season +season's +seasonable +seasonal +seasonally +seasoned +seasoning +seasoning's +seasonings +seasons +seat +seat's +seated +seating +seating's +seats +seaward +seaward's +seawards +seaway +seaway's +seaways +seaweed +seaweed's +seaworthy +sebaceous +secede +seceded +secedes +seceding +secession +secession's +secessionist +secessionist's +secessionists +seclude +secluded +secludes +secluding +seclusion +seclusion's +seclusive +second +second's +secondaries +secondarily +secondary +secondary's +seconded +secondhand +seconding +secondly +seconds +secrecy +secrecy's +secret +secret's +secretarial +secretariat +secretariat's +secretariats +secretaries +secretary +secretary's +secrete +secreted +secretes +secreting +secretion +secretion's +secretions +secretive +secretively +secretiveness +secretiveness's +secretly +secrets +secs +sect +sect's +sectarian +sectarian's +sectarianism +sectarianism's +sectarians +section +section's +sectional +sectional's +sectionalism +sectionalism's +sectionals +sectioned +sectioning +sections +sector +sector's +sectors +sects +secular +secularism +secularism's +secularization +secularization's +secularize +secularized +secularizes +secularizing +secure +secured +securely +securer +secures +securest +securing +securities +security +security's +sedan +sedan's +sedans +sedate +sedated +sedately +sedater +sedates +sedatest +sedating +sedation +sedation's +sedative +sedative's +sedatives +sedentary +sedge +sedge's +sediment +sediment's +sedimentary +sedimentation +sedimentation's +sediments +sedition +sedition's +seditious +seduce +seduced +seducer +seducer's +seducers +seduces +seducing +seduction +seduction's +seductions +seductive +seductively +sedulous +see +see's +seed +seed's +seeded +seedier +seediest +seediness +seediness's +seeding +seedless +seedling +seedling's +seedlings +seeds +seedy +seeing +seeings +seek +seeker +seeker's +seekers +seeking +seeks +seem +seemed +seeming +seemingly +seemlier +seemliest +seemliness +seemliness's +seemly +seems +seen +seep +seepage +seepage's +seeped +seeping +seeps +seer +seer's +seers +seersucker +seersucker's +sees +seesaw +seesaw's +seesawed +seesawing +seesaws +seethe +seethed +seethes +seething +segment +segment's +segmentation +segmentation's +segmented +segmenting +segments +segregate +segregated +segregates +segregating +segregation +segregation's +segregationist +segregationist's +segregationists +segue +segue's +segued +segueing +segues +seismic +seismically +seismograph +seismograph's +seismographic +seismographs +seismologist +seismologist's +seismologists +seismology +seismology's +seize +seized +seizes +seizing +seizure +seizure's +seizures +seldom +select +selected +selecting +selection +selection's +selections +selective +selectively +selectivity +selectivity's +selectman +selectman's +selectmen +selector +selector's +selectors +selects +selenium +selenium's +self +self's +selfie +selfie's +selfies +selfish +selfishly +selfishness +selfishness's +selfless +selflessly +selflessness +selflessness's +selfsame +sell +sell's +seller +seller's +sellers +selling +selloff +selloff's +selloffs +sellout +sellout's +sellouts +sells +seltzer +seltzer's +selvage +selvage's +selvages +selvedge +selvedge's +selvedges +selves +semantic +semantically +semantics +semantics's +semaphore +semaphore's +semaphored +semaphores +semaphoring +semblance +semblance's +semblances +semen +semen's +semester +semester's +semesters +semi +semi's +semiannual +semiautomatic +semiautomatic's +semiautomatics +semicircle +semicircle's +semicircles +semicircular +semicolon +semicolon's +semicolons +semiconductor +semiconductor's +semiconductors +semiconscious +semifinal +semifinal's +semifinalist +semifinalist's +semifinalists +semifinals +semimonthlies +semimonthly +semimonthly's +seminal +seminar +seminar's +seminarian +seminarian's +seminarians +seminaries +seminars +seminary +seminary's +semiotics +semipermeable +semiprecious +semiprivate +semiprofessional +semiprofessional's +semiprofessionals +semiretired +semis +semiskilled +semitone +semitone's +semitones +semitrailer +semitrailer's +semitrailers +semitropical +semiweeklies +semiweekly +semiweekly's +senate +senate's +senates +senator +senator's +senatorial +senators +send +sender +sender's +senders +sending +sends +senile +senility +senility's +senior +senior's +seniority +seniority's +seniors +senna +senna's +sensation +sensation's +sensational +sensationalism +sensationalism's +sensationalist +sensationalist's +sensationalists +sensationally +sensations +sense +sense's +sensed +senseless +senselessly +senselessness +senselessness's +senses +sensibilities +sensibility +sensibility's +sensible +sensibly +sensing +sensitive +sensitive's +sensitively +sensitiveness +sensitiveness's +sensitives +sensitivities +sensitivity +sensitivity's +sensitization +sensitization's +sensitize +sensitized +sensitizes +sensitizing +sensor +sensor's +sensors +sensory +sensual +sensuality +sensuality's +sensually +sensuous +sensuously +sensuousness +sensuousness's +sent +sentence +sentence's +sentenced +sentences +sentencing +sententious +sentience +sentient +sentiment +sentiment's +sentimental +sentimentalism +sentimentalism's +sentimentalist +sentimentalist's +sentimentalists +sentimentality +sentimentality's +sentimentalize +sentimentalized +sentimentalizes +sentimentalizing +sentimentally +sentiments +sentinel +sentinel's +sentinels +sentries +sentry +sentry's +sepal +sepal's +sepals +separable +separate +separate's +separated +separately +separates +separating +separation +separation's +separations +separatism +separatism's +separatist +separatist's +separatists +separator +separator's +separators +sepia +sepia's +sepsis +sepsis's +septa +septet +septet's +septets +septette +septette's +septettes +septic +septicemia +septicemia's +septuagenarian +septuagenarian's +septuagenarians +septum +septum's +septums +sepulcher +sepulcher's +sepulchered +sepulchering +sepulchers +sepulchral +sequel +sequel's +sequels +sequence +sequence's +sequenced +sequencer +sequencers +sequences +sequencing +sequential +sequentially +sequester +sequestered +sequestering +sequesters +sequestration +sequestration's +sequestrations +sequin +sequin's +sequined +sequins +sequitur +sequoia +sequoia's +sequoias +sera +seraglio +seraglio's +seraglios +serape +serape's +serapes +seraph +seraph's +seraphic +seraphim +seraphs +sere +serenade +serenade's +serenaded +serenades +serenading +serendipitous +serendipity +serendipity's +serene +serenely +sereneness +sereneness's +serener +serenest +serenity +serenity's +serer +serest +serf +serf's +serfdom +serfdom's +serfs +serge +serge's +sergeant +sergeant's +sergeants +serial +serial's +serialization +serialization's +serialize +serialized +serializes +serializing +serially +serials +series +series's +serious +seriously +seriousness +seriousness's +sermon +sermon's +sermonize +sermonized +sermonizes +sermonizing +sermons +serous +serpent +serpent's +serpentine +serpentine's +serpents +serrated +serried +serum +serum's +serums +servant +servant's +servants +serve +serve's +served +server +server's +servers +serves +service +service's +serviceable +serviced +serviceman +serviceman's +servicemen +services +servicewoman +servicewoman's +servicewomen +servicing +serviette +serviette's +serviettes +servile +servility +servility's +serving +serving's +servings +servitude +servitude's +servo +servo's +servomechanism +servomechanism's +servomechanisms +servos +sesame +sesame's +sesames +session +session's +sessions +set +set's +setback +setback's +setbacks +sets +settable +settee +settee's +settees +setter +setter's +setters +setting +setting's +settings +settle +settle's +settled +settlement +settlement's +settlements +settler +settler's +settlers +settles +settling +setup +setup's +setups +seven +seven's +sevens +seventeen +seventeen's +seventeens +seventeenth +seventeenth's +seventeenths +seventh +seventh's +sevenths +seventies +seventieth +seventieth's +seventieths +seventy +seventy's +sever +several +several's +severally +severance +severance's +severances +severe +severed +severely +severer +severest +severing +severity +severity's +severs +sew +sewage +sewage's +sewed +sewer +sewer's +sewerage +sewerage's +sewers +sewing +sewing's +sewn +sews +sex +sex's +sexagenarian +sexagenarian's +sexagenarians +sexed +sexes +sexier +sexiest +sexily +sexiness +sexiness's +sexing +sexism +sexism's +sexist +sexist's +sexists +sexless +sexpot +sexpot's +sexpots +sextant +sextant's +sextants +sextet +sextet's +sextets +sextette +sextette's +sextettes +sexting +sexton +sexton's +sextons +sexual +sexuality +sexuality's +sexually +sexy +sh +shabbier +shabbiest +shabbily +shabbiness +shabbiness's +shabby +shack +shack's +shackle +shackle's +shackled +shackles +shackling +shacks +shad +shad's +shade +shade's +shaded +shades +shadier +shadiest +shadiness +shadiness's +shading +shading's +shadings +shadow +shadow's +shadowbox +shadowboxed +shadowboxes +shadowboxing +shadowed +shadowier +shadowiest +shadowing +shadows +shadowy +shads +shady +shaft +shaft's +shafted +shafting +shafts +shag +shag's +shagged +shaggier +shaggiest +shagginess +shagginess's +shagging +shaggy +shags +shah +shah's +shahs +shaikh +shaikh's +shaikhs +shake +shake's +shakedown +shakedown's +shakedowns +shaken +shaker +shaker's +shakers +shakes +shakeup +shakeup's +shakeups +shakier +shakiest +shakily +shakiness +shakiness's +shaking +shaky +shale +shale's +shall +shallot +shallot's +shallots +shallow +shallow's +shallower +shallowest +shallowness +shallowness's +shallows +shalt +sham +sham's +shaman +shaman's +shamans +shamble +shamble's +shambled +shambles +shambles's +shambling +shame +shame's +shamed +shamefaced +shameful +shamefully +shamefulness +shamefulness's +shameless +shamelessly +shames +shaming +shammed +shammies +shamming +shammy +shammy's +shampoo +shampoo's +shampooed +shampooing +shampoos +shamrock +shamrock's +shamrocks +shams +shan't +shandy +shanghai +shanghaied +shanghaiing +shanghais +shank +shank's +shanks +shanties +shantung +shantung's +shanty +shanty's +shantytown +shantytown's +shantytowns +shape +shape's +shaped +shapeless +shapelessly +shapelessness +shapelessness's +shapelier +shapeliest +shapeliness +shapeliness's +shapely +shapes +shaping +sharable +shard +shard's +shards +share +share's +shareable +sharecropper +sharecropper's +sharecroppers +shared +shareholder +shareholder's +shareholders +shares +sharia +shariah +sharing +shark +shark's +sharked +sharking +sharks +sharkskin +sharkskin's +sharp +sharp's +sharped +sharpen +sharpened +sharpener +sharpener's +sharpeners +sharpening +sharpens +sharper +sharper's +sharpers +sharpest +sharping +sharply +sharpness +sharpness's +sharps +sharpshooter +sharpshooter's +sharpshooters +shat +shatter +shatter's +shattered +shattering +shatterproof +shatters +shave +shave's +shaved +shaven +shaver +shaver's +shavers +shaves +shaving +shaving's +shavings +shawl +shawl's +shawls +shaykh +shaykh's +shaykhs +she +she'd +she'll +she's +sheaf +sheaf's +shear +shear's +sheared +shearer +shearer's +shearers +shearing +shears +sheath +sheath's +sheathe +sheathed +sheathes +sheathing +sheathing's +sheathings +sheaths +sheave +sheave's +sheaves +shebang +shebang's +shebangs +shed +shed's +shedding +sheds +sheen +sheen's +sheep +sheep's +sheepdog +sheepdog's +sheepdogs +sheepfold +sheepfold's +sheepfolds +sheepish +sheepishly +sheepishness +sheepishness's +sheepskin +sheepskin's +sheepskins +sheer +sheer's +sheered +sheerer +sheerest +sheering +sheers +sheet +sheet's +sheeting +sheeting's +sheets +sheik +sheik's +sheikdom +sheikdom's +sheikdoms +sheikh +sheikh's +sheikhdom +sheikhdom's +sheikhdoms +sheikhs +sheiks +shekel +shekel's +shekels +shelf +shelf's +shell +shell's +shellac +shellac's +shellacked +shellacking +shellacs +shelled +sheller +shellfish +shellfish's +shellfishes +shelling +shells +shelter +shelter's +sheltered +sheltering +shelters +shelve +shelved +shelves +shelving +shelving's +shenanigan +shenanigan's +shenanigans +shepherd +shepherd's +shepherded +shepherdess +shepherdess's +shepherdesses +shepherding +shepherds +sherbert +sherbert's +sherberts +sherbet +sherbet's +sherbets +sherd +sherd's +sherds +sheriff +sheriff's +sheriffs +sherries +sherry +sherry's +shes +shibboleth +shibboleth's +shibboleths +shied +shield +shield's +shielded +shielding +shields +shies +shift +shift's +shifted +shiftier +shiftiest +shiftily +shiftiness +shiftiness's +shifting +shiftless +shiftlessness +shiftlessness's +shifts +shifty +shiitake +shiitake's +shiitakes +shill +shill's +shillalah +shillalah's +shillalahs +shilled +shillelagh +shillelagh's +shillelaghs +shilling +shilling's +shillings +shills +shim +shim's +shimmed +shimmer +shimmer's +shimmered +shimmering +shimmers +shimmery +shimmied +shimmies +shimming +shimmy +shimmy's +shimmying +shims +shin +shin's +shinbone +shinbone's +shinbones +shindig +shindig's +shindigs +shine +shine's +shined +shiner +shiner's +shiners +shines +shingle +shingle's +shingled +shingles +shingling +shinier +shiniest +shininess +shininess's +shining +shinned +shinnied +shinnies +shinning +shinny +shinnying +shins +shiny +ship +ship's +shipboard +shipboard's +shipboards +shipbuilder +shipbuilder's +shipbuilders +shipbuilding +shipbuilding's +shipload +shipload's +shiploads +shipmate +shipmate's +shipmates +shipment +shipment's +shipments +shipped +shipper +shipper's +shippers +shipping +shipping's +ships +shipshape +shipwreck +shipwreck's +shipwrecked +shipwrecking +shipwrecks +shipwright +shipwright's +shipwrights +shipyard +shipyard's +shipyards +shire +shire's +shires +shirk +shirked +shirker +shirker's +shirkers +shirking +shirks +shirr +shirr's +shirred +shirring +shirring's +shirrings +shirrs +shirt +shirt's +shirted +shirting +shirts +shirtsleeve +shirtsleeve's +shirtsleeves +shirttail +shirttail's +shirttails +shirtwaist +shirtwaist's +shirtwaists +shit +shit's +shits +shittier +shittiest +shitting +shitty +shiver +shiver's +shivered +shivering +shivers +shivery +shlemiel +shlemiel's +shlemiels +shlep +shlep's +shlepp +shlepp's +shlepped +shlepping +shlepps +shleps +shlock +shlocky +shoal +shoal's +shoaled +shoaling +shoals +shock +shock's +shocked +shocker +shocker's +shockers +shocking +shockingly +shockproof +shocks +shod +shodden +shoddier +shoddiest +shoddily +shoddiness +shoddiness's +shoddy +shoddy's +shoe +shoe's +shoed +shoehorn +shoehorn's +shoehorned +shoehorning +shoehorns +shoeing +shoelace +shoelace's +shoelaces +shoemaker +shoemaker's +shoemakers +shoes +shoeshine +shoeshine's +shoeshines +shoestring +shoestring's +shoestrings +shogun +shogun's +shoguns +shone +shoo +shooed +shooing +shook +shoon +shoos +shoot +shoot's +shooter +shooter's +shooters +shooting +shooting's +shootings +shootout +shootout's +shootouts +shoots +shop +shop's +shopaholic +shopaholic's +shopaholics +shopkeeper +shopkeeper's +shopkeepers +shoplift +shoplifted +shoplifter +shoplifter's +shoplifters +shoplifting +shoplifting's +shoplifts +shopped +shopper +shopper's +shoppers +shopping +shopping's +shops +shoptalk +shoptalk's +shopworn +shore +shore's +shored +shoreline +shoreline's +shorelines +shores +shoring +shorn +short +short's +shortage +shortage's +shortages +shortbread +shortbread's +shortcake +shortcake's +shortcakes +shortchange +shortchanged +shortchanges +shortchanging +shortcoming +shortcoming's +shortcomings +shortcut +shortcut's +shortcuts +shorted +shorten +shortened +shortening +shortening's +shortenings +shortens +shorter +shortest +shortfall +shortfall's +shortfalls +shorthand +shorthand's +shorthorn +shorthorn's +shorthorns +shorting +shortish +shortlist +shortly +shortness +shortness's +shorts +shortsighted +shortsightedly +shortsightedness +shortsightedness's +shortstop +shortstop's +shortstops +shortwave +shortwave's +shortwaves +shot +shot's +shotgun +shotgun's +shotgunned +shotgunning +shotguns +shots +should +should've +shoulder +shoulder's +shouldered +shouldering +shoulders +shouldn't +shout +shout's +shouted +shouting +shouts +shove +shove's +shoved +shovel +shovel's +shoveled +shovelful +shovelful's +shovelfuls +shoveling +shovelled +shovelling +shovels +shoves +shoving +show +show's +showbiz +showbiz's +showboat +showboat's +showboated +showboating +showboats +showcase +showcase's +showcased +showcases +showcasing +showdown +showdown's +showdowns +showed +shower +shower's +showered +showering +showers +showery +showgirl +showgirl's +showgirls +showier +showiest +showily +showiness +showiness's +showing +showing's +showings +showman +showman's +showmanship +showmanship's +showmen +shown +showoff +showoff's +showoffs +showpiece +showpiece's +showpieces +showplace +showplace's +showplaces +showroom +showroom's +showrooms +shows +showy +shrank +shrapnel +shrapnel's +shred +shred's +shredded +shredder +shredder's +shredders +shredding +shreds +shrew +shrew's +shrewd +shrewder +shrewdest +shrewdly +shrewdness +shrewdness's +shrewish +shrews +shriek +shriek's +shrieked +shrieking +shrieks +shrift +shrift's +shrike +shrike's +shrikes +shrill +shrilled +shriller +shrillest +shrilling +shrillness +shrillness's +shrills +shrilly +shrimp +shrimp's +shrimped +shrimping +shrimps +shrine +shrine's +shrines +shrink +shrink's +shrinkable +shrinkage +shrinkage's +shrinking +shrinks +shrive +shrived +shrivel +shriveled +shriveling +shrivelled +shrivelling +shrivels +shriven +shrives +shriving +shroud +shroud's +shrouded +shrouding +shrouds +shrove +shrub +shrub's +shrubberies +shrubbery +shrubbery's +shrubbier +shrubbiest +shrubby +shrubs +shrug +shrug's +shrugged +shrugging +shrugs +shrunk +shrunken +shtick +shtick's +shticks +shtik +shtik's +shtiks +shuck +shuck's +shucked +shucking +shucks +shuckses +shudder +shudder's +shuddered +shuddering +shudders +shuffle +shuffle's +shuffleboard +shuffleboard's +shuffleboards +shuffled +shuffler +shuffler's +shufflers +shuffles +shuffling +shun +shunned +shunning +shuns +shunt +shunt's +shunted +shunting +shunts +shush +shushed +shushes +shushing +shut +shutdown +shutdown's +shutdowns +shuteye +shuteye's +shutout +shutout's +shutouts +shuts +shutter +shutter's +shutterbug +shutterbug's +shutterbugs +shuttered +shuttering +shutters +shutting +shuttle +shuttle's +shuttlecock +shuttlecock's +shuttlecocked +shuttlecocking +shuttlecocks +shuttled +shuttles +shuttling +shy +shy's +shyer +shyest +shying +shyly +shyness +shyness's +shyster +shyster's +shysters +sibilant +sibilant's +sibilants +sibling +sibling's +siblings +sibyl +sibyl's +sibyls +sic +sick +sickbed +sickbed's +sickbeds +sicked +sicken +sickened +sickening +sickeningly +sickens +sicker +sickest +sicking +sickle +sickle's +sickles +sicklier +sickliest +sickly +sickness +sickness's +sicknesses +sicks +sics +side +side's +sidearm +sidearm's +sidearms +sidebar +sidebar's +sidebars +sideboard +sideboard's +sideboards +sideburns +sideburns's +sidecar +sidecar's +sidecars +sided +sidekick +sidekick's +sidekicks +sidelight +sidelight's +sidelights +sideline +sideline's +sidelined +sidelines +sidelining +sidelong +sidereal +sides +sidesaddle +sidesaddle's +sidesaddles +sideshow +sideshow's +sideshows +sidesplitting +sidestep +sidestep's +sidestepped +sidestepping +sidesteps +sidestroke +sidestroke's +sidestroked +sidestrokes +sidestroking +sideswipe +sideswipe's +sideswiped +sideswipes +sideswiping +sidetrack +sidetrack's +sidetracked +sidetracking +sidetracks +sidewalk +sidewalk's +sidewalks +sidewall +sidewall's +sidewalls +sideways +sidewise +siding +siding's +sidings +sidle +sidle's +sidled +sidles +sidling +siege +siege's +sieges +sierra +sierra's +sierras +siesta +siesta's +siestas +sieve +sieve's +sieved +sieves +sieving +sift +sifted +sifter +sifter's +sifters +sifting +sifts +sigh +sigh's +sighed +sighing +sighs +sight +sight's +sighted +sighting +sighting's +sightings +sightless +sightread +sights +sightseeing +sightseeing's +sightseer +sightseer's +sightseers +sigma +sign +sign's +signal +signal's +signaled +signaling +signalize +signalized +signalizes +signalizing +signalled +signalling +signally +signals +signatories +signatory +signatory's +signature +signature's +signatures +signboard +signboard's +signboards +signed +signer +signer's +signers +signet +signet's +signets +significance +significance's +significant +significantly +signification +signification's +significations +signified +signifies +signify +signifying +signing +signing's +signings +signpost +signpost's +signposted +signposting +signposts +signs +silage +silage's +silence +silence's +silenced +silencer +silencer's +silencers +silences +silencing +silent +silent's +silenter +silentest +silently +silents +silhouette +silhouette's +silhouetted +silhouettes +silhouetting +silica +silica's +silicate +silicate's +silicates +siliceous +silicious +silicon +silicon's +silicone +silicone's +silicosis +silicosis's +silk +silk's +silken +silkier +silkiest +silks +silkworm +silkworm's +silkworms +silky +sill +sill's +sillier +sillies +silliest +silliness +silliness's +sills +silly +silly's +silo +silo's +silos +silt +silt's +silted +silting +silts +silvan +silver +silver's +silvered +silverfish +silverfish's +silverfishes +silvering +silvers +silversmith +silversmith's +silversmiths +silverware +silverware's +silvery +sim +sim's +simian +simian's +simians +similar +similarities +similarity +similarity's +similarly +simile +simile's +similes +simmer +simmer's +simmered +simmering +simmers +simpatico +simper +simper's +simpered +simpering +simpers +simple +simpleness +simpleness's +simpler +simplest +simpleton +simpleton's +simpletons +simplex +simplicity +simplicity's +simplification +simplification's +simplifications +simplified +simplifies +simplify +simplifying +simplistic +simply +sims +simulate +simulated +simulates +simulating +simulation +simulation's +simulations +simulator +simulator's +simulators +simulcast +simulcast's +simulcasted +simulcasting +simulcasts +simultaneous +simultaneously +sin +sin's +since +sincere +sincerely +sincerer +sincerest +sincerity +sincerity's +sine +sinecure +sinecure's +sinecures +sinew +sinew's +sinews +sinewy +sinful +sinfully +sinfulness +sinfulness's +sing +sing's +singe +singe's +singed +singeing +singer +singer's +singers +singes +singing +singing's +single +single's +singled +singles +singles's +singleton +singleton's +singletons +singling +singly +sings +singsong +singsong's +singsonged +singsonging +singsongs +singular +singular's +singularities +singularity +singularity's +singularly +singulars +sinister +sink +sink's +sinkable +sinker +sinker's +sinkers +sinkhole +sinkhole's +sinkholes +sinking +sinks +sinned +sinner +sinner's +sinners +sinning +sins +sinuous +sinus +sinus's +sinuses +sinusitis +sinusitis's +sinusoidal +sip +sip's +siphon +siphon's +siphoned +siphoning +siphons +sipped +sipping +sips +sir +sir's +sire +sire's +sired +siren +siren's +sirens +sires +siring +sirloin +sirloin's +sirloins +sirocco +sirocco's +siroccos +sirs +sirup +sirup's +sirups +sis +sis's +sisal +sisal's +sises +sissier +sissies +sissiest +sissy +sissy's +sister +sister's +sisterhood +sisterhood's +sisterhoods +sisterly +sisters +sit +sitar +sitar's +sitars +sitcom +sitcom's +sitcoms +site +site's +sited +sites +siting +sits +sitter +sitter's +sitters +sitting +sitting's +sittings +situate +situated +situates +situating +situation +situation's +situations +six +six's +sixes +sixpence +sixpence's +sixpences +sixteen +sixteen's +sixteens +sixteenth +sixteenth's +sixteenths +sixth +sixth's +sixths +sixties +sixtieth +sixtieth's +sixtieths +sixty +sixty's +sizable +size +size's +sizeable +sized +sizer +sizes +sizing +sizing's +sizzle +sizzle's +sizzled +sizzles +sizzling +skate +skate's +skateboard +skateboard's +skateboarded +skateboarder +skateboarder's +skateboarders +skateboarding +skateboarding's +skateboards +skated +skater +skater's +skaters +skates +skating +skedaddle +skedaddle's +skedaddled +skedaddles +skedaddling +skeet +skeet's +skein +skein's +skeins +skeletal +skeleton +skeleton's +skeletons +skeptic +skeptic's +skeptical +skeptically +skepticism +skepticism's +skeptics +sketch +sketch's +sketched +sketches +sketchier +sketchiest +sketching +sketchy +skew +skew's +skewed +skewer +skewer's +skewered +skewering +skewers +skewing +skews +ski +ski's +skid +skid's +skidded +skidding +skids +skied +skier +skier's +skiers +skies +skiff +skiff's +skiffs +skiing +skiing's +skilful +skill +skill's +skilled +skillet +skillet's +skillets +skillful +skillfully +skills +skim +skim's +skimmed +skimming +skimp +skimped +skimpier +skimpiest +skimpiness +skimpiness's +skimping +skimps +skimpy +skims +skin +skin's +skinflint +skinflint's +skinflints +skinhead +skinhead's +skinheads +skinless +skinned +skinnier +skinniest +skinniness +skinniness's +skinning +skinny +skinny's +skins +skintight +skip +skip's +skipped +skipper +skipper's +skippered +skippering +skippers +skipping +skips +skirmish +skirmish's +skirmished +skirmishes +skirmishing +skirt +skirt's +skirted +skirting +skirts +skis +skit +skit's +skits +skitter +skittered +skittering +skitters +skittish +skivvied +skivvies +skivvy +skivvy's +skivvying +skulduggery +skulduggery's +skulk +skulked +skulking +skulks +skull +skull's +skullcap +skullcap's +skullcaps +skullduggery +skullduggery's +skulls +skunk +skunk's +skunked +skunking +skunks +sky +sky's +skycap +skycap's +skycaps +skydive +skydived +skydiver +skydiver's +skydivers +skydives +skydiving +skydiving's +skydove +skyed +skying +skyjack +skyjacked +skyjacker +skyjacker's +skyjackers +skyjacking +skyjacks +skylark +skylark's +skylarked +skylarking +skylarks +skylight +skylight's +skylights +skyline +skyline's +skylines +skyrocket +skyrocket's +skyrocketed +skyrocketing +skyrockets +skyscraper +skyscraper's +skyscrapers +skyward +skywards +skywriter +skywriter's +skywriters +skywriting +skywriting's +slab +slab's +slabbed +slabbing +slabs +slack +slack's +slacked +slacken +slackened +slackening +slackens +slacker +slacker's +slackers +slackest +slacking +slackly +slackness +slackness's +slacks +slacks's +slag +slag's +slags +slain +slake +slaked +slakes +slaking +slalom +slalom's +slalomed +slaloming +slaloms +slam +slam's +slammed +slammer +slammer's +slammers +slamming +slams +slander +slander's +slandered +slanderer +slanderer's +slanderers +slandering +slanderous +slanders +slang +slang's +slangier +slangiest +slangy +slant +slant's +slanted +slanting +slants +slantwise +slap +slap's +slapdash +slaphappy +slapped +slapping +slaps +slapstick +slapstick's +slash +slash's +slashed +slashes +slashing +slat +slat's +slate +slate's +slated +slates +slather +slathered +slathering +slathers +slating +slats +slattern +slattern's +slatternly +slatterns +slaughter +slaughter's +slaughtered +slaughterer +slaughterer's +slaughterers +slaughterhouse +slaughterhouse's +slaughterhouses +slaughtering +slaughters +slave +slave's +slaved +slaver +slaver's +slavered +slavering +slavers +slavery +slavery's +slaves +slaving +slavish +slavishly +slaw +slaw's +slay +slayer +slayer's +slayers +slaying +slaying's +slayings +slays +sleaze +sleaze's +sleazes +sleazier +sleaziest +sleazily +sleaziness +sleaziness's +sleazy +sled +sled's +sledded +sledding +sledge +sledge's +sledged +sledgehammer +sledgehammer's +sledgehammered +sledgehammering +sledgehammers +sledges +sledging +sleds +sleek +sleeked +sleeker +sleekest +sleeking +sleekly +sleekness +sleekness's +sleeks +sleep +sleep's +sleeper +sleeper's +sleepers +sleepier +sleepiest +sleepily +sleepiness +sleepiness's +sleeping +sleepless +sleeplessness +sleeplessness's +sleeps +sleepwalk +sleepwalked +sleepwalker +sleepwalker's +sleepwalkers +sleepwalking +sleepwalking's +sleepwalks +sleepwear +sleepwear's +sleepy +sleepyhead +sleepyhead's +sleepyheads +sleet +sleet's +sleeted +sleeting +sleets +sleety +sleeve +sleeve's +sleeveless +sleeves +sleigh +sleigh's +sleighed +sleighing +sleighs +slender +slenderer +slenderest +slenderize +slenderized +slenderizes +slenderizing +slenderness +slenderness's +slept +sleuth +sleuth's +sleuths +slew +slew's +slewed +slewing +slews +slice +slice's +sliced +slicer +slicer's +slicers +slices +slicing +slick +slick's +slicked +slicker +slicker's +slickers +slickest +slicking +slickly +slickness +slickness's +slicks +slid +slide +slide's +slider +slider's +sliders +slides +slideshow +slideshow's +slideshows +sliding +slier +sliest +slight +slight's +slighted +slighter +slightest +slighting +slightly +slightness +slightness's +slights +slily +slim +slime +slime's +slimier +slimiest +slimmed +slimmer +slimmest +slimming +slimness +slimness's +slims +slimy +sling +sling's +slinging +slings +slingshot +slingshot's +slingshots +slink +slinked +slinkier +slinkiest +slinking +slinks +slinky +slip +slip's +slipcover +slipcover's +slipcovers +slipknot +slipknot's +slipknots +slippage +slippage's +slippages +slipped +slipper +slipper's +slipperier +slipperiest +slipperiness +slipperiness's +slippers +slippery +slipping +slips +slipshod +slit +slit's +slither +slither's +slithered +slithering +slithers +slithery +slits +slitter +slitting +sliver +sliver's +slivered +slivering +slivers +slob +slob's +slobber +slobber's +slobbered +slobbering +slobbers +slobs +sloe +sloe's +sloes +slog +slog's +slogan +slogan's +slogans +slogged +slogging +slogs +sloop +sloop's +sloops +slop +slop's +slope +slope's +sloped +slopes +sloping +slopped +sloppier +sloppiest +sloppily +sloppiness +sloppiness's +slopping +sloppy +slops +slosh +sloshed +sloshes +sloshing +slot +slot's +sloth +sloth's +slothful +slothfulness +slothfulness's +sloths +slots +slotted +slotting +slouch +slouch's +slouched +slouches +slouchier +slouchiest +slouching +slouchy +slough +slough's +sloughed +sloughing +sloughs +sloven +sloven's +slovenlier +slovenliest +slovenliness +slovenliness's +slovenly +slovens +slow +slowdown +slowdown's +slowdowns +slowed +slower +slowest +slowing +slowly +slowness +slowness's +slowpoke +slowpoke's +slowpokes +slows +sludge +sludge's +slue +slue's +slued +slues +slug +slug's +sluggard +sluggard's +sluggards +slugged +slugger +slugger's +sluggers +slugging +sluggish +sluggishly +sluggishness +sluggishness's +slugs +sluice +sluice's +sluiced +sluices +sluicing +sluing +slum +slum's +slumber +slumber's +slumbered +slumbering +slumberous +slumbers +slumbrous +slumdog +slumdog's +slumdogs +slumlord +slumlord's +slumlords +slummed +slummer +slumming +slump +slump's +slumped +slumping +slumps +slums +slung +slunk +slur +slur's +slurp +slurp's +slurped +slurping +slurps +slurred +slurring +slurs +slush +slush's +slushier +slushiest +slushy +slut +slut's +sluts +sluttish +sly +slyer +slyest +slyly +slyness +slyness's +smack +smack's +smacked +smacker +smacker's +smackers +smacking +smacks +small +small's +smaller +smallest +smallish +smallness +smallness's +smallpox +smallpox's +smalls +smarmier +smarmiest +smarmy +smart +smart's +smarted +smarten +smartened +smartening +smartens +smarter +smartest +smarting +smartly +smartness +smartness's +smartphone +smartphone's +smartphones +smarts +smarts's +smartwatch +smartwatch's +smartwatches +smash +smash's +smashed +smashes +smashing +smattering +smattering's +smatterings +smear +smear's +smeared +smearing +smears +smell +smell's +smelled +smellier +smelliest +smelling +smells +smelly +smelt +smelt's +smelted +smelter +smelter's +smelters +smelting +smelts +smidge +smidge's +smidgen +smidgen's +smidgens +smidgeon +smidgeon's +smidgeons +smidges +smidgin +smidgin's +smidgins +smile +smile's +smiled +smiles +smiling +smilingly +smirch +smirch's +smirched +smirches +smirching +smirk +smirk's +smirked +smirking +smirks +smit +smite +smites +smith +smith's +smithereens +smithereens's +smithies +smiths +smithy +smithy's +smiting +smitten +smock +smock's +smocked +smocking +smocking's +smocks +smog +smog's +smoggier +smoggiest +smoggy +smoke +smoke's +smoked +smokehouse +smokehouse's +smokehouses +smokeless +smoker +smoker's +smokers +smokes +smokestack +smokestack's +smokestacks +smokier +smokiest +smokiness +smokiness's +smoking +smoking's +smoky +smolder +smolder's +smoldered +smoldering +smolders +smooch +smooch's +smooched +smooches +smooching +smooth +smoothed +smoother +smoothes +smoothest +smoothie +smoothie's +smoothies +smoothing +smoothly +smoothness +smoothness's +smooths +smoothy +smoothy's +smote +smother +smother's +smothered +smothering +smothers +smoulder +smoulder's +smouldered +smouldering +smoulders +smudge +smudge's +smudged +smudges +smudgier +smudgiest +smudging +smudgy +smug +smugger +smuggest +smuggle +smuggled +smuggler +smuggler's +smugglers +smuggles +smuggling +smuggling's +smugly +smugness +smugness's +smut +smut's +smuts +smuttier +smuttiest +smutty +smörgåsbord +smörgåsbord's +smörgåsbords +snack +snack's +snacked +snacking +snacks +snaffle +snaffle's +snaffled +snaffles +snaffling +snafu +snafu's +snafus +snag +snag's +snagged +snagging +snags +snail +snail's +snailed +snailing +snails +snake +snake's +snakebite +snakebite's +snakebites +snaked +snakes +snakier +snakiest +snaking +snaky +snap +snap's +snapdragon +snapdragon's +snapdragons +snapped +snapper +snapper's +snappers +snappier +snappiest +snapping +snappish +snappy +snaps +snapshot +snapshot's +snapshots +snare +snare's +snared +snares +snaring +snarkier +snarkiest +snarky +snarl +snarl's +snarled +snarling +snarls +snatch +snatch's +snatched +snatches +snatching +snazzier +snazziest +snazzy +sneak +sneak's +sneaked +sneaker +sneaker's +sneakers +sneakier +sneakiest +sneaking +sneaks +sneaky +sneer +sneer's +sneered +sneering +sneeringly +sneers +sneeze +sneeze's +sneezed +sneezes +sneezing +snicker +snicker's +snickered +snickering +snickers +snide +snider +snidest +sniff +sniff's +sniffed +sniffing +sniffle +sniffle's +sniffled +sniffles +sniffling +sniffs +snifter +snifter's +snifters +snigger +snigger's +sniggered +sniggering +sniggers +snip +snip's +snipe +snipe's +sniped +sniper +sniper's +snipers +snipes +sniping +snipped +snippet +snippet's +snippets +snippier +snippiest +snipping +snippy +snips +snit +snit's +snitch +snitch's +snitched +snitches +snitching +snits +snivel +snivel's +sniveled +sniveling +snivelled +snivelling +snivels +snob +snob's +snobbery +snobbery's +snobbier +snobbiest +snobbish +snobbishness +snobbishness's +snobby +snobs +snooker +snoop +snoop's +snooped +snooper +snooper's +snoopers +snoopier +snoopiest +snooping +snoops +snoopy +snoot +snoot's +snootier +snootiest +snootiness +snootiness's +snoots +snooty +snooze +snooze's +snoozed +snoozes +snoozing +snore +snore's +snored +snorer +snorer's +snorers +snores +snoring +snorkel +snorkel's +snorkeled +snorkeler +snorkeler's +snorkelers +snorkeling +snorkeling's +snorkelled +snorkelling +snorkels +snort +snort's +snorted +snorting +snorts +snot +snot's +snots +snottier +snottiest +snotty +snout +snout's +snouts +snow +snow's +snowball +snowball's +snowballed +snowballing +snowballs +snowblower +snowblower's +snowblowers +snowboard +snowboard's +snowboarded +snowboarding +snowboarding's +snowboards +snowbound +snowdrift +snowdrift's +snowdrifts +snowdrop +snowdrop's +snowdrops +snowed +snowfall +snowfall's +snowfalls +snowflake +snowflake's +snowflakes +snowier +snowiest +snowing +snowman +snowman's +snowmen +snowmobile +snowmobile's +snowmobiled +snowmobiles +snowmobiling +snowplow +snowplow's +snowplowed +snowplowing +snowplows +snows +snowshed +snowshoe +snowshoe's +snowshoeing +snowshoes +snowstorm +snowstorm's +snowstorms +snowsuit +snowsuit's +snowsuits +snowy +snub +snub's +snubbed +snubbing +snubs +snuck +snuff +snuff's +snuffbox +snuffbox's +snuffboxes +snuffed +snuffer +snuffer's +snuffers +snuffing +snuffle +snuffle's +snuffled +snuffles +snuffling +snuffs +snug +snug's +snugged +snugger +snuggest +snugging +snuggle +snuggle's +snuggled +snuggles +snuggling +snugly +snugs +so +so's +soak +soak's +soaked +soaking +soaking's +soakings +soaks +soap +soap's +soapbox +soapbox's +soapboxes +soaped +soapier +soapiest +soapiness +soapiness's +soaping +soaps +soapstone +soapstone's +soapsuds +soapsuds's +soapy +soar +soar's +soared +soaring +soars +sob +sob's +sobbed +sobbing +sober +sobered +soberer +soberest +sobering +soberly +soberness +soberness's +sobers +sobriety +sobriety's +sobriquet +sobriquet's +sobriquets +sobs +soccer +soccer's +sociability +sociability's +sociable +sociable's +sociables +sociably +social +social's +socialism +socialism's +socialist +socialist's +socialistic +socialists +socialite +socialite's +socialites +socialization +socialization's +socialize +socialized +socializes +socializing +socially +socials +societal +societies +society +society's +socioeconomic +sociological +sociologist +sociologist's +sociologists +sociology +sociology's +sociopath +sociopath's +sociopaths +sock +sock's +socked +socket +socket's +sockets +socking +socks +sod +sod's +soda +soda's +sodas +sodded +sodden +sodding +sodium +sodium's +sodomite +sodomite's +sodomites +sodomy +sodomy's +sods +sofa +sofa's +sofas +soft +softball +softball's +softballs +soften +softened +softener +softener's +softeners +softening +softens +softer +softest +softhearted +softie +softie's +softies +softly +softness +softness's +software +software's +softwood +softwood's +softwoods +softy +softy's +soggier +soggiest +soggily +sogginess +sogginess's +soggy +soil +soil's +soiled +soiling +soils +soirée +soirée's +soirées +sojourn +sojourn's +sojourned +sojourning +sojourns +sol +sol's +solace +solace's +solaced +solaces +solacing +solar +solaria +solarium +solarium's +solariums +sold +solder +solder's +soldered +soldering +solders +soldier +soldier's +soldiered +soldiering +soldierly +soldiers +sole +sole's +solecism +solecism's +solecisms +soled +solely +solemn +solemner +solemnest +solemnity +solemnity's +solemnize +solemnized +solemnizes +solemnizing +solemnly +solenoid +solenoid's +solenoids +soles +soli +solicit +solicitation +solicitation's +solicitations +solicited +soliciting +solicitor +solicitor's +solicitors +solicitous +solicitously +solicits +solicitude +solicitude's +solid +solid's +solidarity +solidarity's +solider +solidest +solidification +solidification's +solidified +solidifies +solidify +solidifying +solidity +solidity's +solidly +solidness +solidness's +solids +soliloquies +soliloquize +soliloquized +soliloquizes +soliloquizing +soliloquy +soliloquy's +soling +solitaire +solitaire's +solitaires +solitaries +solitary +solitary's +solitude +solitude's +solo +solo's +soloed +soloing +soloist +soloist's +soloists +solos +sols +solstice +solstice's +solstices +solubility +solubility's +soluble +soluble's +solubles +solution +solution's +solutions +solvable +solve +solved +solvency +solvency's +solvent +solvent's +solvents +solver +solver's +solvers +solves +solving +somber +somberly +sombre +sombrely +sombrero +sombrero's +sombreros +some +somebodies +somebody +somebody's +someday +somehow +someone +someone's +someones +someplace +somersault +somersault's +somersaulted +somersaulting +somersaults +something +something's +somethings +sometime +sometimes +someway +somewhat +somewhats +somewhere +somnambulism +somnambulism's +somnambulist +somnambulist's +somnambulists +somnolence +somnolence's +somnolent +son +son's +sonar +sonar's +sonars +sonata +sonata's +sonatas +song +song's +songbird +songbird's +songbirds +songs +songster +songster's +songsters +songwriter +songwriter's +songwriters +sonic +sonnet +sonnet's +sonnets +sonnies +sonny +sonny's +sonority +sonority's +sonorous +sons +soon +sooner +soonest +soot +soot's +sooth +sooth's +soothe +soothed +soothes +soothing +soothingly +soothsayer +soothsayer's +soothsayers +sootier +sootiest +sooty +sop +sop's +sophism +sophism's +sophist +sophist's +sophisticate +sophisticate's +sophisticated +sophisticates +sophisticating +sophistication +sophistication's +sophistries +sophistry +sophistry's +sophists +sophomore +sophomore's +sophomores +sophomoric +soporific +soporific's +soporifics +sopped +soppier +soppiest +sopping +soppy +soprano +soprano's +sopranos +sops +sorbet +sorbet's +sorbets +sorcerer +sorcerer's +sorcerers +sorceress +sorceress's +sorceresses +sorcery +sorcery's +sordid +sordidly +sordidness +sordidness's +sore +sore's +sorehead +sorehead's +soreheads +sorely +soreness +soreness's +sorer +sores +sorest +sorghum +sorghum's +sororities +sorority +sorority's +sorrel +sorrel's +sorrels +sorrier +sorriest +sorrow +sorrow's +sorrowed +sorrowful +sorrowfully +sorrowing +sorrows +sorry +sort +sort's +sorta +sorted +sorter +sorter's +sorters +sortie +sortie's +sortied +sortieing +sorties +sorting +sorts +sos +sot +sot's +sots +sottish +sou'wester +soubriquet +soubriquet's +soubriquets +soufflé +soufflé's +soufflés +sough +sough's +soughed +soughing +soughs +sought +soul +soul's +soulful +soulfully +soulfulness +soulfulness's +soulless +soulmate +soulmate's +soulmates +souls +sound +sound's +sounded +sounder +soundest +sounding +sounding's +soundings +soundless +soundlessly +soundly +soundness +soundness's +soundproof +soundproofed +soundproofing +soundproofs +sounds +soundtrack +soundtrack's +soundtracks +soup +soup's +souped +soupier +soupiest +souping +soups +soupy +soupçon +soupçon's +soupçons +sour +sour's +source +source's +sourced +sources +sourcing +sourdough +sourdough's +sourdoughs +soured +sourer +sourest +souring +sourly +sourness +sourness's +sourpuss +sourpuss's +sourpusses +sours +souse +souse's +soused +souses +sousing +south +south's +southbound +southeast +southeast's +southeasterly +southeastern +southeastward +southerlies +southerly +southerly's +southern +southern's +southerner +southerner's +southerners +southernmost +southerns +southpaw +southpaw's +southpaws +southward +southward's +southwards +southwest +southwest's +southwester +southwester's +southwesterly +southwestern +southwesters +southwestward +souvenir +souvenir's +souvenirs +sovereign +sovereign's +sovereigns +sovereignty +sovereignty's +soviet +soviet's +soviets +sow +sow's +sowed +sower +sower's +sowers +sowing +sown +sows +sox +soy +soy's +soya +soya's +soybean +soybean's +soybeans +spa +spa's +space +space's +spacecraft +spacecraft's +spacecrafts +spaced +spaceflight +spaceflight's +spaceflights +spaceman +spaceman's +spacemen +spaces +spaceship +spaceship's +spaceships +spacesuit +spacesuit's +spacesuits +spacewalk +spacewalk's +spacewalked +spacewalking +spacewalks +spacey +spacial +spacier +spaciest +spacing +spacing's +spacious +spaciously +spaciousness +spaciousness's +spacy +spade +spade's +spaded +spadeful +spadeful's +spadefuls +spades +spadework +spadework's +spading +spaghetti +spaghetti's +spake +spam +spam's +spammed +spammer +spammer's +spammers +spamming +spams +span +span's +spandex +spandex's +spangle +spangle's +spangled +spangles +spangling +spaniel +spaniel's +spaniels +spank +spank's +spanked +spanking +spanking's +spankings +spanks +spanned +spanner +spanner's +spanners +spanning +spans +spar +spar's +spare +spare's +spared +sparely +spareness +spareness's +sparer +spareribs +spareribs's +spares +sparest +sparing +sparingly +spark +spark's +sparked +sparking +sparkle +sparkle's +sparkled +sparkler +sparkler's +sparklers +sparkles +sparkling +sparks +sparred +sparring +sparrow +sparrow's +sparrows +spars +sparse +sparsely +sparseness +sparseness's +sparser +sparsest +sparsity +sparsity's +spartan +spas +spasm +spasm's +spasmodic +spasmodically +spasms +spastic +spastic's +spastics +spat +spat's +spate +spate's +spates +spatial +spatially +spats +spatted +spatter +spatter's +spattered +spattering +spatters +spatting +spatula +spatula's +spatulas +spawn +spawn's +spawned +spawning +spawns +spay +spayed +spaying +spays +speak +speakeasies +speakeasy +speakeasy's +speaker +speaker's +speakers +speaking +speaks +spear +spear's +speared +spearhead +spearhead's +spearheaded +spearheading +spearheads +spearing +spearmint +spearmint's +spears +spec +spec's +specced +speccing +special +special's +specialist +specialist's +specialists +specialization +specialization's +specializations +specialize +specialized +specializes +specializing +specially +specials +specialties +specialty +specialty's +specie +specie's +species +species's +specifiable +specific +specific's +specifically +specification +specification's +specifications +specifics +specified +specifier +specifiers +specifies +specify +specifying +specimen +specimen's +specimens +specious +speciously +speck +speck's +specked +specking +speckle +speckle's +speckled +speckles +speckling +specks +specs +specs's +spectacle +spectacle's +spectacles +spectacles's +spectacular +spectacular's +spectacularly +spectaculars +spectator +spectator's +spectators +specter +specter's +specters +spectra +spectral +spectroscope +spectroscope's +spectroscopes +spectroscopic +spectroscopy +spectroscopy's +spectrum +spectrum's +spectrums +speculate +speculated +speculates +speculating +speculation +speculation's +speculations +speculative +speculator +speculator's +speculators +sped +speech +speech's +speeches +speechless +speed +speed's +speedboat +speedboat's +speedboats +speeded +speeder +speeder's +speeders +speedier +speediest +speedily +speeding +speeding's +speedometer +speedometer's +speedometers +speeds +speedster +speedster's +speedsters +speedup +speedup's +speedups +speedway +speedway's +speedways +speedy +spell +spell's +spellbind +spellbinder +spellbinder's +spellbinders +spellbinding +spellbinds +spellbound +spellcheck +spellcheck's +spellchecked +spellchecker +spellchecker's +spellcheckers +spellchecking +spellchecks +spelled +speller +speller's +spellers +spelling +spelling's +spellings +spells +spelt +spelunker +spelunker's +spelunkers +spend +spender +spender's +spenders +spending +spending's +spends +spendthrift +spendthrift's +spendthrifts +spent +sperm +sperm's +spermatozoa +spermatozoon +spermatozoon's +spermicide +spermicide's +spermicides +sperms +spew +spew's +spewed +spewing +spews +sphere +sphere's +spheres +spherical +spheroid +spheroid's +spheroidal +spheroids +sphincter +sphincter's +sphincters +sphinges +sphinx +sphinx's +sphinxes +spice +spice's +spiced +spices +spicier +spiciest +spiciness +spiciness's +spicing +spicy +spider +spider's +spiders +spidery +spied +spiel +spiel's +spieled +spieling +spiels +spies +spiffier +spiffiest +spiffy +spigot +spigot's +spigots +spike +spike's +spiked +spikes +spikier +spikiest +spiking +spiky +spill +spill's +spillage +spillage's +spillages +spilled +spilling +spills +spillway +spillway's +spillways +spilt +spin +spin's +spinach +spinach's +spinal +spinal's +spinals +spindle +spindle's +spindled +spindles +spindlier +spindliest +spindling +spindly +spine +spine's +spineless +spines +spinet +spinet's +spinets +spinier +spiniest +spinnaker +spinnaker's +spinnakers +spinner +spinner's +spinners +spinning +spinoff +spinoff's +spinoffs +spins +spinster +spinster's +spinsterhood +spinsterhood's +spinsters +spiny +spiraea +spiraea's +spiraeas +spiral +spiral's +spiraled +spiraling +spiralled +spiralling +spirally +spirals +spire +spire's +spirea +spirea's +spireas +spires +spirit +spirit's +spirited +spiriting +spiritless +spirits +spiritual +spiritual's +spiritualism +spiritualism's +spiritualist +spiritualist's +spiritualistic +spiritualists +spirituality +spirituality's +spiritually +spirituals +spirituous +spit +spit's +spitball +spitball's +spitballs +spite +spite's +spited +spiteful +spitefuller +spitefullest +spitefully +spitefulness +spitefulness's +spites +spitfire +spitfire's +spitfires +spiting +spits +spitted +spitting +spittle +spittle's +spittoon +spittoon's +spittoons +splash +splash's +splashdown +splashdown's +splashdowns +splashed +splashes +splashier +splashiest +splashing +splashy +splat +splat's +splats +splatted +splatter +splatter's +splattered +splattering +splatters +splatting +splay +splay's +splayed +splaying +splays +spleen +spleen's +spleens +splendid +splendider +splendidest +splendidly +splendor +splendor's +splenetic +splice +splice's +spliced +splicer +splicer's +splicers +splices +splicing +spline +splines +splint +splint's +splinted +splinter +splinter's +splintered +splintering +splinters +splinting +splints +split +split's +splits +splitting +splitting's +splittings +splodge +splotch +splotch's +splotched +splotches +splotchier +splotchiest +splotching +splotchy +splurge +splurge's +splurged +splurges +splurging +splutter +splutter's +spluttered +spluttering +splutters +spoil +spoil's +spoilage +spoilage's +spoiled +spoiler +spoiler's +spoilers +spoiling +spoils +spoilsport +spoilsport's +spoilsports +spoilt +spoke +spoke's +spoken +spokes +spokesman +spokesman's +spokesmen +spokespeople +spokesperson +spokesperson's +spokespersons +spokeswoman +spokeswoman's +spokeswomen +spoliation +spoliation's +sponge +sponge's +sponged +sponger +sponger's +spongers +sponges +spongier +spongiest +sponging +spongy +sponsor +sponsor's +sponsored +sponsoring +sponsors +sponsorship +sponsorship's +spontaneity +spontaneity's +spontaneous +spontaneously +spoof +spoof's +spoofed +spoofing +spoofs +spook +spook's +spooked +spookier +spookiest +spooking +spooks +spooky +spool +spool's +spooled +spooling +spools +spoon +spoon's +spoonbill +spoonbill's +spoonbills +spooned +spoonerism +spoonerism's +spoonerisms +spoonful +spoonful's +spoonfuls +spooning +spoons +spoonsful +spoor +spoor's +spoored +spooring +spoors +sporadic +sporadically +spore +spore's +spored +spores +sporing +sporran +sport +sport's +sported +sportier +sportiest +sporting +sportive +sports +sportscast +sportscast's +sportscaster +sportscaster's +sportscasters +sportscasting +sportscasts +sportsman +sportsman's +sportsmanlike +sportsmanship +sportsmanship's +sportsmen +sportswear +sportswear's +sportswoman +sportswoman's +sportswomen +sporty +spot +spot's +spotless +spotlessly +spotlessness +spotlessness's +spotlight +spotlight's +spotlighted +spotlighting +spotlights +spots +spotted +spotter +spotter's +spotters +spottier +spottiest +spottiness +spottiness's +spotting +spotty +spouse +spouse's +spouses +spout +spout's +spouted +spouting +spouts +sprain +sprain's +sprained +spraining +sprains +sprang +sprat +sprat's +sprats +sprawl +sprawl's +sprawled +sprawling +sprawls +spray +spray's +sprayed +sprayer +sprayer's +sprayers +spraying +sprays +spread +spread's +spreader +spreader's +spreaders +spreading +spreads +spreadsheet +spreadsheet's +spreadsheets +spree +spree's +spreed +spreeing +sprees +sprier +spriest +sprig +sprig's +sprightlier +sprightliest +sprightliness +sprightliness's +sprightly +sprigs +spring +spring's +springboard +springboard's +springboards +springier +springiest +springiness +springiness's +springing +springs +springtime +springtime's +springy +sprinkle +sprinkle's +sprinkled +sprinkler +sprinkler's +sprinklers +sprinkles +sprinkling +sprinkling's +sprinklings +sprint +sprint's +sprinted +sprinter +sprinter's +sprinters +sprinting +sprints +sprite +sprite's +sprites +spritz +spritz's +spritzed +spritzes +spritzing +sprocket +sprocket's +sprockets +sprout +sprout's +sprouted +sprouting +sprouts +spruce +spruce's +spruced +sprucer +spruces +sprucest +sprucing +sprung +spry +spryer +spryest +spryly +spryness +spryness's +spud +spud's +spuds +spume +spume's +spumed +spumes +spuming +spumone +spumone's +spumoni +spumoni's +spun +spunk +spunk's +spunkier +spunkiest +spunky +spur +spur's +spurious +spuriously +spuriousness +spuriousness's +spurn +spurned +spurning +spurns +spurred +spurring +spurs +spurt +spurt's +spurted +spurting +spurts +sputter +sputter's +sputtered +sputtering +sputters +sputum +sputum's +spy +spy's +spyglass +spyglass's +spyglasses +spying +spyware +spyware's +squab +squab's +squabble +squabble's +squabbled +squabbles +squabbling +squabs +squad +squad's +squadron +squadron's +squadrons +squads +squalid +squalider +squalidest +squall +squall's +squalled +squalling +squalls +squalor +squalor's +squander +squandered +squandering +squanders +square +square's +squared +squarely +squareness +squareness's +squarer +squares +squarest +squaring +squash +squash's +squashed +squashes +squashier +squashiest +squashing +squashy +squat +squat's +squats +squatted +squatter +squatter's +squatters +squattest +squatting +squaw +squaw's +squawk +squawk's +squawked +squawking +squawks +squaws +squeak +squeak's +squeaked +squeakier +squeakiest +squeaking +squeaks +squeaky +squeal +squeal's +squealed +squealer +squealer's +squealers +squealing +squeals +squeamish +squeamishly +squeamishness +squeamishness's +squeegee +squeegee's +squeegeed +squeegeeing +squeegees +squeeze +squeeze's +squeezed +squeezer +squeezer's +squeezers +squeezes +squeezing +squelch +squelch's +squelched +squelches +squelching +squid +squid's +squids +squiggle +squiggle's +squiggled +squiggles +squiggling +squiggly +squint +squint's +squinted +squinter +squintest +squinting +squints +squire +squire's +squired +squires +squiring +squirm +squirm's +squirmed +squirmier +squirmiest +squirming +squirms +squirmy +squirrel +squirrel's +squirreled +squirreling +squirrelled +squirrelling +squirrels +squirt +squirt's +squirted +squirting +squirts +squish +squish's +squished +squishes +squishier +squishiest +squishing +squishy +sriracha +stab +stab's +stabbed +stabbing +stabbing's +stabbings +stability +stability's +stabilization +stabilization's +stabilize +stabilized +stabilizer +stabilizer's +stabilizers +stabilizes +stabilizing +stable +stable's +stabled +stabler +stables +stablest +stabling +stabs +staccati +staccato +staccato's +staccatos +stack +stack's +stacked +stacking +stacks +stadia +stadium +stadium's +stadiums +staff +staff's +staffed +staffer +staffer's +staffers +staffing +staffing's +staffs +stag +stag's +stage +stage's +stagecoach +stagecoach's +stagecoaches +staged +stagehand +stagehand's +stagehands +stages +stagflation +stagflation's +stagger +stagger's +staggered +staggering +staggeringly +staggers +staging +staging's +stagings +stagnant +stagnate +stagnated +stagnates +stagnating +stagnation +stagnation's +stags +staid +staider +staidest +staidly +stain +stain's +stained +staining +stainless +stainless's +stains +stair +stair's +staircase +staircase's +staircases +stairs +stairway +stairway's +stairways +stairwell +stairwell's +stairwells +stake +stake's +staked +stakeout +stakeout's +stakeouts +stakes +staking +stalactite +stalactite's +stalactites +stalagmite +stalagmite's +stalagmites +stale +staled +stalemate +stalemate's +stalemated +stalemates +stalemating +staleness +staleness's +staler +stales +stalest +staling +stalk +stalk's +stalked +stalker +stalker's +stalkers +stalking +stalking's +stalkings +stalks +stall +stall's +stalled +stalling +stallion +stallion's +stallions +stalls +stalwart +stalwart's +stalwarts +stamen +stamen's +stamens +stamina +stamina's +stammer +stammer's +stammered +stammerer +stammerer's +stammerers +stammering +stammers +stamp +stamp's +stamped +stampede +stampede's +stampeded +stampedes +stampeding +stamping +stamps +stance +stance's +stances +stanch +stanched +stancher +stanches +stanchest +stanching +stanchion +stanchion's +stanchions +stand +stand's +standard +standard's +standardization +standardization's +standardize +standardized +standardizes +standardizing +standards +standby +standby's +standbys +standing +standing's +standings +standoff +standoff's +standoffish +standoffs +standout +standout's +standouts +standpoint +standpoint's +standpoints +stands +standstill +standstill's +standstills +stank +stanza +stanza's +stanzas +staph +staph's +staphylococci +staphylococcus +staphylococcus's +staple +staple's +stapled +stapler +stapler's +staplers +staples +stapling +star +star's +starboard +starboard's +starch +starch's +starched +starches +starchier +starchiest +starching +starchy +stardom +stardom's +stare +stare's +stared +stares +starfish +starfish's +starfishes +stargazer +stargazer's +stargazers +staring +stark +starker +starkest +starkly +starkness +starkness's +starless +starlet +starlet's +starlets +starlight +starlight's +starling +starling's +starlings +starlit +starred +starrier +starriest +starring +starry +stars +start +start's +started +starter +starter's +starters +starting +startle +startled +startles +startling +startlingly +starts +startup +startup's +startups +starvation +starvation's +starve +starved +starves +starving +starvings +stash +stash's +stashed +stashes +stashing +state +state's +stated +statehood +statehood's +statehouse +statehouse's +statehouses +stateless +statelier +stateliest +stateliness +stateliness's +stately +statement +statement's +statements +stater +stateroom +stateroom's +staterooms +states +stateside +statesman +statesman's +statesmanlike +statesmanship +statesmanship's +statesmen +statewide +static +static's +statically +stating +station +station's +stationary +stationed +stationer +stationer's +stationers +stationery +stationery's +stationing +stations +statistic +statistic's +statistical +statistically +statistician +statistician's +statisticians +statistics +stats +statuary +statuary's +statue +statue's +statues +statuesque +statuette +statuette's +statuettes +stature +stature's +statures +status +status's +statuses +statute +statute's +statutes +statutory +staunch +staunched +stauncher +staunches +staunchest +staunching +staunchly +stave +stave's +staved +staves +staving +stay +stay's +stayed +staying +stays +stead +stead's +steadfast +steadfastly +steadfastness +steadfastness's +steadied +steadier +steadies +steadiest +steadily +steadiness +steadiness's +steads +steady +steady's +steadying +steak +steak's +steakhouse +steakhouse's +steakhouses +steaks +steal +steal's +stealing +steals +stealth +stealth's +stealthier +stealthiest +stealthily +stealthy +steam +steam's +steamboat +steamboat's +steamboats +steamed +steamer +steamer's +steamers +steamier +steamiest +steaming +steamroll +steamrolled +steamroller +steamroller's +steamrollered +steamrollering +steamrollers +steamrolling +steamrolls +steams +steamship +steamship's +steamships +steamy +steed +steed's +steeds +steel +steel's +steeled +steelier +steeliest +steeling +steels +steely +steep +steep's +steeped +steeper +steepest +steeping +steeple +steeple's +steeplechase +steeplechase's +steeplechases +steeplejack +steeplejack's +steeplejacks +steeples +steeply +steepness +steepness's +steeps +steer +steer's +steerage +steerage's +steered +steering +steering's +steers +stein +stein's +steins +stellar +stem +stem's +stemmed +stemming +stems +stench +stench's +stenches +stencil +stencil's +stenciled +stenciling +stencilled +stencilling +stencils +stenographer +stenographer's +stenographers +stenographic +stenography +stenography's +stent +stent's +stentorian +stents +step +step's +stepbrother +stepbrother's +stepbrothers +stepchild +stepchild's +stepchildren +stepchildren's +stepdad +stepdad's +stepdads +stepdaughter +stepdaughter's +stepdaughters +stepfather +stepfather's +stepfathers +stepladder +stepladder's +stepladders +stepmom +stepmom's +stepmoms +stepmother +stepmother's +stepmothers +stepparent +stepparent's +stepparents +steppe +steppe's +stepped +steppes +stepping +steppingstone +steppingstone's +steppingstones +steps +stepsister +stepsister's +stepsisters +stepson +stepson's +stepsons +stereo +stereo's +stereophonic +stereos +stereoscope +stereoscope's +stereoscopes +stereotype +stereotype's +stereotyped +stereotypes +stereotypical +stereotyping +sterile +sterility +sterility's +sterilization +sterilization's +sterilize +sterilized +sterilizer +sterilizer's +sterilizers +sterilizes +sterilizing +sterling +sterling's +stern +stern's +sterna +sterner +sternest +sternly +sternness +sternness's +sterns +sternum +sternum's +sternums +steroid +steroid's +steroids +stethoscope +stethoscope's +stethoscopes +stevedore +stevedore's +stevedores +stew +stew's +steward +steward's +stewarded +stewardess +stewardess's +stewardesses +stewarding +stewards +stewardship +stewardship's +stewed +stewing +stews +stick +stick's +sticker +sticker's +stickers +stickier +stickies +stickiest +stickiness +stickiness's +sticking +stickleback +stickleback's +sticklebacks +stickler +stickler's +sticklers +stickpin +stickpin's +stickpins +sticks +stickup +stickup's +stickups +sticky +sticky's +sties +stiff +stiff's +stiffed +stiffen +stiffened +stiffener +stiffener's +stiffeners +stiffening +stiffens +stiffer +stiffest +stiffing +stiffly +stiffness +stiffness's +stiffs +stifle +stifled +stifles +stifling +stiflings +stigma +stigma's +stigmas +stigmata +stigmatize +stigmatized +stigmatizes +stigmatizing +stile +stile's +stiles +stiletto +stiletto's +stilettoes +stilettos +still +still's +stillbirth +stillbirth's +stillbirths +stillborn +stilled +stiller +stillest +stilling +stillness +stillness's +stills +stilt +stilt's +stilted +stilts +stimulant +stimulant's +stimulants +stimulate +stimulated +stimulates +stimulating +stimulation +stimulation's +stimuli +stimulus +stimulus's +sting +sting's +stinger +stinger's +stingers +stingier +stingiest +stingily +stinginess +stinginess's +stinging +stingray +stingray's +stingrays +stings +stingy +stink +stink's +stinker +stinker's +stinkers +stinking +stinks +stint +stint's +stinted +stinting +stints +stipend +stipend's +stipends +stipple +stipple's +stippled +stipples +stippling +stipulate +stipulated +stipulates +stipulating +stipulation +stipulation's +stipulations +stir +stir's +stirred +stirrer +stirrer's +stirrers +stirring +stirrings +stirrup +stirrup's +stirrups +stirs +stitch +stitch's +stitched +stitches +stitching +stitching's +stoat +stoat's +stoats +stochastic +stock +stock's +stockade +stockade's +stockaded +stockades +stockading +stockbroker +stockbroker's +stockbrokers +stocked +stockholder +stockholder's +stockholders +stockier +stockiest +stockiness +stockiness's +stocking +stocking's +stockings +stockpile +stockpile's +stockpiled +stockpiles +stockpiling +stockroom +stockroom's +stockrooms +stocks +stocky +stockyard +stockyard's +stockyards +stodgier +stodgiest +stodginess +stodginess's +stodgy +stoic +stoic's +stoical +stoically +stoicism +stoicism's +stoics +stoke +stoked +stoker +stoker's +stokers +stokes +stoking +stole +stole's +stolen +stoles +stolid +stolider +stolidest +stolidity +stolidity's +stolidly +stomach +stomach's +stomachache +stomachache's +stomachaches +stomached +stomaching +stomachs +stomp +stomp's +stomped +stomping +stomps +stone +stone's +stoned +stoner +stoner's +stoners +stones +stonewall +stonewalled +stonewalling +stonewalls +stoneware +stoneware's +stonework +stonework's +stoney +stonier +stoniest +stonily +stoning +stony +stood +stooge +stooge's +stooges +stool +stool's +stools +stoop +stoop's +stooped +stooping +stoops +stop +stop's +stopcock +stopcock's +stopcocks +stopgap +stopgap's +stopgaps +stoplight +stoplight's +stoplights +stopover +stopover's +stopovers +stoppable +stoppage +stoppage's +stoppages +stopped +stopper +stopper's +stoppered +stoppering +stoppers +stopping +stops +stopwatch +stopwatch's +stopwatches +storage +storage's +store +store's +stored +storefront +storefront's +storefronts +storehouse +storehouse's +storehouses +storekeeper +storekeeper's +storekeepers +storeroom +storeroom's +storerooms +stores +storey +storey's +storeys +storied +stories +storing +stork +stork's +storks +storm +storm's +stormed +stormier +stormiest +stormily +storminess +storminess's +storming +storms +stormy +story +story's +storybook +storybook's +storybooks +storyteller +storyteller's +storytellers +stout +stout's +stouter +stoutest +stoutly +stoutness +stoutness's +stove +stove's +stovepipe +stovepipe's +stovepipes +stoves +stow +stowaway +stowaway's +stowaways +stowed +stowing +stows +straddle +straddle's +straddled +straddles +straddling +strafe +strafe's +strafed +strafes +strafing +straggle +straggled +straggler +straggler's +stragglers +straggles +stragglier +straggliest +straggling +straggly +straight +straight's +straightaway +straightaway's +straightaways +straightedge +straightedge's +straightedges +straighten +straightened +straightening +straightens +straighter +straightest +straightforward +straightforwardly +straightjacket +straightjacket's +straightjacketed +straightjacketing +straightjackets +straightness +straightness's +straights +strain +strain's +strained +strainer +strainer's +strainers +straining +strains +strait +strait's +straiten +straitened +straitening +straitens +straitjacket +straitjacket's +straitjacketed +straitjacketing +straitjackets +straits +strand +strand's +stranded +stranding +strands +strange +strangely +strangeness +strangeness's +stranger +stranger's +strangers +strangest +strangle +strangled +stranglehold +stranglehold's +strangleholds +strangler +strangler's +stranglers +strangles +strangling +strangulate +strangulated +strangulates +strangulating +strangulation +strangulation's +strap +strap's +strapless +strapless's +straplesses +strapped +strapping +strapping's +straps +strata +stratagem +stratagem's +stratagems +strategic +strategically +strategies +strategist +strategist's +strategists +strategy +strategy's +stratification +stratification's +stratified +stratifies +stratify +stratifying +stratosphere +stratosphere's +stratospheres +stratum +stratum's +stratums +straw +straw's +strawberries +strawberry +strawberry's +strawed +strawing +straws +stray +stray's +strayed +straying +strays +streak +streak's +streaked +streakier +streakiest +streaking +streaks +streaky +stream +stream's +streamed +streamer +streamer's +streamers +streaming +streamline +streamlined +streamlines +streamlining +streams +street +street's +streetcar +streetcar's +streetcars +streetlight +streetlight's +streetlights +streets +streetwalker +streetwalker's +streetwalkers +streetwise +strength +strength's +strengthen +strengthened +strengthening +strengthens +strengths +strenuous +strenuously +strenuousness +strenuousness's +strep +strep's +streptococcal +streptococci +streptococcus +streptococcus's +streptomycin +streptomycin's +stress +stress's +stressed +stresses +stressful +stressing +stretch +stretch's +stretched +stretcher +stretcher's +stretchers +stretches +stretchier +stretchiest +stretching +stretchy +strew +strewed +strewing +strewn +strews +striated +stricken +strict +stricter +strictest +strictly +strictness +strictness's +stricture +stricture's +strictures +stridden +stride +stride's +strident +stridently +strides +striding +strife +strife's +strike +strike's +strikeout +strikeout's +strikeouts +striker +striker's +strikers +strikes +striking +strikingly +strikings +string +string's +stringed +stringency +stringency's +stringent +stringently +stringer +stringer's +stringers +stringier +stringiest +stringing +strings +stringy +strip +strip's +stripe +stripe's +striped +stripes +striping +stripling +stripling's +striplings +stripped +stripper +stripper's +strippers +stripping +strips +stript +striptease +striptease's +stripteased +stripteases +stripteasing +strive +strived +striven +strives +striving +strobe +strobe's +strobes +strode +stroke +stroke's +stroked +strokes +stroking +stroll +stroll's +strolled +stroller +stroller's +strollers +strolling +strolls +strong +strongbox +strongbox's +strongboxes +stronger +strongest +stronghold +stronghold's +strongholds +strongly +strontium +strontium's +strop +strop's +strophe +strophe's +strophes +stropped +stropping +strops +strove +struck +structural +structuralist +structurally +structure +structure's +structured +structures +structuring +strudel +strudel's +strudels +struggle +struggle's +struggled +struggles +struggling +strum +strum's +strummed +strumming +strumpet +strumpet's +strumpets +strums +strung +strut +strut's +struts +strutted +strutting +strychnine +strychnine's +stub +stub's +stubbed +stubbier +stubbiest +stubbing +stubble +stubble's +stubbly +stubborn +stubborner +stubbornest +stubbornly +stubbornness +stubbornness's +stubby +stubs +stucco +stucco's +stuccoed +stuccoes +stuccoing +stuccos +stuck +stud +stud's +studded +studding +student +student's +students +studentship +studentships +studied +studies +studio +studio's +studios +studious +studiously +studs +study +study's +studying +stuff +stuff's +stuffed +stuffier +stuffiest +stuffily +stuffiness +stuffiness's +stuffing +stuffing's +stuffings +stuffs +stuffy +stultification +stultification's +stultified +stultifies +stultify +stultifying +stumble +stumble's +stumbled +stumbler +stumbler's +stumblers +stumbles +stumbling +stump +stump's +stumped +stumpier +stumpiest +stumping +stumps +stumpy +stun +stung +stunk +stunned +stunning +stunningly +stuns +stunt +stunt's +stunted +stunting +stunts +stupefaction +stupefaction's +stupefied +stupefies +stupefy +stupefying +stupendous +stupendously +stupid +stupid's +stupider +stupidest +stupidities +stupidity +stupidity's +stupidly +stupids +stupor +stupor's +stupors +sturdier +sturdiest +sturdily +sturdiness +sturdiness's +sturdy +sturgeon +sturgeon's +sturgeons +stutter +stutter's +stuttered +stutterer +stutterer's +stutterers +stuttering +stutters +sty +sty's +stye +stye's +styes +style +style's +styled +styles +styli +styling +stylish +stylishly +stylishness +stylishness's +stylist +stylist's +stylistic +stylistically +stylists +stylize +stylized +stylizes +stylizing +stylus +stylus's +styluses +stymie +stymie's +stymied +stymieing +stymies +stymying +styptic +styptic's +styptics +suave +suavely +suaver +suavest +suavity +suavity's +sub +sub's +subatomic +subbasement +subbasement's +subbasements +subbed +subbing +subclass +subcommittee +subcommittee's +subcommittees +subcompact +subcompact's +subcompacts +subconscious +subconscious's +subconsciously +subcontinent +subcontinent's +subcontinents +subcontract +subcontract's +subcontracted +subcontracting +subcontractor +subcontractor's +subcontractors +subcontracts +subculture +subculture's +subcultures +subcutaneous +subdivide +subdivided +subdivides +subdividing +subdivision +subdivision's +subdivisions +subdue +subdued +subdues +subduing +subgroup +subgroup's +subgroups +subhead +subhead's +subheading +subheading's +subheadings +subheads +subhuman +subhuman's +subhumans +subject +subject's +subjected +subjecting +subjection +subjection's +subjective +subjectively +subjectivity +subjectivity's +subjects +subjoin +subjoined +subjoining +subjoins +subjugate +subjugated +subjugates +subjugating +subjugation +subjugation's +subjunctive +subjunctive's +subjunctives +sublease +sublease's +subleased +subleases +subleasing +sublet +sublet's +sublets +subletting +sublimate +sublimated +sublimates +sublimating +sublimation +sublimation's +sublime +sublimed +sublimely +sublimer +sublimes +sublimest +subliminal +subliminally +subliming +sublimity +sublimity's +submarine +submarine's +submarines +submerge +submerged +submergence +submergence's +submerges +submerging +submerse +submersed +submerses +submersible +submersible's +submersibles +submersing +submersion +submersion's +submission +submission's +submissions +submissive +submit +submits +submitted +submitter +submitting +subnormal +suborbital +subordinate +subordinate's +subordinated +subordinates +subordinating +subordination +subordination's +suborn +subornation +subornation's +suborned +suborning +suborns +subplot +subplot's +subplots +subpoena +subpoena's +subpoenaed +subpoenaing +subpoenas +subprime +subprogram +subprograms +subroutine +subroutine's +subroutines +subs +subscribe +subscribed +subscriber +subscriber's +subscribers +subscribes +subscribing +subscript +subscript's +subscription +subscription's +subscriptions +subscripts +subsection +subsection's +subsections +subsequent +subsequently +subservience +subservience's +subservient +subset +subset's +subsets +subside +subsided +subsidence +subsidence's +subsides +subsidiaries +subsidiary +subsidiary's +subsidies +subsiding +subsidization +subsidization's +subsidize +subsidized +subsidizes +subsidizing +subsidy +subsidy's +subsist +subsisted +subsistence +subsistence's +subsisting +subsists +subsoil +subsoil's +subsonic +subspace +substance +substance's +substances +substandard +substantial +substantially +substantiate +substantiated +substantiates +substantiating +substantiation +substantiation's +substantiations +substantive +substantive's +substantives +substation +substation's +substations +substitute +substitute's +substituted +substitutes +substituting +substitution +substitution's +substitutions +substrata +substrate +substratum +substratum's +substratums +substructure +substructure's +substructures +subsume +subsumed +subsumes +subsuming +subsystem +subsystem's +subsystems +subteen +subteen's +subteens +subterfuge +subterfuge's +subterfuges +subterranean +subtitle +subtitle's +subtitled +subtitles +subtitling +subtle +subtler +subtlest +subtleties +subtlety +subtlety's +subtly +subtotal +subtotal's +subtotaled +subtotaling +subtotalled +subtotalling +subtotals +subtract +subtracted +subtracting +subtraction +subtraction's +subtractions +subtracts +subtrahend +subtrahend's +subtrahends +subtropical +suburb +suburb's +suburban +suburban's +suburbanite +suburbanite's +suburbanites +suburbans +suburbia +suburbia's +suburbs +subversion +subversion's +subversive +subversive's +subversives +subvert +subverted +subverting +subverts +subway +subway's +subways +succeed +succeeded +succeeding +succeeds +success +success's +successes +successful +successfully +succession +succession's +successions +successive +successively +successor +successor's +successors +succinct +succincter +succinctest +succinctly +succinctness +succinctness's +succor +succor's +succored +succoring +succors +succotash +succotash's +succulence +succulence's +succulent +succulent's +succulents +succumb +succumbed +succumbing +succumbs +such +suchlike +suck +suck's +sucked +sucker +sucker's +suckered +suckering +suckers +sucking +suckle +suckled +suckles +suckling +suckling's +sucklings +sucks +sucrose +sucrose's +suction +suction's +suctioned +suctioning +suctions +sudden +suddenly +suddenness +suddenness's +suds +suds's +sudsier +sudsiest +sudsy +sue +sued +suede +suede's +sues +suet +suet's +suffer +sufferance +sufferance's +suffered +sufferer +sufferer's +sufferers +suffering +suffering's +sufferings +suffers +suffice +sufficed +suffices +sufficiency +sufficiency's +sufficient +sufficiently +sufficing +suffix +suffix's +suffixed +suffixes +suffixing +suffocate +suffocated +suffocates +suffocating +suffocation +suffocation's +suffragan +suffragan's +suffragans +suffrage +suffrage's +suffragette +suffragette's +suffragettes +suffragist +suffragist's +suffragists +suffuse +suffused +suffuses +suffusing +suffusion +suffusion's +sugar +sugar's +sugarcane +sugarcane's +sugarcoat +sugarcoated +sugarcoating +sugarcoats +sugared +sugarier +sugariest +sugaring +sugarless +sugars +sugary +suggest +suggested +suggester +suggestible +suggesting +suggestion +suggestion's +suggestions +suggestive +suggestively +suggests +suicidal +suicide +suicide's +suicides +suing +suit +suit's +suitability +suitability's +suitable +suitably +suitcase +suitcase's +suitcases +suite +suite's +suited +suites +suiting +suiting's +suitor +suitor's +suitors +suits +sukiyaki +sukiyaki's +sulfate +sulfate's +sulfates +sulfide +sulfide's +sulfides +sulfur +sulfur's +sulfured +sulfuric +sulfuring +sulfurous +sulfurs +sulk +sulk's +sulked +sulkier +sulkies +sulkiest +sulkily +sulkiness +sulkiness's +sulking +sulks +sulky +sulky's +sullen +sullener +sullenest +sullenly +sullenness +sullenness's +sullied +sullies +sully +sullying +sulphur +sulphur's +sulphured +sulphuring +sulphurous +sulphurs +sultan +sultan's +sultana +sultana's +sultanas +sultanate +sultanate's +sultanates +sultans +sultrier +sultriest +sultry +sum +sum's +sumac +sumac's +sumach +sumach's +summaries +summarily +summarize +summarized +summarizes +summarizing +summary +summary's +summation +summation's +summations +summed +summer +summer's +summered +summerhouse +summerhouse's +summerhouses +summering +summers +summertime +summertime's +summery +summing +summit +summit's +summitry +summitry's +summits +summon +summoned +summoner +summoner's +summoners +summoning +summons +summons's +summonsed +summonses +summonsing +sumo +sumo's +sump +sump's +sumps +sumptuous +sums +sun +sun's +sunbathe +sunbathed +sunbather +sunbather's +sunbathers +sunbathes +sunbathing +sunbathing's +sunbeam +sunbeam's +sunbeams +sunblock +sunblock's +sunblocks +sunbonnet +sunbonnet's +sunbonnets +sunburn +sunburn's +sunburned +sunburning +sunburns +sunburnt +sundae +sundae's +sundaes +sunder +sundered +sundering +sunders +sundial +sundial's +sundials +sundown +sundown's +sundowns +sundries +sundries's +sundry +sunfish +sunfish's +sunfishes +sunflower +sunflower's +sunflowers +sung +sunglasses +sunglasses's +sunk +sunken +sunlamp +sunlamp's +sunlamps +sunless +sunlight +sunlight's +sunlit +sunned +sunnier +sunniest +sunning +sunny +sunrise +sunrise's +sunrises +sunroof +sunroof's +sunroofs +suns +sunscreen +sunscreen's +sunscreens +sunset +sunset's +sunsets +sunshine +sunshine's +sunspot +sunspot's +sunspots +sunstroke +sunstroke's +suntan +suntan's +suntanned +suntanning +suntans +sunup +sunup's +sup +sup's +super +super's +superabundance +superabundance's +superabundances +superabundant +superannuate +superannuated +superannuates +superannuating +superb +superber +superbest +superbly +supercharge +supercharged +supercharger +supercharger's +superchargers +supercharges +supercharging +supercilious +supercomputer +supercomputer's +supercomputers +superconductivity +superconductivity's +superconductor +superconductor's +superconductors +superego +superego's +superegos +superficial +superficiality +superficiality's +superficially +superfluity +superfluity's +superfluous +superhighway +superhighway's +superhighways +superhuman +superimpose +superimposed +superimposes +superimposing +superintend +superintended +superintendence +superintendence's +superintendency +superintendency's +superintendent +superintendent's +superintendents +superintending +superintends +superior +superior's +superiority +superiority's +superiors +superlative +superlative's +superlatively +superlatives +superman +superman's +supermarket +supermarket's +supermarkets +supermen +supermodel +supermodel's +supermodels +supernatural +supernaturals +supernova +supernova's +supernovae +supernovas +supernumeraries +supernumerary +supernumerary's +superpower +superpower's +superpowers +supers +superscript +superscript's +superscripts +supersede +superseded +supersedes +superseding +supersize +supersized +supersizes +supersizing +supersonic +superstar +superstar's +superstars +superstition +superstition's +superstitions +superstitious +superstitiously +superstructure +superstructure's +superstructures +supertanker +supertanker's +supertankers +supervene +supervened +supervenes +supervening +supervise +supervised +supervises +supervising +supervision +supervision's +supervisions +supervisor +supervisor's +supervisors +supervisory +supine +supped +supper +supper's +suppers +supping +supplant +supplanted +supplanting +supplants +supple +supplement +supplement's +supplemental +supplementary +supplemented +supplementing +supplements +suppleness +suppleness's +suppler +supplest +suppliant +suppliant's +suppliants +supplicant +supplicant's +supplicants +supplicate +supplicated +supplicates +supplicating +supplication +supplication's +supplications +supplied +supplier +supplier's +suppliers +supplies +supply +supply's +supplying +support +support's +supportable +supported +supporter +supporter's +supporters +supporting +supportive +supports +suppose +supposed +supposedly +supposes +supposing +supposition +supposition's +suppositions +suppositories +suppository +suppository's +suppress +suppressed +suppresses +suppressing +suppression +suppression's +suppurate +suppurated +suppurates +suppurating +suppuration +suppuration's +supranational +supremacist +supremacist's +supremacists +supremacy +supremacy's +supreme +supremely +sups +surcease +surcease's +surceased +surceases +surceasing +surcharge +surcharge's +surcharged +surcharges +surcharging +sure +surefire +surefooted +surely +sureness +sureness's +surer +surest +sureties +surety +surety's +surf +surf's +surface +surface's +surfaced +surfaces +surfacing +surfboard +surfboard's +surfboarded +surfboarding +surfboards +surfed +surfeit +surfeit's +surfeited +surfeiting +surfeits +surfer +surfer's +surfers +surfing +surfing's +surfs +surge +surge's +surged +surgeon +surgeon's +surgeons +surgeries +surgery +surgery's +surges +surgical +surgically +surging +surlier +surliest +surliness +surliness's +surly +surmise +surmise's +surmised +surmises +surmising +surmount +surmountable +surmounted +surmounting +surmounts +surname +surname's +surnames +surpass +surpassed +surpasses +surpassing +surplice +surplice's +surplices +surplus +surplus's +surplused +surpluses +surplusing +surplussed +surplussing +surprise +surprise's +surprised +surprises +surprising +surprisingly +surprisings +surreal +surrealism +surrealism's +surrealist +surrealist's +surrealistic +surrealists +surrender +surrender's +surrendered +surrendering +surrenders +surreptitious +surreptitiously +surrey +surrey's +surreys +surrogate +surrogate's +surrogates +surround +surrounded +surrounding +surrounding's +surroundings +surroundings's +surrounds +surtax +surtax's +surtaxed +surtaxes +surtaxing +surveillance +surveillance's +survey +survey's +surveyed +surveying +surveyor +surveyor's +surveyors +surveys +survival +survival's +survivals +survive +survived +survives +surviving +survivor +survivor's +survivors +susceptibility +susceptibility's +susceptible +sushi +sushi's +suspect +suspect's +suspected +suspecting +suspects +suspend +suspended +suspender +suspender's +suspenders +suspending +suspends +suspense +suspense's +suspenseful +suspension +suspension's +suspensions +suspicion +suspicion's +suspicions +suspicious +suspiciously +sustain +sustainable +sustained +sustaining +sustains +sustenance +sustenance's +suture +suture's +sutured +sutures +suturing +svelte +svelter +sveltest +swab +swab's +swabbed +swabbing +swabs +swaddle +swaddled +swaddles +swaddling +swag +swag's +swagged +swagger +swagger's +swaggered +swaggerer +swaggering +swaggers +swagging +swags +swain +swain's +swains +swallow +swallow's +swallowed +swallowing +swallows +swallowtail +swallowtail's +swallowtails +swam +swami +swami's +swamis +swamp +swamp's +swamped +swampier +swampiest +swamping +swamps +swampy +swan +swan's +swank +swank's +swanked +swanker +swankest +swankier +swankiest +swanking +swanks +swanky +swans +swap +swap's +swapped +swapping +swaps +sward +sward's +swards +swarm +swarm's +swarmed +swarming +swarms +swarthier +swarthiest +swarthy +swash +swash's +swashbuckler +swashbuckler's +swashbucklers +swashbuckling +swashbuckling's +swashed +swashes +swashing +swastika +swastika's +swastikas +swat +swat's +swatch +swatch's +swatches +swath +swath's +swathe +swathe's +swathed +swathes +swathing +swaths +swats +swatted +swatter +swatter's +swattered +swattering +swatters +swatting +sway +sway's +swaybacked +swayed +swaying +sways +swear +swearer +swearer's +swearers +swearing +swears +swearword +swearword's +swearwords +sweat +sweat's +sweater +sweater's +sweaters +sweatier +sweatiest +sweating +sweatpants +sweatpants's +sweats +sweats's +sweatshirt +sweatshirt's +sweatshirts +sweatshop +sweatshop's +sweatshops +sweaty +sweep +sweep's +sweeper +sweeper's +sweepers +sweeping +sweeping's +sweepings +sweepings's +sweeps +sweepstake +sweepstake's +sweepstakes +sweepstakes's +sweet +sweet's +sweetbread +sweetbread's +sweetbreads +sweetbriar +sweetbriar's +sweetbriars +sweetbrier +sweetbrier's +sweetbriers +sweeten +sweetened +sweetener +sweetener's +sweeteners +sweetening +sweetening's +sweetens +sweeter +sweetest +sweetheart +sweetheart's +sweethearts +sweetie +sweetie's +sweeties +sweetish +sweetly +sweetmeat +sweetmeat's +sweetmeats +sweetness +sweetness's +sweets +swell +swell's +swelled +sweller +swellest +swellhead +swellhead's +swellheaded +swellheads +swelling +swelling's +swellings +swells +swelter +swelter's +sweltered +sweltering +swelters +swept +swerve +swerve's +swerved +swerves +swerving +swift +swift's +swifter +swiftest +swiftly +swiftness +swiftness's +swifts +swig +swig's +swigged +swigging +swigs +swill +swill's +swilled +swilling +swills +swim +swim's +swimmer +swimmer's +swimmers +swimming +swimming's +swims +swimsuit +swimsuit's +swimsuits +swindle +swindle's +swindled +swindler +swindler's +swindlers +swindles +swindling +swine +swine's +swines +swing +swing's +swinger +swinger's +swingers +swinging +swings +swinish +swipe +swipe's +swiped +swipes +swiping +swirl +swirl's +swirled +swirling +swirls +swirly +swish +swish's +swished +swisher +swishes +swishest +swishing +switch +switch's +switchable +switchback +switchback's +switchbacks +switchblade +switchblade's +switchblades +switchboard +switchboard's +switchboards +switched +switcher +switches +switching +swivel +swivel's +swiveled +swiveling +swivelled +swivelling +swivels +swollen +swoon +swoon's +swooned +swooning +swoons +swoop +swoop's +swooped +swooping +swoops +swop +swop's +swopped +swopping +swops +sword +sword's +swordfish +swordfish's +swordfishes +swordplay +swordplay's +swords +swordsman +swordsman's +swordsmen +swore +sworn +swum +swung +sybarite +sybarite's +sybarites +sybaritic +sycamore +sycamore's +sycamores +sycophant +sycophant's +sycophantic +sycophants +syllabi +syllabic +syllabication +syllabication's +syllabification +syllabification's +syllabified +syllabifies +syllabify +syllabifying +syllable +syllable's +syllables +syllabus +syllabus's +syllabuses +syllogism +syllogism's +syllogisms +syllogistic +sylph +sylph's +sylphs +sylvan +symbioses +symbiosis +symbiosis's +symbiotic +symbol +symbol's +symbolic +symbolically +symbolism +symbolism's +symbolization +symbolization's +symbolize +symbolized +symbolizes +symbolizing +symbols +symmetric +symmetrical +symmetrically +symmetricly +symmetries +symmetry +symmetry's +sympathetic +sympathetically +sympathies +sympathies's +sympathize +sympathized +sympathizer +sympathizer's +sympathizers +sympathizes +sympathizing +sympathy +sympathy's +symphonic +symphonies +symphony +symphony's +symposia +symposium +symposium's +symposiums +symptom +symptom's +symptomatic +symptoms +synagog +synagog's +synagogs +synagogue +synagogue's +synagogues +synapse +synapse's +synapses +sync +sync's +synced +synch +synch's +synched +synches +synching +synchronization +synchronization's +synchronizations +synchronize +synchronized +synchronizes +synchronizing +synchronous +synchronously +synchs +syncing +syncopate +syncopated +syncopates +syncopating +syncopation +syncopation's +syncs +syndicate +syndicate's +syndicated +syndicates +syndicating +syndication +syndication's +syndrome +syndrome's +syndromes +synergism +synergism's +synergistic +synergy +synergy's +synod +synod's +synods +synonym +synonym's +synonymous +synonyms +synopses +synopsis +synopsis's +syntactic +syntactical +syntactically +syntax +syntax's +syntheses +synthesis +synthesis's +synthesize +synthesized +synthesizer +synthesizer's +synthesizers +synthesizes +synthesizing +synthetic +synthetic's +synthetically +synthetics +syphilis +syphilis's +syphilitic +syphilitic's +syphilitics +syphon +syphon's +syphoned +syphoning +syphons +syringe +syringe's +syringed +syringes +syringing +syrup +syrup's +syrups +syrupy +system +system's +systematic +systematically +systematize +systematized +systematizes +systematizing +systemic +systemic's +systemics +systems +systolic +séance +séance's +séances +t +tab +tab's +tabbed +tabbies +tabbing +tabby +tabby's +tabernacle +tabernacle's +tabernacles +table +table's +tableau +tableau's +tableaus +tableaux +tablecloth +tablecloth's +tablecloths +tabled +tableland +tableland's +tablelands +tables +tablespoon +tablespoon's +tablespoonful +tablespoonful's +tablespoonfuls +tablespoons +tablespoonsful +tablet +tablet's +tablets +tableware +tableware's +tabling +tabloid +tabloid's +tabloids +taboo +taboo's +tabooed +tabooing +taboos +tabs +tabu +tabu's +tabued +tabuing +tabular +tabulate +tabulated +tabulates +tabulating +tabulation +tabulation's +tabulator +tabulator's +tabulators +tabus +tachometer +tachometer's +tachometers +tacit +tacitly +tacitness +tacitness's +taciturn +taciturnity +taciturnity's +tack +tack's +tacked +tackier +tackiest +tackiness +tackiness's +tacking +tackle +tackle's +tackled +tackler +tackler's +tacklers +tackles +tackling +tacks +tacky +taco +taco's +tacos +tact +tact's +tactful +tactfully +tactic +tactic's +tactical +tactically +tactician +tactician's +tacticians +tactics +tactile +tactless +tactlessly +tactlessness +tactlessness's +tad +tad's +tadpole +tadpole's +tadpoles +tads +taffeta +taffeta's +taffies +taffy +taffy's +tag +tag's +tagged +tagging +tags +tail +tail's +tailcoat +tailcoat's +tailcoats +tailed +tailgate +tailgate's +tailgated +tailgates +tailgating +tailing +tailless +taillight +taillight's +taillights +tailor +tailor's +tailored +tailoring +tailoring's +tailors +tailpipe +tailpipe's +tailpipes +tails +tailspin +tailspin's +tailspins +tailwind +tailwind's +tailwinds +taint +taint's +tainted +tainting +taints +take +take's +takeaways +taken +takeoff +takeoff's +takeoffs +takeout +takeout's +takeouts +takeover +takeover's +takeovers +taker +taker's +takers +takes +taking +taking's +takings +takings's +talc +talc's +tale +tale's +talent +talent's +talented +talents +tales +talisman +talisman's +talismans +talk +talk's +talkative +talkativeness +talkativeness's +talked +talker +talker's +talkers +talking +talks +tall +taller +tallest +tallied +tallies +tallness +tallness's +tallow +tallow's +tally +tally's +tallyho +tallyho's +tallyhoed +tallyhoing +tallyhos +tallying +talon +talon's +talons +tam +tam's +tamable +tamale +tamale's +tamales +tamarind +tamarind's +tamarinds +tambourine +tambourine's +tambourines +tame +tameable +tamed +tamely +tameness +tameness's +tamer +tamer's +tamers +tames +tamest +taming +tamp +tamped +tamper +tampered +tampering +tampers +tamping +tampon +tampon's +tampons +tamps +tams +tan +tan's +tanager +tanager's +tanagers +tandem +tandem's +tandems +tang +tang's +tangelo +tangelo's +tangelos +tangent +tangent's +tangential +tangents +tangerine +tangerine's +tangerines +tangibility +tangibility's +tangible +tangible's +tangibles +tangibly +tangier +tangiest +tangle +tangle's +tangled +tangles +tangling +tango +tango's +tangoed +tangoing +tangos +tangs +tangy +tank +tank's +tankard +tankard's +tankards +tanked +tanker +tanker's +tankers +tankful +tankful's +tankfuls +tanking +tanks +tanned +tanner +tanner's +tanneries +tanners +tannery +tannery's +tannest +tannin +tannin's +tanning +tans +tansy +tansy's +tantalize +tantalized +tantalizes +tantalizing +tantalizingly +tantamount +tantrum +tantrum's +tantrums +tap +tap's +tape +tape's +taped +taper +taper's +tapered +tapering +tapers +tapes +tapestries +tapestry +tapestry's +tapeworm +tapeworm's +tapeworms +taping +tapioca +tapioca's +tapir +tapir's +tapirs +tapped +tapping +taproom +taproom's +taprooms +taproot +taproot's +taproots +taps +tar +tar's +tarantula +tarantula's +tarantulae +tarantulas +tardier +tardiest +tardily +tardiness +tardiness's +tardy +tare +tare's +tared +tares +target +target's +targeted +targeting +targets +tariff +tariff's +tariffs +taring +tarmac +tarmac's +tarmacked +tarmacking +tarmacs +tarnish +tarnish's +tarnished +tarnishes +tarnishing +taro +taro's +taros +tarot +tarot's +tarots +tarp +tarp's +tarpaulin +tarpaulin's +tarpaulins +tarpon +tarpon's +tarpons +tarps +tarragon +tarragon's +tarragons +tarred +tarried +tarrier +tarries +tarriest +tarring +tarry +tarrying +tars +tart +tart's +tartan +tartan's +tartans +tartar +tartar's +tartars +tarter +tartest +tartly +tartness +tartness's +tarts +taser +taser's +tasered +tasering +tasers +task +task's +tasked +tasking +taskmaster +taskmaster's +taskmasters +tasks +tassel +tassel's +tasseled +tasseling +tasselled +tasselling +tassels +taste +taste's +tasted +tasteful +tastefully +tasteless +tastelessly +tastelessness +tastelessness's +taster +taster's +tasters +tastes +tastier +tastiest +tastiness +tastiness's +tasting +tasty +tat +tats +tatted +tatter +tatter's +tattered +tattering +tatters +tatting +tatting's +tattle +tattle's +tattled +tattler +tattler's +tattlers +tattles +tattletale +tattletale's +tattletales +tattling +tattoo +tattoo's +tattooed +tattooing +tattooist +tattooist's +tattooists +tattoos +tatty +taught +taunt +taunt's +taunted +taunting +taunts +taupe +taupe's +taut +tauter +tautest +tautly +tautness +tautness's +tautological +tautologies +tautology +tautology's +tavern +tavern's +taverns +tawdrier +tawdriest +tawdriness +tawdriness's +tawdry +tawnier +tawniest +tawny +tawny's +tax +tax's +taxable +taxation +taxation's +taxed +taxes +taxi +taxi's +taxicab +taxicab's +taxicabs +taxidermist +taxidermist's +taxidermists +taxidermy +taxidermy's +taxied +taxies +taxiing +taxing +taxis +taxonomic +taxonomies +taxonomy +taxonomy's +taxpayer +taxpayer's +taxpayers +taxying +tea +tea's +teabag +teach +teachable +teacher +teacher's +teachers +teaches +teaching +teaching's +teachings +teacup +teacup's +teacups +teak +teak's +teakettle +teakettle's +teakettles +teaks +teal +teal's +tealight +tealight's +tealights +teals +team +team's +teamed +teaming +teammate +teammate's +teammates +teams +teamster +teamster's +teamsters +teamwork +teamwork's +teapot +teapot's +teapots +tear +tear's +teardrop +teardrop's +teardrops +teared +tearful +tearfully +teargas +teargas's +teargases +teargassed +teargasses +teargassing +tearier +teariest +tearing +tearjerker +tearjerker's +tearjerkers +tearoom +tearoom's +tearooms +tears +teary +teas +tease +tease's +teased +teasel +teasel's +teasels +teaser +teaser's +teasers +teases +teasing +teaspoon +teaspoon's +teaspoonful +teaspoonful's +teaspoonfuls +teaspoons +teaspoonsful +teat +teat's +teatime +teats +teazel +teazel's +teazels +teazle +teazle's +teazles +technical +technicalities +technicality +technicality's +technically +technician +technician's +technicians +technique +technique's +techniques +techno +technocracy +technocracy's +technocrat +technocrat's +technocrats +technological +technologically +technologies +technologist +technologist's +technologists +technology +technology's +techs +tectonics +tectonics's +tedious +tediously +tediousness +tediousness's +tedium +tedium's +tee +tee's +teed +teeing +teem +teemed +teeming +teems +teen +teen's +teenage +teenaged +teenager +teenager's +teenagers +teenier +teeniest +teens +teensier +teensiest +teensy +teeny +teepee +teepee's +teepees +tees +teeter +teeter's +teetered +teetering +teeters +teeth +teethe +teethed +teethes +teething +teetotal +teetotaler +teetotaler's +teetotalers +teetotaller +teetotaller's +teetotallers +telecast +telecast's +telecasted +telecaster +telecaster's +telecasters +telecasting +telecasts +telecommunication +telecommunication's +telecommunications +telecommunications's +telecommute +telecommuted +telecommuter +telecommuter's +telecommuters +telecommutes +telecommuting +telecommuting's +teleconference +teleconference's +teleconferenced +teleconferences +teleconferencing +telegram +telegram's +telegrams +telegraph +telegraph's +telegraphed +telegrapher +telegrapher's +telegraphers +telegraphic +telegraphing +telegraphs +telegraphy +telegraphy's +telekinesis +telekinesis's +telemarketing +telemarketing's +telemeter +telemeter's +telemeters +telemetries +telemetry +telemetry's +telepathic +telepathically +telepathy +telepathy's +telephone +telephone's +telephoned +telephones +telephonic +telephoning +telephony +telephony's +telephoto +telephoto's +telephotos +telescope +telescope's +telescoped +telescopes +telescopic +telescoping +telethon +telethon's +telethons +teletype +teletypes +teletypewriter +teletypewriter's +teletypewriters +televangelist +televangelist's +televangelists +televise +televised +televises +televising +television +television's +televisions +telex +telex's +telexed +telexes +telexing +tell +teller +teller's +tellers +telling +tellingly +tells +telltale +telltale's +telltales +temblor +temblor's +temblors +temerity +temerity's +temp +temp's +temped +temper +temper's +tempera +tempera's +temperament +temperament's +temperamental +temperamentally +temperaments +temperance +temperance's +temperas +temperate +temperature +temperature's +temperatures +tempered +tempering +tempers +tempest +tempest's +tempests +tempestuous +tempestuously +tempestuousness +tempestuousness's +tempi +temping +template +template's +templates +temple +temple's +temples +tempo +tempo's +temporal +temporally +temporaries +temporarily +temporary +temporary's +temporize +temporized +temporizes +temporizing +tempos +temps +tempt +temptation +temptation's +temptations +tempted +tempter +tempter's +tempters +tempting +temptingly +temptress +temptress's +temptresses +tempts +tempura +tempura's +ten +ten's +tenability +tenability's +tenable +tenacious +tenaciously +tenacity +tenacity's +tenancies +tenancy +tenancy's +tenant +tenant's +tenanted +tenanting +tenants +tend +tended +tendencies +tendency +tendency's +tendentious +tendentiously +tendentiousness +tendentiousness's +tender +tender's +tendered +tenderer +tenderest +tenderfeet +tenderfoot +tenderfoot's +tenderfoots +tenderhearted +tendering +tenderize +tenderized +tenderizer +tenderizer's +tenderizers +tenderizes +tenderizing +tenderloin +tenderloin's +tenderloins +tenderly +tenderness +tenderness's +tenders +tending +tendinitis +tendinitis's +tendon +tendon's +tendonitis +tendonitis's +tendons +tendril +tendril's +tendrils +tends +tenement +tenement's +tenements +tenet +tenet's +tenets +tenfold +tennis +tennis's +tenon +tenon's +tenoned +tenoning +tenons +tenor +tenor's +tenors +tenpin +tenpin's +tenpins +tenpins's +tens +tense +tense's +tensed +tensely +tenseness +tenseness's +tenser +tenses +tensest +tensile +tensing +tension +tension's +tensions +tensor +tensors +tent +tent's +tentacle +tentacle's +tentacles +tentative +tentatively +tented +tenth +tenth's +tenths +tenting +tents +tenuous +tenuously +tenuousness +tenuousness's +tenure +tenure's +tenured +tenures +tenuring +tepee +tepee's +tepees +tepid +tequila +tequila's +tequilas +terabit +terabit's +terabits +terabyte +terabyte's +terabytes +tercentenaries +tercentenary +tercentenary's +term +term's +termagant +termagant's +termagants +termed +terminable +terminal +terminal's +terminally +terminals +terminate +terminated +terminates +terminating +termination +termination's +terminations +terminator +terminators +terming +termini +terminological +terminologies +terminology +terminology's +terminus +terminus's +terminuses +termite +termite's +termites +termly +terms +tern +tern's +terns +terrace +terrace's +terraced +terraces +terracing +terrain +terrain's +terrains +terrapin +terrapin's +terrapins +terraria +terrarium +terrarium's +terrariums +terrestrial +terrestrial's +terrestrials +terrible +terribly +terrier +terrier's +terriers +terrific +terrifically +terrified +terrifies +terrify +terrifying +terrifyingly +territorial +territorial's +territorials +territories +territory +territory's +terror +terror's +terrorism +terrorism's +terrorist +terrorist's +terrorists +terrorize +terrorized +terrorizes +terrorizing +terrors +terry +terry's +terse +tersely +terseness +terseness's +terser +tersest +tertiary +test +test's +testable +testament +testament's +testamentary +testaments +testate +testates +tested +tester +tester's +testers +testes +testicle +testicle's +testicles +testier +testiest +testified +testifies +testify +testifying +testily +testimonial +testimonial's +testimonials +testimonies +testimony +testimony's +testiness +testiness's +testing +testis +testis's +testosterone +testosterone's +tests +testy +tetanus +tetanus's +tether +tether's +tethered +tethering +tethers +tetrahedra +tetrahedron +tetrahedron's +tetrahedrons +text +text's +textbook +textbook's +textbooks +texted +textile +textile's +textiles +texting +texts +textual +textually +textural +texture +texture's +textured +textures +texturing +thalami +thalamus +thalamus's +thallium +thallium's +than +thank +thanked +thankful +thankfully +thankfulness +thankfulness's +thanking +thankless +thanklessly +thanks +thanksgiving +thanksgiving's +thanksgivings +that +that's +thatch +thatch's +thatched +thatcher +thatches +thatching +thatching's +thaw +thaw's +thawed +thawing +thaws +the +theater +theater's +theaters +theatre +theatre's +theatres +theatrical +theatrically +thee +thees +theft +theft's +thefts +their +theirs +theism +theism's +theist +theist's +theistic +theists +them +thematic +thematically +theme +theme's +themes +themselves +then +then's +thence +thenceforth +thenceforward +theocracies +theocracy +theocracy's +theocratic +theologian +theologian's +theologians +theological +theologies +theology +theology's +theorem +theorem's +theorems +theoretic +theoretical +theoretically +theoretician +theoretician's +theoreticians +theories +theorist +theorist's +theorists +theorize +theorized +theorizes +theorizing +theory +theory's +theosophy +theosophy's +therapeutic +therapeutically +therapeutics +therapeutics's +therapies +therapist +therapist's +therapists +therapy +therapy's +there +there's +thereabout +thereabouts +thereafter +thereby +therefore +therefrom +therein +thereof +thereon +thereto +thereupon +therewith +thermal +thermal's +thermally +thermals +thermionic +thermodynamic +thermodynamics +thermodynamics's +thermometer +thermometer's +thermometers +thermonuclear +thermoplastic +thermoplastic's +thermoplastics +thermos +thermos's +thermoses +thermostat +thermostat's +thermostatic +thermostats +thesauri +thesaurus +thesaurus's +thesauruses +these +theses +thesis +thesis's +thespian +thespian's +thespians +theta +they +they'd +they'll +they're +they've +thiamin +thiamin's +thiamine +thiamine's +thick +thick's +thicken +thickened +thickener +thickener's +thickeners +thickening +thickening's +thickenings +thickens +thicker +thickest +thicket +thicket's +thickets +thickly +thickness +thickness's +thicknesses +thickset +thief +thief's +thieve +thieved +thievery +thievery's +thieves +thieving +thievish +thigh +thigh's +thighbone +thighbone's +thighbones +thighs +thimble +thimble's +thimbleful +thimbleful's +thimblefuls +thimbles +thin +thine +thing +thing's +thingamajig +thingamajig's +thingamajigs +things +think +thinker +thinker's +thinkers +thinking +thinking's +thinks +thinly +thinned +thinner +thinner's +thinners +thinness +thinness's +thinnest +thinning +thins +third +third's +thirdly +thirds +thirst +thirst's +thirsted +thirstier +thirstiest +thirstily +thirsting +thirsts +thirsty +thirteen +thirteen's +thirteens +thirteenth +thirteenth's +thirteenths +thirties +thirtieth +thirtieth's +thirtieths +thirty +thirty's +this +thistle +thistle's +thistledown +thistledown's +thistles +thither +tho +thong +thong's +thongs +thoraces +thoracic +thorax +thorax's +thoraxes +thorium +thorium's +thorn +thorn's +thornier +thorniest +thorns +thorny +thorough +thoroughbred +thoroughbred's +thoroughbreds +thorougher +thoroughest +thoroughfare +thoroughfare's +thoroughfares +thoroughgoing +thoroughly +thoroughness +thoroughness's +those +thou +thou's +though +thought +thought's +thoughtful +thoughtfully +thoughtfulness +thoughtfulness's +thoughtless +thoughtlessly +thoughtlessness +thoughtlessness's +thoughts +thous +thousand +thousand's +thousands +thousandth +thousandth's +thousandths +thraldom +thraldom's +thrall +thrall's +thralldom +thralldom's +thralled +thralling +thralls +thrash +thrash's +thrashed +thrasher +thrasher's +thrashers +thrashes +thrashing +thrashing's +thrashings +thread +thread's +threadbare +threaded +threading +threads +threat +threat's +threaten +threatened +threatening +threateningly +threatens +threats +three +three's +threefold +threes +threescore +threescore's +threescores +threesome +threesome's +threesomes +threnodies +threnody +threnody's +thresh +thresh's +threshed +thresher +thresher's +threshers +threshes +threshing +threshold +threshold's +thresholds +threw +thrice +thrift +thrift's +thriftier +thriftiest +thriftily +thriftiness +thriftiness's +thrifts +thrifty +thrill +thrill's +thrilled +thriller +thriller's +thrillers +thrilling +thrills +thrive +thrived +thriven +thrives +thriving +throat +throat's +throatier +throatiest +throatily +throatiness +throatiness's +throats +throaty +throb +throb's +throbbed +throbbing +throbs +throe +throe's +throes +thromboses +thrombosis +thrombosis's +throne +throne's +thrones +throng +throng's +thronged +thronging +throngs +throttle +throttle's +throttled +throttles +throttling +through +throughout +throughput +throughway +throughway's +throughways +throve +throw +throw's +throwaway +throwaway's +throwaways +throwback +throwback's +throwbacks +thrower +thrower's +throwers +throwing +thrown +throws +thru +thrum +thrum's +thrummed +thrumming +thrums +thrush +thrush's +thrushes +thrust +thrust's +thrusting +thrusts +thruway +thruway's +thruways +thud +thud's +thudded +thudding +thuds +thug +thug's +thugs +thumb +thumb's +thumbed +thumbing +thumbnail +thumbnail's +thumbnails +thumbs +thumbscrew +thumbscrew's +thumbscrews +thumbtack +thumbtack's +thumbtacks +thump +thump's +thumped +thumping +thumps +thunder +thunder's +thunderbolt +thunderbolt's +thunderbolts +thunderclap +thunderclap's +thunderclaps +thundercloud +thundercloud's +thunderclouds +thundered +thunderhead +thunderhead's +thunderheads +thundering +thunderous +thunderously +thunders +thundershower +thundershower's +thundershowers +thunderstorm +thunderstorm's +thunderstorms +thunderstruck +thus +thwack +thwack's +thwacked +thwacking +thwacks +thwart +thwart's +thwarted +thwarting +thwarts +thy +thyme +thyme's +thymi +thymus +thymus's +thymuses +thyroid +thyroid's +thyroids +thyself +ti +ti's +tiara +tiara's +tiaras +tibia +tibia's +tibiae +tibias +tic +tic's +tick +tick's +ticked +ticker +ticker's +tickers +ticket +ticket's +ticketed +ticketing +tickets +ticking +ticking's +tickle +tickle's +tickled +tickles +tickling +ticklish +ticks +tics +tidal +tidbit +tidbit's +tidbits +tiddlywinks +tiddlywinks's +tide +tide's +tided +tides +tidewater +tidewater's +tidewaters +tidied +tidier +tidies +tidiest +tidily +tidiness +tidiness's +tiding +tidings +tidings's +tidy +tidy's +tidying +tie +tie's +tiebreaker +tiebreaker's +tiebreakers +tied +tieing +tier +tier's +tiers +ties +tiff +tiff's +tiffed +tiffing +tiffs +tiger +tiger's +tigers +tight +tighten +tightened +tightening +tightens +tighter +tightest +tightfisted +tightly +tightness +tightness's +tightrope +tightrope's +tightropes +tights +tights's +tightwad +tightwad's +tightwads +tigress +tigress's +tigresses +tike +tike's +tikes +tilde +tilde's +tildes +tile +tile's +tiled +tiles +tiling +tiling's +till +till's +tillable +tillage +tillage's +tilled +tiller +tiller's +tillers +tilling +tills +tilt +tilt's +tilted +tilting +tilts +timber +timber's +timbered +timbering +timberland +timberland's +timberline +timberline's +timberlines +timbers +timbre +timbre's +timbres +time +time's +timed +timekeeper +timekeeper's +timekeepers +timeless +timelessness +timelessness's +timelier +timeliest +timeline +timeline's +timelines +timeliness +timeliness's +timely +timepiece +timepiece's +timepieces +timer +timer's +timers +times +timescale +timescales +timestamp +timestamp's +timestamps +timetable +timetable's +timetabled +timetables +timetabling +timeworn +timezone +timid +timider +timidest +timidity +timidity's +timidly +timing +timing's +timings +timorous +timorously +timpani +timpani's +timpanist +timpanist's +timpanists +tin +tin's +tincture +tincture's +tinctured +tinctures +tincturing +tinder +tinder's +tinderbox +tinderbox's +tinderboxes +tine +tine's +tines +tinfoil +tinfoil's +ting +tinge +tinge's +tinged +tingeing +tinges +tinging +tingle +tingle's +tingled +tingles +tingling +tingling's +tinglings +tingly +tings +tinier +tiniest +tinker +tinker's +tinkered +tinkering +tinkers +tinkle +tinkle's +tinkled +tinkles +tinkling +tinned +tinnier +tinniest +tinning +tinny +tins +tinsel +tinsel's +tinseled +tinseling +tinselled +tinselling +tinsels +tinsmith +tinsmith's +tinsmiths +tint +tint's +tinted +tinting +tintinnabulation +tintinnabulation's +tintinnabulations +tints +tiny +tip +tip's +tipi +tipi's +tipis +tipped +tipper +tipper's +tippers +tipping +tipple +tipple's +tippled +tippler +tippler's +tipplers +tipples +tippling +tips +tipsier +tipsiest +tipsily +tipster +tipster's +tipsters +tipsy +tiptoe +tiptoe's +tiptoed +tiptoeing +tiptoes +tiptop +tiptop's +tiptops +tirade +tirade's +tirades +tire +tire's +tired +tireder +tiredest +tiredness +tiredness's +tireless +tirelessly +tirelessness +tirelessness's +tires +tiresome +tiresomely +tiresomeness +tiresomeness's +tiring +tiro +tiro's +tiros +tissue +tissue's +tissues +tit +tit's +titan +titan's +titanic +titanium +titanium's +titans +titbit +titbit's +titbits +tithe +tithe's +tithed +tithes +tithing +titillate +titillated +titillates +titillating +titillation +titillation's +title +title's +titled +titles +titling +titmice +titmouse +titmouse's +tits +titter +titter's +tittered +tittering +titters +tittle +tittle's +tittles +titular +tizzies +tizzy +tizzy's +to +toad +toad's +toadied +toadies +toads +toadstool +toadstool's +toadstools +toady +toady's +toadying +toast +toast's +toasted +toaster +toaster's +toasters +toastier +toastiest +toasting +toastmaster +toastmaster's +toastmasters +toasts +toasty +tobacco +tobacco's +tobaccoes +tobacconist +tobacconist's +tobacconists +tobaccos +toboggan +toboggan's +tobogganed +tobogganing +toboggans +tocsin +tocsin's +tocsins +today +today's +toddies +toddle +toddle's +toddled +toddler +toddler's +toddlers +toddles +toddling +toddy +toddy's +toe +toe's +toed +toehold +toehold's +toeholds +toeing +toenail +toenail's +toenails +toes +toffee +toffee's +toffees +toffies +toffy +toffy's +tofu +tofu's +tog +tog's +toga +toga's +togae +togas +together +togetherness +togetherness's +toggle +toggle's +toggled +toggles +toggling +togs +togs's +toil +toil's +toiled +toiler +toiler's +toilers +toilet +toilet's +toileted +toileting +toiletries +toiletry +toiletry's +toilets +toilette +toilette's +toiling +toils +toilsome +toke +toke's +toked +token +token's +tokenism +tokenism's +tokens +tokes +toking +told +tolerable +tolerably +tolerance +tolerance's +tolerances +tolerant +tolerantly +tolerate +tolerated +tolerates +tolerating +toleration +toleration's +toll +toll's +tollbooth +tollbooth's +tollbooths +tolled +tollgate +tollgate's +tollgates +tolling +tolls +tom +tom's +tomahawk +tomahawk's +tomahawked +tomahawking +tomahawks +tomato +tomato's +tomatoes +tomb +tomb's +tombed +tombing +tomboy +tomboy's +tomboys +tombs +tombstone +tombstone's +tombstones +tomcat +tomcat's +tomcats +tome +tome's +tomes +tomfooleries +tomfoolery +tomfoolery's +tomorrow +tomorrow's +tomorrows +toms +ton +ton's +tonal +tonalities +tonality +tonality's +tone +tone's +toned +toneless +toner +tones +tong +tong's +tongs +tongue +tongue's +tongued +tongues +tonguing +tonic +tonic's +tonics +tonier +toniest +tonight +tonight's +toning +tonnage +tonnage's +tonnages +tonne +tonne's +tonnes +tons +tonsil +tonsil's +tonsillectomies +tonsillectomy +tonsillectomy's +tonsillitis +tonsillitis's +tonsils +tonsorial +tonsure +tonsure's +tonsured +tonsures +tonsuring +tony +too +took +tool +tool's +toolbar +toolbar's +toolbars +toolbox +toolbox's +toolboxes +tooled +tooling +toolkit +tools +toot +toot's +tooted +tooth +tooth's +toothache +toothache's +toothaches +toothbrush +toothbrush's +toothbrushes +toothed +toothier +toothiest +toothless +toothpaste +toothpaste's +toothpastes +toothpick +toothpick's +toothpicks +toothsome +toothy +tooting +toots +top +top's +topaz +topaz's +topazes +topcoat +topcoat's +topcoats +topic +topic's +topical +topically +topics +topknot +topknot's +topknots +topless +topmast +topmast's +topmasts +topmost +topographer +topographer's +topographers +topographic +topographical +topographies +topography +topography's +topological +topologically +topology +topped +topping +topping's +toppings +topple +toppled +topples +toppling +tops +topsail +topsail's +topsails +topside +topside's +topsides +topsoil +topsoil's +toque +toque's +toques +tor +tor's +torch +torch's +torched +torches +torching +torchlight +torchlight's +tore +toreador +toreador's +toreadors +torment +torment's +tormented +tormenter +tormenter's +tormenters +tormenting +tormentor +tormentor's +tormentors +torments +torn +tornado +tornado's +tornadoes +tornados +torpedo +torpedo's +torpedoed +torpedoes +torpedoing +torpedos +torpid +torpidity +torpidity's +torpor +torpor's +torque +torque's +torqued +torques +torquing +torrent +torrent's +torrential +torrents +torrid +tors +torsi +torsion +torsion's +torso +torso's +torsos +tort +tort's +torte +torte's +tortes +tortilla +tortilla's +tortillas +tortoise +tortoise's +tortoises +tortoiseshell +tortoiseshell's +tortoiseshells +torts +tortuous +tortuously +torture +torture's +tortured +torturer +torturer's +torturers +tortures +torturing +torus +toss +toss's +tossed +tosses +tossing +tossup +tossup's +tossups +tost +tot +tot's +total +total's +totaled +totaling +totalitarian +totalitarian's +totalitarianism +totalitarianism's +totalitarians +totalities +totality +totality's +totalled +totalling +totally +totals +tote +tote's +toted +totem +totem's +totemic +totems +totes +toting +tots +totted +totter +totter's +tottered +tottering +totters +totting +toucan +toucan's +toucans +touch +touch's +touchdown +touchdown's +touchdowns +touched +touches +touchier +touchiest +touching +touchingly +touchings +touchstone +touchstone's +touchstones +touchy +touché +tough +tough's +toughen +toughened +toughening +toughens +tougher +toughest +toughly +toughness +toughness's +toughs +toupee +toupee's +toupees +tour +tour's +toured +touring +tourism +tourism's +tourist +tourist's +tourists +tourmaline +tourmaline's +tournament +tournament's +tournaments +tourney +tourney's +tourneys +tourniquet +tourniquet's +tourniquets +tours +tousle +tousled +tousles +tousling +tout +tout's +touted +touting +touts +tow +tow's +toward +towards +towed +towel +towel's +toweled +toweling +toweling's +towelings +towelled +towelling +towelling's +towellings +towels +tower +tower's +towered +towering +towers +towhead +towhead's +towheaded +towheads +towing +town +town's +townhouse +townhouse's +townhouses +towns +townsfolk +townsfolk's +township +township's +townships +townsman +townsman's +townsmen +townspeople +townspeople's +towpath +towpath's +towpaths +tows +toxemia +toxemia's +toxic +toxicity +toxicity's +toxicologist +toxicologist's +toxicologists +toxicology +toxicology's +toxin +toxin's +toxins +toy +toy's +toyed +toying +toys +trace +trace's +traceable +traced +tracer +tracer's +traceries +tracers +tracery +tracery's +traces +trachea +trachea's +tracheae +tracheas +tracheotomies +tracheotomy +tracheotomy's +tracing +tracing's +tracings +track +track's +tracked +tracker +tracker's +trackers +tracking +tracks +tract +tract's +tractable +traction +traction's +tractor +tractor's +tractors +tracts +trade +trade's +traded +trademark +trademark's +trademarked +trademarking +trademarks +trader +trader's +traders +trades +tradesman +tradesman's +tradesmen +trading +tradition +tradition's +traditional +traditionalist +traditionalist's +traditionalists +traditionally +traditions +traduce +traduced +traduces +traducing +traffic +traffic's +trafficked +trafficker +trafficker's +traffickers +trafficking +traffics +tragedian +tragedian's +tragedians +tragedies +tragedy +tragedy's +tragic +tragically +tragicomedies +tragicomedy +tragicomedy's +trail +trail's +trailblazer +trailblazer's +trailblazers +trailed +trailer +trailer's +trailers +trailing +trails +train +train's +trained +trainee +trainee's +trainees +trainer +trainer's +trainers +training +training's +trains +traipse +traipse's +traipsed +traipses +traipsing +trait +trait's +traitor +traitor's +traitorous +traitors +traits +trajectories +trajectory +trajectory's +tram +tram's +trammed +trammel +trammel's +trammeled +trammeling +trammelled +trammelling +trammels +tramming +tramp +tramp's +tramped +tramping +trample +trample's +trampled +tramples +trampling +trampoline +trampoline's +trampolines +tramps +trams +trance +trance's +trances +tranquil +tranquiler +tranquilest +tranquility +tranquility's +tranquilize +tranquilized +tranquilizer +tranquilizer's +tranquilizers +tranquilizes +tranquilizing +tranquiller +tranquillest +tranquillity +tranquillity's +tranquillize +tranquillized +tranquillizer +tranquillizer's +tranquillizers +tranquillizes +tranquillizing +tranquilly +transact +transacted +transacting +transaction +transaction's +transactions +transacts +transatlantic +transceiver +transceiver's +transceivers +transcend +transcended +transcendence +transcendence's +transcendent +transcendental +transcendentalism +transcendentalism's +transcendentalist +transcendentalist's +transcendentalists +transcendentally +transcending +transcends +transcontinental +transcribe +transcribed +transcribes +transcribing +transcript +transcript's +transcription +transcription's +transcriptions +transcripts +transducer +transducer's +transducers +transept +transept's +transepts +transfer +transfer's +transferable +transferal +transferal's +transferals +transference +transference's +transferred +transferring +transfers +transfiguration +transfiguration's +transfigure +transfigured +transfigures +transfiguring +transfinite +transfix +transfixed +transfixes +transfixing +transfixt +transform +transform's +transformation +transformation's +transformations +transformed +transformer +transformer's +transformers +transforming +transforms +transfuse +transfused +transfuses +transfusing +transfusion +transfusion's +transfusions +transgress +transgressed +transgresses +transgressing +transgression +transgression's +transgressions +transgressor +transgressor's +transgressors +transience +transience's +transiency +transiency's +transient +transient's +transients +transistor +transistor's +transistors +transit +transit's +transited +transiting +transition +transition's +transitional +transitioned +transitioning +transitions +transitive +transitive's +transitively +transitives +transitory +transits +transitted +transitting +translate +translated +translates +translating +translation +translation's +translations +translator +translator's +translators +transliterate +transliterated +transliterates +transliterating +transliteration +transliteration's +transliterations +translucence +translucence's +translucent +transmigrate +transmigrated +transmigrates +transmigrating +transmigration +transmigration's +transmissible +transmission +transmission's +transmissions +transmit +transmits +transmittable +transmittal +transmittal's +transmitted +transmitter +transmitter's +transmitters +transmitting +transmutation +transmutation's +transmutations +transmute +transmuted +transmutes +transmuting +transnational +transnational's +transnationals +transoceanic +transom +transom's +transoms +transparencies +transparency +transparency's +transparent +transparently +transpiration +transpiration's +transpire +transpired +transpires +transpiring +transplant +transplant's +transplantation +transplantation's +transplanted +transplanting +transplants +transponder +transponder's +transponders +transport +transport's +transportable +transportation +transportation's +transported +transporter +transporter's +transporters +transporting +transports +transpose +transposed +transposes +transposing +transposition +transposition's +transpositions +transsexual +transsexual's +transsexuals +transship +transshipment +transshipment's +transshipped +transshipping +transships +transubstantiation +transubstantiation's +transverse +transverse's +transversely +transverses +transvestism +transvestism's +transvestite +transvestite's +transvestites +trap +trap's +trapdoor +trapdoor's +trapdoors +trapeze +trapeze's +trapezes +trapezoid +trapezoid's +trapezoidal +trapezoids +trappable +trapped +trapper +trapper's +trappers +trapping +trappings +trappings's +traps +trapshooting +trapshooting's +trash +trash's +trashcan +trashcan's +trashcans +trashed +trashes +trashier +trashiest +trashing +trashy +trauma +trauma's +traumas +traumata +traumatic +traumatize +traumatized +traumatizes +traumatizing +travail +travail's +travailed +travailing +travails +travel +travel's +traveled +traveler +traveler's +travelers +traveling +travelings +travelled +traveller +traveller's +travellers +travelling +travelog +travelog's +travelogs +travelogue +travelogue's +travelogues +travels +traverse +traverse's +traversed +traverses +traversing +travestied +travesties +travesty +travesty's +travestying +trawl +trawl's +trawled +trawler +trawler's +trawlers +trawling +trawls +tray +tray's +trays +treacheries +treacherous +treacherously +treachery +treachery's +treacle +treacle's +tread +tread's +treading +treadle +treadle's +treadled +treadles +treadling +treadmill +treadmill's +treadmills +treads +treason +treason's +treasonable +treasonous +treasure +treasure's +treasured +treasurer +treasurer's +treasurers +treasures +treasuries +treasuring +treasury +treasury's +treat +treat's +treatable +treated +treaties +treating +treatise +treatise's +treatises +treatment +treatment's +treatments +treats +treaty +treaty's +treble +treble's +trebled +trebles +trebling +tree +tree's +treed +treeing +treeless +trees +treetop +treetop's +treetops +trefoil +trefoil's +trefoils +trek +trek's +trekked +trekking +treks +trellis +trellis's +trellised +trellises +trellising +tremble +tremble's +trembled +trembles +trembling +tremendous +tremendously +tremolo +tremolo's +tremolos +tremor +tremor's +tremors +tremulous +tremulously +trench +trench's +trenchant +trenchantly +trenched +trenches +trenching +trend +trend's +trended +trendier +trendies +trendiest +trending +trends +trendy +trendy's +trepidation +trepidation's +trespass +trespass's +trespassed +trespasser +trespasser's +trespassers +trespasses +trespassing +tress +tress's +tresses +trestle +trestle's +trestles +triad +triad's +triads +triage +triage's +trial +trial's +trialed +trialing +trials +triangle +triangle's +triangles +triangular +triangulation +triangulation's +triathlon +triathlon's +triathlons +tribal +tribalism +tribalism's +tribe +tribe's +tribes +tribesman +tribesman's +tribesmen +tribulation +tribulation's +tribulations +tribunal +tribunal's +tribunals +tribune +tribune's +tribunes +tributaries +tributary +tributary's +tribute +tribute's +tributes +trice +trice's +triceps +triceps's +tricepses +triceratops +triceratops's +triceratopses +trick +trick's +tricked +trickery +trickery's +trickier +trickiest +trickiness +trickiness's +tricking +trickle +trickle's +trickled +trickles +trickling +tricks +trickster +trickster's +tricksters +tricky +tricolor +tricolor's +tricolors +tricycle +tricycle's +tricycles +trident +trident's +tridents +tried +triennial +triennial's +triennials +tries +trifecta +trifecta's +trifectas +trifle +trifle's +trifled +trifler +trifler's +triflers +trifles +trifling +trifocals +trifocals's +trig +trig's +trigger +trigger's +triggered +triggering +triggers +triglyceride +triglyceride's +triglycerides +trigonometric +trigonometry +trigonometry's +trike +trike's +trikes +trilateral +trilaterals +trill +trill's +trilled +trilling +trillion +trillion's +trillions +trillionth +trillionth's +trillionths +trills +trilogies +trilogy +trilogy's +trim +trim's +trimaran +trimaran's +trimarans +trimester +trimester's +trimesters +trimly +trimmed +trimmer +trimmer's +trimmers +trimmest +trimming +trimming's +trimmings +trimmings's +trimness +trimness's +trims +trinities +trinity +trinity's +trinket +trinket's +trinkets +trio +trio's +trios +trip +trip's +tripartite +tripe +tripe's +triple +triple's +tripled +triples +triplet +triplet's +triplets +triplicate +triplicate's +triplicated +triplicates +triplicating +tripling +triply +tripod +tripod's +tripods +tripos +tripped +tripping +trips +triptych +triptych's +triptychs +trisect +trisected +trisecting +trisects +trite +tritely +triteness +triteness's +triter +tritest +triumph +triumph's +triumphal +triumphant +triumphantly +triumphed +triumphing +triumphs +triumvirate +triumvirate's +triumvirates +trivet +trivet's +trivets +trivia +trivia's +trivial +trivialities +triviality +triviality's +trivialize +trivialized +trivializes +trivializing +trivially +trochee +trochee's +trochees +trod +trodden +troglodyte +troglodyte's +troglodytes +troika +troika's +troikas +troll +troll's +trolled +trolley +trolley's +trolleys +trollies +trolling +trollop +trollop's +trollops +trolls +trolly +trolly's +trombone +trombone's +trombones +trombonist +trombonist's +trombonists +tromp +tromped +tromping +tromps +troop +troop's +trooped +trooper +trooper's +troopers +trooping +troops +troopship +troopship's +troopships +trope +trope's +tropes +trophies +trophy +trophy's +tropic +tropic's +tropical +tropics +tropics's +tropism +tropism's +tropisms +troposphere +troposphere's +tropospheres +trot +trot's +troth +troth's +trots +trotted +trotter +trotter's +trotters +trotting +troubadour +troubadour's +troubadours +trouble +trouble's +troubled +troublemaker +troublemaker's +troublemakers +troubles +troubleshoot +troubleshooted +troubleshooter +troubleshooter's +troubleshooters +troubleshooting +troubleshooting's +troubleshoots +troubleshot +troublesome +troubling +trough +trough's +troughs +trounce +trounced +trounces +trouncing +troupe +troupe's +trouped +trouper +trouper's +troupers +troupes +trouping +trouser +trouser's +trousers +trousers's +trousseau +trousseau's +trousseaus +trousseaux +trout +trout's +trouts +trowel +trowel's +troweled +troweling +trowelled +trowelling +trowels +troy +troys +truancy +truancy's +truant +truant's +truanted +truanting +truants +truce +truce's +truces +truck +truck's +trucked +trucker +trucker's +truckers +trucking +trucking's +truckle +truckle's +truckled +truckles +truckling +truckload +truckload's +truckloads +trucks +truculence +truculence's +truculent +truculently +trudge +trudge's +trudged +trudges +trudging +true +true's +trued +trueing +truer +trues +truest +truffle +truffle's +truffles +truing +truism +truism's +truisms +truly +trump +trump's +trumped +trumpery +trumpery's +trumpet +trumpet's +trumpeted +trumpeter +trumpeter's +trumpeters +trumpeting +trumpets +trumping +trumps +truncate +truncated +truncates +truncating +truncation +truncation's +truncheon +truncheon's +truncheons +trundle +trundle's +trundled +trundles +trundling +trunk +trunk's +trunking +trunks +truss +truss's +trussed +trusses +trussing +trust +trust's +trusted +trustee +trustee's +trustees +trusteeship +trusteeship's +trusteeships +trustful +trustfully +trustfulness +trustfulness's +trustier +trusties +trustiest +trusting +trusts +trustworthier +trustworthiest +trustworthiness +trustworthiness's +trustworthy +trusty +trusty's +truth +truth's +truther +truther's +truthers +truthful +truthfully +truthfulness +truthfulness's +truthiness +truths +try +try's +trying +tryout +tryout's +tryouts +tryst +tryst's +trysted +trysting +trysts +ts +tsar +tsar's +tsarina +tsarina's +tsarinas +tsars +tsunami +tsunami's +tsunamis +tub +tub's +tuba +tuba's +tubas +tubbier +tubbiest +tubby +tube +tube's +tubed +tubeless +tubeless's +tuber +tuber's +tubercle +tubercle's +tubercles +tubercular +tuberculosis +tuberculosis's +tuberculous +tuberous +tubers +tubes +tubing +tubing's +tubs +tubular +tuck +tuck's +tucked +tucker +tucker's +tuckered +tuckering +tuckers +tucking +tucks +tuft +tuft's +tufted +tufting +tufts +tug +tug's +tugboat +tugboat's +tugboats +tugged +tugging +tugs +tuition +tuition's +tulip +tulip's +tulips +tulle +tulle's +tumble +tumble's +tumbled +tumbledown +tumbler +tumbler's +tumblers +tumbles +tumbleweed +tumbleweed's +tumbleweeds +tumbling +tumbrel +tumbrel's +tumbrels +tumbril +tumbril's +tumbrils +tumid +tummies +tummy +tummy's +tumor +tumor's +tumors +tumult +tumult's +tumults +tumultuous +tun +tun's +tuna +tuna's +tunas +tundra +tundra's +tundras +tune +tune's +tuned +tuneful +tunefully +tuneless +tunelessly +tuner +tuner's +tuners +tunes +tungsten +tungsten's +tunic +tunic's +tunics +tuning +tunnel +tunnel's +tunneled +tunneling +tunnelings +tunnelled +tunnelling +tunnels +tunnies +tunny +tunny's +tuns +turban +turban's +turbans +turbid +turbine +turbine's +turbines +turbojet +turbojet's +turbojets +turboprop +turboprop's +turboprops +turbot +turbot's +turbots +turbulence +turbulence's +turbulent +turbulently +turd +turd's +turds +turducken +turducken's +turduckens +tureen +tureen's +tureens +turf +turf's +turfed +turfing +turfs +turgid +turgidity +turgidity's +turgidly +turkey +turkey's +turkeys +turmeric +turmeric's +turmerics +turmoil +turmoil's +turmoils +turn +turn's +turnabout +turnabout's +turnabouts +turnaround +turnaround's +turnarounds +turncoat +turncoat's +turncoats +turned +turner +turner's +turners +turning +turnip +turnip's +turnips +turnkey +turnkey's +turnkeys +turnoff +turnoff's +turnoffs +turnout +turnout's +turnouts +turnover +turnover's +turnovers +turnpike +turnpike's +turnpikes +turns +turnstile +turnstile's +turnstiles +turntable +turntable's +turntables +turpentine +turpentine's +turpitude +turpitude's +turquoise +turquoise's +turquoises +turret +turret's +turrets +turtle +turtle's +turtledove +turtledove's +turtledoves +turtleneck +turtleneck's +turtlenecks +turtles +turves +tush +tush's +tushes +tusk +tusk's +tusked +tusks +tussle +tussle's +tussled +tussles +tussling +tussock +tussock's +tussocks +tutelage +tutelage's +tutor +tutor's +tutored +tutorial +tutorial's +tutorials +tutoring +tutors +tutu +tutu's +tutus +tux +tux's +tuxedo +tuxedo's +tuxedoes +tuxedos +tuxes +twaddle +twaddle's +twaddled +twaddles +twaddling +twain +twain's +twang +twang's +twanged +twanging +twangs +tweak +tweak's +tweaked +tweaking +tweaks +twee +tweed +tweed's +tweedier +tweediest +tweeds +tweeds's +tweedy +tweet +tweet's +tweeted +tweeter +tweeter's +tweeters +tweeting +tweets +tweezers +tweezers's +twelfth +twelfth's +twelfths +twelve +twelve's +twelves +twenties +twentieth +twentieth's +twentieths +twenty +twenty's +twerk +twerked +twerking +twerks +twerp +twerp's +twerps +twice +twiddle +twiddle's +twiddled +twiddles +twiddling +twig +twig's +twigged +twiggier +twiggiest +twigging +twiggy +twigs +twilight +twilight's +twill +twill's +twilled +twin +twin's +twine +twine's +twined +twines +twinge +twinge's +twinged +twingeing +twinges +twinging +twining +twinkle +twinkle's +twinkled +twinkles +twinkling +twinkling's +twinklings +twinned +twinning +twins +twirl +twirl's +twirled +twirler +twirler's +twirlers +twirling +twirls +twist +twist's +twisted +twister +twister's +twisters +twisting +twists +twit +twit's +twitch +twitch's +twitched +twitches +twitching +twits +twitted +twitter +twitter's +twittered +twittering +twitters +twitting +two +two's +twofer +twofer's +twofers +twofold +twos +twosome +twosome's +twosomes +tycoon +tycoon's +tycoons +tying +tyke +tyke's +tykes +tympana +tympanum +tympanum's +tympanums +type +type's +typecast +typecasting +typecasts +typed +typeface +typeface's +typefaces +types +typescript +typescript's +typescripts +typeset +typesets +typesetter +typesetter's +typesetters +typesetting +typewrite +typewriter +typewriter's +typewriters +typewrites +typewriting +typewritten +typewrote +typhoid +typhoid's +typhoon +typhoon's +typhoons +typhus +typhus's +typical +typically +typified +typifies +typify +typifying +typing +typing's +typist +typist's +typists +typo +typo's +typographer +typographer's +typographers +typographic +typographical +typographically +typography +typography's +typos +tyrannical +tyrannically +tyrannies +tyrannize +tyrannized +tyrannizes +tyrannizing +tyrannosaur +tyrannosaur's +tyrannosaurs +tyrannosaurus +tyrannosaurus's +tyrannosauruses +tyrannous +tyranny +tyranny's +tyrant +tyrant's +tyrants +tyro +tyro's +tyroes +tyros +tzar +tzar's +tzarina +tzarina's +tzarinas +tzars +u +ubiquitous +ubiquitously +ubiquity +ubiquity's +udder +udder's +udders +ugh +uglier +ugliest +ugliness +ugliness's +ugly +uh +ukelele +ukelele's +ukeleles +ukulele +ukulele's +ukuleles +ulcer +ulcer's +ulcerate +ulcerated +ulcerates +ulcerating +ulceration +ulceration's +ulcerations +ulcerous +ulcers +ulna +ulna's +ulnae +ulnas +ulterior +ultimata +ultimate +ultimate's +ultimately +ultimatum +ultimatum's +ultimatums +ultra +ultra's +ultraconservative +ultraconservative's +ultraconservatives +ultramarine +ultramarine's +ultras +ultrasonic +ultrasonically +ultrasound +ultrasound's +ultrasounds +ultraviolet +ultraviolet's +ululate +ululated +ululates +ululating +um +umbel +umbel's +umbels +umber +umber's +umbilical +umbilici +umbilicus +umbilicus's +umbilicuses +umbrage +umbrage's +umbrella +umbrella's +umbrellas +umiak +umiak's +umiaks +umlaut +umlaut's +umlauts +ump +ump's +umped +umping +umpire +umpire's +umpired +umpires +umpiring +umps +umpteen +umpteenth +unabashed +unabated +unable +unabridged +unabridged's +unabridgeds +unaccented +unacceptability +unacceptable +unacceptably +unaccepted +unaccompanied +unaccountable +unaccountably +unaccustomed +unacknowledged +unacquainted +unadorned +unadulterated +unadvised +unaffected +unafraid +unaided +unalterable +unalterably +unaltered +unambiguous +unambiguously +unanimity +unanimity's +unanimous +unanimously +unannounced +unanswerable +unanswered +unanticipated +unappealing +unappetizing +unappreciated +unappreciative +unapproachable +unarmed +unashamed +unashamedly +unasked +unassailable +unassigned +unassisted +unassuming +unattached +unattainable +unattended +unattractive +unattributed +unauthenticated +unauthorized +unavailable +unavailing +unavoidable +unavoidably +unaware +unawares +unbalanced +unbar +unbarred +unbarring +unbars +unbearable +unbearably +unbeatable +unbeaten +unbecoming +unbeknown +unbeknownst +unbelief +unbelief's +unbelievable +unbelievably +unbeliever +unbeliever's +unbelievers +unbend +unbending +unbends +unbent +unbiased +unbiassed +unbidden +unbind +unbinding +unbinds +unblock +unblocked +unblocking +unblocks +unblushing +unbolt +unbolted +unbolting +unbolts +unborn +unbosom +unbosomed +unbosoming +unbosoms +unbound +unbounded +unbranded +unbreakable +unbridled +unbroken +unbuckle +unbuckled +unbuckles +unbuckling +unburden +unburdened +unburdening +unburdens +unbutton +unbuttoned +unbuttoning +unbuttons +uncalled +uncannier +uncanniest +uncannily +uncanny +uncaring +uncased +uncatalogued +unceasing +unceasingly +uncensored +unceremonious +unceremoniously +uncertain +uncertainly +uncertainties +uncertainty +uncertainty's +unchallenged +unchanged +unchanging +uncharacteristic +uncharacteristically +uncharitable +uncharitably +uncharted +unchecked +unchristian +uncivil +uncivilized +unclaimed +unclasp +unclasped +unclasping +unclasps +unclassified +uncle +uncle's +unclean +uncleaner +uncleanest +uncleanlier +uncleanliest +uncleanly +uncleanness +uncleanness's +unclear +unclearer +unclearest +uncles +unclothe +unclothed +unclothes +unclothing +uncluttered +uncoil +uncoiled +uncoiling +uncoils +uncollected +uncomfortable +uncomfortably +uncommitted +uncommon +uncommoner +uncommonest +uncommonly +uncommunicative +uncomplaining +uncompleted +uncomplicated +uncomplimentary +uncomprehending +uncompressed +uncompromising +uncompromisingly +unconcern +unconcern's +unconcerned +unconcernedly +unconditional +unconditionally +unconfirmed +unconnected +unconquerable +unconscionable +unconscionably +unconscious +unconscious's +unconsciously +unconsciousness +unconsciousness's +unconsidered +unconstitutional +uncontaminated +uncontested +uncontrollable +uncontrollably +uncontrolled +uncontroversial +unconventional +unconventionally +unconvinced +unconvincing +unconvincingly +uncooked +uncooperative +uncoordinated +uncork +uncorked +uncorking +uncorks +uncorrelated +uncorroborated +uncountable +uncounted +uncouple +uncoupled +uncouples +uncoupling +uncouth +uncover +uncovered +uncovering +uncovers +uncritical +unction +unction's +unctions +unctuous +unctuously +unctuousness +unctuousness's +uncultivated +uncultured +uncut +undamaged +undated +undaunted +undeceive +undeceived +undeceives +undeceiving +undecidable +undecided +undecided's +undecideds +undecipherable +undeclared +undefeated +undefended +undefinable +undefined +undelivered +undemanding +undemocratic +undemonstrative +undeniable +undeniably +undependable +under +underachieve +underachieved +underachiever +underachiever's +underachievers +underachieves +underachieving +underact +underacted +underacting +underacts +underage +underarm +underarm's +underarms +underbellies +underbelly +underbelly's +underbid +underbidding +underbids +underbrush +underbrush's +undercarriage +undercarriage's +undercarriages +undercharge +undercharge's +undercharged +undercharges +undercharging +underclass +underclass's +underclassman +underclassman's +underclassmen +underclothes +underclothes's +underclothing +underclothing's +undercoat +undercoat's +undercoated +undercoating +undercoats +undercover +undercurrent +undercurrent's +undercurrents +undercut +undercut's +undercuts +undercutting +underdeveloped +underdog +underdog's +underdogs +underdone +underemployed +underestimate +underestimate's +underestimated +underestimates +underestimating +underexpose +underexposed +underexposes +underexposing +underfed +underfeed +underfeeding +underfeeds +underflow +underfoot +underfunded +undergarment +undergarment's +undergarments +undergo +undergoes +undergoing +undergone +undergrad +undergrads +undergraduate +undergraduate's +undergraduates +underground +underground's +undergrounds +undergrowth +undergrowth's +underhand +underhanded +underhandedly +underlain +underlay +underlay's +underlays +underlie +underlies +underline +underline's +underlined +underlines +underling +underling's +underlings +underlining +underlying +undermine +undermined +undermines +undermining +undermost +underneath +underneath's +underneaths +undernourished +underpaid +underpants +underpants's +underpass +underpass's +underpasses +underpay +underpaying +underpays +underpin +underpinned +underpinning +underpinning's +underpinnings +underpins +underplay +underplayed +underplaying +underplays +underprivileged +underrate +underrated +underrates +underrating +underscore +underscore's +underscored +underscores +underscoring +undersea +undersecretaries +undersecretary +undersecretary's +undersell +underselling +undersells +undershirt +undershirt's +undershirts +undershoot +undershooting +undershoots +undershorts +undershorts's +undershot +underside +underside's +undersides +undersign +undersigned +undersigned's +undersigning +undersigns +undersize +undersized +underskirt +underskirt's +underskirts +undersold +understaffed +understand +understandable +understandably +understanding +understanding's +understandingly +understandings +understands +understate +understated +understatement +understatement's +understatements +understates +understating +understood +understudied +understudies +understudy +understudy's +understudying +undertake +undertaken +undertaker +undertaker's +undertakers +undertakes +undertaking +undertaking's +undertakings +undertone +undertone's +undertones +undertook +undertow +undertow's +undertows +underused +undervalue +undervalued +undervalues +undervaluing +underwater +underwear +underwear's +underweight +underweight's +underwent +underworld +underworld's +underworlds +underwrite +underwriter +underwriter's +underwriters +underwrites +underwriting +underwritten +underwrote +undeserved +undeservedly +undeserving +undesirability +undesirable +undesirable's +undesirables +undetectable +undetected +undetermined +undeterred +undeveloped +undid +undies +undies's +undignified +undiluted +undiminished +undisciplined +undisclosed +undiscovered +undiscriminating +undisguised +undisputed +undistinguished +undisturbed +undivided +undo +undocumented +undoes +undoing +undoing's +undoings +undone +undoubted +undoubtedly +undress +undress's +undressed +undresses +undressing +undue +undulant +undulate +undulated +undulates +undulating +undulation +undulation's +undulations +unduly +undying +unearned +unearth +unearthed +unearthing +unearthly +unearths +unease +unease's +uneasier +uneasiest +uneasily +uneasiness +uneasiness's +uneasy +uneaten +uneconomic +uneconomical +unedited +uneducated +unembarrassed +unemotional +unemployable +unemployed +unemployed's +unemployment +unemployment's +unending +unendurable +unenforceable +unenlightened +unenthusiastic +unenviable +unequal +unequaled +unequalled +unequally +unequivocal +unequivocally +unerring +unerringly +unethical +uneven +unevenly +unevenness +unevenness's +uneventful +uneventfully +unexampled +unexceptionable +unexceptional +unexciting +unexpected +unexpectedly +unexplained +unexplored +unexpurgated +unfailing +unfailingly +unfair +unfairer +unfairest +unfairly +unfairness +unfairness's +unfaithful +unfaithfully +unfaithfulness +unfaithfulness's +unfamiliar +unfamiliarity +unfamiliarity's +unfashionable +unfasten +unfastened +unfastening +unfastens +unfathomable +unfavorable +unfavorably +unfeasible +unfeeling +unfeelingly +unfeigned +unfetter +unfettered +unfettering +unfetters +unfilled +unfinished +unfit +unfits +unfitted +unfitting +unflagging +unflappable +unflattering +unflinching +unflinchingly +unfold +unfolded +unfolding +unfolds +unforeseeable +unforeseen +unforgettable +unforgettably +unforgivable +unforgiving +unformed +unfortunate +unfortunate's +unfortunately +unfortunates +unfounded +unfrequented +unfriend +unfriended +unfriending +unfriendlier +unfriendliest +unfriendliness +unfriendliness's +unfriendly +unfriends +unfrock +unfrocked +unfrocking +unfrocks +unfulfilled +unfunny +unfurl +unfurled +unfurling +unfurls +unfurnished +ungainlier +ungainliest +ungainliness +ungainliness's +ungainly +ungentlemanly +ungodlier +ungodliest +ungodly +ungovernable +ungracious +ungrammatical +ungrateful +ungratefully +ungratefulness +ungratefulness's +ungrudging +unguarded +unguent +unguent's +unguents +ungulate +ungulate's +ungulates +unhand +unhanded +unhanding +unhands +unhappier +unhappiest +unhappily +unhappiness +unhappiness's +unhappy +unharmed +unhealthful +unhealthier +unhealthiest +unhealthy +unheard +unheeded +unhelpful +unhesitating +unhesitatingly +unhindered +unhinge +unhinged +unhinges +unhinging +unhitch +unhitched +unhitches +unhitching +unholier +unholiest +unholy +unhook +unhooked +unhooking +unhooks +unhorse +unhorsed +unhorses +unhorsing +unhurried +unhurt +unicameral +unicorn +unicorn's +unicorns +unicycle +unicycle's +unicycles +unidentifiable +unidentified +unidirectional +unification +unification's +unified +unifies +uniform +uniform's +uniformed +uniforming +uniformity +uniformity's +uniformly +uniforms +unify +unifying +unilateral +unilaterally +unimaginable +unimaginative +unimpaired +unimpeachable +unimplementable +unimplemented +unimportant +unimpressed +unimpressive +uninformative +uninformed +uninhabitable +uninhabited +uninhibited +uninitialized +uninitiated +uninjured +uninspired +uninspiring +uninstall +uninstallable +uninstalled +uninstaller +uninstaller's +uninstallers +uninstalling +uninstalls +uninsured +unintelligent +unintelligible +unintelligibly +unintended +unintentional +unintentionally +uninterested +uninteresting +uninterpreted +uninterrupted +uninvited +uninviting +union +union's +unionization +unionization's +unionize +unionized +unionizes +unionizing +unions +unique +uniquely +uniqueness +uniqueness's +uniquer +uniquest +unisex +unisex's +unison +unison's +unit +unit's +unitary +unite +united +unites +unities +uniting +units +unity +unity's +universal +universal's +universality +universality's +universally +universals +universe +universe's +universes +universities +university +university's +unjust +unjustifiable +unjustified +unjustly +unkempt +unkind +unkinder +unkindest +unkindlier +unkindliest +unkindly +unkindness +unkindness's +unknowable +unknowing +unknowingly +unknowings +unknown +unknown's +unknowns +unlabeled +unlace +unlaced +unlaces +unlacing +unlatch +unlatched +unlatches +unlatching +unlawful +unlawfully +unleaded +unleaded's +unlearn +unlearned +unlearning +unlearns +unleash +unleashed +unleashes +unleashing +unleavened +unless +unlettered +unlicensed +unlike +unlikelier +unlikeliest +unlikelihood +unlikelihood's +unlikely +unlimited +unlisted +unload +unloaded +unloading +unloads +unlock +unlocked +unlocking +unlocks +unloose +unloosed +unlooses +unloosing +unloved +unluckier +unluckiest +unluckily +unlucky +unmade +unmake +unmakes +unmaking +unman +unmanageable +unmanlier +unmanliest +unmanly +unmanned +unmannerly +unmanning +unmans +unmarked +unmarried +unmask +unmasked +unmasking +unmasks +unmatched +unmemorable +unmentionable +unmentionable's +unmentionables +unmerciful +unmercifully +unmindful +unmissed +unmistakable +unmistakably +unmitigated +unmodified +unmoral +unmoved +unnamed +unnatural +unnaturally +unnecessarily +unnecessary +unneeded +unnerve +unnerved +unnerves +unnerving +unnoticeable +unnoticed +unnumbered +unobjectionable +unobservant +unobserved +unobstructed +unobtainable +unobtrusive +unobtrusively +unoccupied +unoffensive +unofficial +unofficially +unopened +unopposed +unorganized +unoriginal +unorthodox +unpack +unpacked +unpacking +unpacks +unpaid +unpainted +unpalatable +unparalleled +unpardonable +unpatriotic +unpaved +unperturbed +unpick +unpin +unpinned +unpinning +unpins +unplanned +unpleasant +unpleasantly +unpleasantness +unpleasantness's +unplug +unplugged +unplugging +unplugs +unplumbed +unpolluted +unpopular +unpopularity +unpopularity's +unprecedented +unpredictability +unpredictability's +unpredictable +unprejudiced +unpremeditated +unprepared +unpretentious +unpreventable +unprincipled +unprintable +unprivileged +unproductive +unprofessional +unprofitable +unpromising +unprompted +unpronounceable +unprotected +unproved +unproven +unprovoked +unpublished +unpunished +unqualified +unquenchable +unquestionable +unquestionably +unquestioned +unquestioning +unquestioningly +unquote +unquoted +unquotes +unquoting +unravel +unraveled +unraveling +unravelled +unravelling +unravels +unreachable +unread +unreadable +unready +unreal +unrealistic +unrealistically +unrealized +unreasonable +unreasonableness +unreasonableness's +unreasonably +unreasoning +unrecognizable +unrecognized +unreconstructed +unrecorded +unrefined +unregenerate +unregistered +unregulated +unrehearsed +unrelated +unreleased +unrelenting +unrelentingly +unreliability +unreliable +unrelieved +unremarkable +unremitting +unrepeatable +unrepentant +unrepresentative +unrequited +unreserved +unreservedly +unresolved +unresponsive +unrest +unrest's +unrestrained +unrestricted +unrewarding +unripe +unriper +unripest +unrivaled +unrivalled +unroll +unrolled +unrolling +unrolls +unromantic +unruffled +unrulier +unruliest +unruliness +unruliness's +unruly +unsaddle +unsaddled +unsaddles +unsaddling +unsafe +unsafer +unsafest +unsaid +unsalted +unsanctioned +unsanitary +unsatisfactory +unsatisfied +unsatisfying +unsaturated +unsavory +unsay +unsaying +unsays +unscathed +unscheduled +unschooled +unscientific +unscramble +unscrambled +unscrambles +unscrambling +unscrew +unscrewed +unscrewing +unscrews +unscrupulous +unscrupulously +unscrupulousness +unscrupulousness's +unseal +unsealed +unsealing +unseals +unseasonable +unseasonably +unseasoned +unseat +unseated +unseating +unseats +unseeing +unseemlier +unseemliest +unseemliness +unseemliness's +unseemly +unseen +unseen's +unselfish +unselfishly +unselfishness +unselfishness's +unsent +unsentimental +unset +unsettle +unsettled +unsettles +unsettling +unshakable +unshakeable +unshaven +unsheathe +unsheathed +unsheathes +unsheathing +unsightlier +unsightliest +unsightliness +unsightliness's +unsightly +unsigned +unskilled +unskillful +unsmiling +unsnap +unsnapped +unsnapping +unsnaps +unsnarl +unsnarled +unsnarling +unsnarls +unsociable +unsold +unsolicited +unsolved +unsophisticated +unsound +unsounder +unsoundest +unsparing +unspeakable +unspeakably +unspecific +unspecified +unspoiled +unspoilt +unspoken +unsportsmanlike +unstable +unstated +unsteadier +unsteadiest +unsteadily +unsteadiness +unsteadiness's +unsteady +unstop +unstoppable +unstopped +unstopping +unstops +unstressed +unstructured +unstrung +unstuck +unstudied +unsubscribe +unsubscribed +unsubscribes +unsubscribing +unsubstantial +unsubstantiated +unsubtle +unsuccessful +unsuccessfully +unsuitable +unsuitably +unsuited +unsung +unsupervised +unsupportable +unsupported +unsure +unsurpassed +unsurprising +unsuspected +unsuspecting +unsweetened +unswerving +unsympathetic +untainted +untamed +untangle +untangled +untangles +untangling +untapped +untaught +untenable +untested +unthinkable +unthinking +unthinkingly +untidier +untidiest +untidiness +untidiness's +untidy +untie +untied +unties +until +untimelier +untimeliest +untimeliness +untimeliness's +untimely +untiring +untiringly +untitled +unto +untold +untouchable +untouchable's +untouchables +untouched +untoward +untrained +untreated +untried +untroubled +untrue +untruer +untruest +untrustworthy +untruth +untruth's +untruthful +untruthfully +untruths +untutored +untwist +untwisted +untwisting +untwists +untying +unusable +unused +unusual +unusually +unutterable +unutterably +unvarnished +unvarying +unveil +unveiled +unveiling +unveils +unverified +unvoiced +unwanted +unwarier +unwariest +unwariness +unwariness's +unwarranted +unwary +unwashed +unwavering +unwed +unwelcome +unwell +unwholesome +unwieldier +unwieldiest +unwieldiness +unwieldiness's +unwieldy +unwilling +unwillingly +unwillingness +unwillingness's +unwind +unwinding +unwinds +unwise +unwisely +unwiser +unwisest +unwitting +unwittingly +unwonted +unworkable +unworldly +unworthier +unworthiest +unworthiness +unworthiness's +unworthy +unwound +unwrap +unwrapped +unwrapping +unwraps +unwritten +unyielding +unzip +unzipped +unzipping +unzips +up +upbeat +upbeat's +upbeats +upbraid +upbraided +upbraiding +upbraids +upbringing +upbringing's +upbringings +upchuck +upchucked +upchucking +upchucks +upcoming +upcountry +upcountry's +update +update's +updated +updater +updates +updating +updraft +updraft's +updrafts +upend +upended +upending +upends +upfront +upgrade +upgrade's +upgraded +upgrades +upgrading +upheaval +upheaval's +upheavals +upheld +uphill +uphill's +uphills +uphold +upholding +upholds +upholster +upholstered +upholsterer +upholsterer's +upholsterers +upholstering +upholsters +upholstery +upholstery's +upkeep +upkeep's +upland +upland's +uplands +uplift +uplift's +uplifted +uplifting +upliftings +uplifts +upload +upmarket +upon +upped +upper +upper's +uppercase +uppercase's +upperclassman +upperclassman's +upperclassmen +uppercut +uppercut's +uppercuts +uppercutting +uppermost +uppers +upping +uppity +upraise +upraised +upraises +upraising +upright +upright's +uprights +uprising +uprising's +uprisings +uproar +uproar's +uproarious +uproariously +uproars +uproot +uprooted +uprooting +uproots +ups +upscale +upset +upset's +upsets +upsetting +upshot +upshot's +upshots +upside +upside's +upsides +upstage +upstaged +upstages +upstaging +upstairs +upstanding +upstart +upstart's +upstarted +upstarting +upstarts +upstate +upstate's +upstream +upsurge +upsurge's +upsurged +upsurges +upsurging +upswing +upswing's +upswings +uptake +uptake's +uptakes +uptight +uptown +uptown's +upturn +upturn's +upturned +upturning +upturns +upward +upwardly +upwards +uranium +uranium's +urban +urbane +urbaner +urbanest +urbanity +urbanity's +urbanization +urbanization's +urbanize +urbanized +urbanizes +urbanizing +urchin +urchin's +urchins +urea +urea's +urethra +urethra's +urethrae +urethras +urge +urge's +urged +urgency +urgency's +urgent +urgently +urges +urging +uric +urinal +urinal's +urinals +urinalyses +urinalysis +urinalysis's +urinary +urinate +urinated +urinates +urinating +urination +urination's +urine +urine's +urn +urn's +urns +urologist +urologist's +urologists +urology +urology's +us +usability +usability's +usable +usage +usage's +usages +use +use's +useability +useability's +useable +used +useful +usefully +usefulness +usefulness's +useless +uselessly +uselessness +uselessness's +user +user's +username +username's +usernames +users +uses +usher +usher's +ushered +usherette +usherette's +usherettes +ushering +ushers +using +usual +usual's +usually +usurer +usurer's +usurers +usurious +usurp +usurpation +usurpation's +usurped +usurper +usurper's +usurpers +usurping +usurps +usury +usury's +utensil +utensil's +utensils +uteri +uterine +uterus +uterus's +uteruses +utilitarian +utilitarian's +utilitarianism +utilitarians +utilities +utility +utility's +utilization +utilization's +utilize +utilized +utilizes +utilizing +utmost +utmost's +utopia +utopia's +utopian +utopian's +utopians +utopias +utter +utterance +utterance's +utterances +uttered +uttering +utterly +uttermost +uttermost's +utters +uvula +uvula's +uvulae +uvular +uvular's +uvulars +uvulas +v +vacancies +vacancy +vacancy's +vacant +vacantly +vacate +vacated +vacates +vacating +vacation +vacation's +vacationed +vacationer +vacationer's +vacationers +vacationing +vacations +vaccinate +vaccinated +vaccinates +vaccinating +vaccination +vaccination's +vaccinations +vaccine +vaccine's +vaccines +vacillate +vacillated +vacillates +vacillating +vacillation +vacillation's +vacillations +vacua +vacuity +vacuity's +vacuous +vacuously +vacuum +vacuum's +vacuumed +vacuuming +vacuums +vagabond +vagabond's +vagabonded +vagabonding +vagabonds +vagaries +vagary +vagary's +vagina +vagina's +vaginae +vaginal +vagrancy +vagrancy's +vagrant +vagrant's +vagrants +vague +vaguely +vagueness +vagueness's +vaguer +vaguest +vain +vainer +vainest +vainglorious +vainglory +vainglory's +vainly +valance +valance's +valances +vale +vale's +valedictorian +valedictorian's +valedictorians +valedictories +valedictory +valedictory's +valence +valence's +valences +valentine +valentine's +valentines +vales +valet +valet's +valeted +valeting +valets +valiant +valiantly +valid +validate +validated +validates +validating +validation +validation's +validations +validity +validity's +validly +validness +validness's +valise +valise's +valises +valley +valley's +valleys +valor +valor's +valorous +valuable +valuable's +valuables +valuation +valuation's +valuations +value +value's +valued +valueless +values +valuing +valve +valve's +valved +valves +valving +vamoose +vamoosed +vamooses +vamoosing +vamp +vamp's +vamped +vamping +vampire +vampire's +vampires +vamps +van +van's +vanadium +vanadium's +vandal +vandal's +vandalism +vandalism's +vandalize +vandalized +vandalizes +vandalizing +vandals +vane +vane's +vanes +vanguard +vanguard's +vanguards +vanilla +vanilla's +vanillas +vanish +vanished +vanishes +vanishing +vanishings +vanities +vanity +vanity's +vanned +vanning +vanquish +vanquished +vanquishes +vanquishing +vans +vantage +vantage's +vantages +vape +vaped +vapes +vapid +vapidity +vapidity's +vapidness +vapidness's +vaping +vapor +vapor's +vaporization +vaporization's +vaporize +vaporized +vaporizer +vaporizer's +vaporizers +vaporizes +vaporizing +vaporous +vapors +variability +variability's +variable +variable's +variables +variably +variance +variance's +variances +variant +variant's +variants +variate +variation +variation's +variations +varicolored +varicose +varied +variegate +variegated +variegates +variegating +varies +varieties +variety +variety's +various +variously +varlet +varlet's +varlets +varmint +varmint's +varmints +varnish +varnish's +varnished +varnishes +varnishing +varsities +varsity +varsity's +vary +varying +vascular +vase +vase's +vasectomies +vasectomy +vasectomy's +vases +vassal +vassal's +vassalage +vassalage's +vassals +vast +vast's +vaster +vastest +vastly +vastness +vastness's +vasts +vat +vat's +vats +vatted +vatting +vaudeville +vaudeville's +vault +vault's +vaulted +vaulter +vaulter's +vaulters +vaulting +vaulting's +vaults +vaunt +vaunt's +vaunted +vaunting +vaunts +veal +veal's +vector +vector's +vectored +vectoring +vectors +veep +veep's +veeps +veer +veer's +veered +veering +veers +vegan +vegan's +vegans +vegetable +vegetable's +vegetables +vegetarian +vegetarian's +vegetarianism +vegetarianism's +vegetarians +vegetate +vegetated +vegetates +vegetating +vegetation +vegetation's +vegetative +veggie +veggie's +veggies +vehemence +vehemence's +vehement +vehemently +vehicle +vehicle's +vehicles +vehicular +veil +veil's +veiled +veiling +veils +vein +vein's +veined +veining +veins +veld +veld's +velds +veldt +veldt's +veldts +vellum +vellum's +velocities +velocity +velocity's +velour +velour's +velours +velours's +velvet +velvet's +velveteen +velveteen's +velvety +venal +venality +venality's +venally +vend +vended +vender +vender's +venders +vendetta +vendetta's +vendettas +vending +vendor +vendor's +vendors +vends +veneer +veneer's +veneered +veneering +veneers +venerable +venerate +venerated +venerates +venerating +veneration +veneration's +venereal +vengeance +vengeance's +vengeful +vengefully +venial +venison +venison's +venom +venom's +venomous +venomously +venous +vent +vent's +vented +ventilate +ventilated +ventilates +ventilating +ventilation +ventilation's +ventilator +ventilator's +ventilators +venting +ventral +ventricle +ventricle's +ventricles +ventricular +ventriloquism +ventriloquism's +ventriloquist +ventriloquist's +ventriloquists +vents +venture +venture's +ventured +ventures +venturesome +venturing +venturous +venue +venue's +venues +veracious +veracity +veracity's +veranda +veranda's +verandah +verandah's +verandahs +verandas +verb +verb's +verbal +verbal's +verbalize +verbalized +verbalizes +verbalizing +verbally +verbals +verbatim +verbena +verbena's +verbenas +verbiage +verbiage's +verbose +verbosity +verbosity's +verbs +verdant +verdict +verdict's +verdicts +verdigris +verdigris's +verdigrised +verdigrises +verdigrising +verdure +verdure's +verge +verge's +verged +verges +verging +verier +veriest +verifiable +verification +verification's +verified +verifies +verify +verifying +verily +verisimilitude +verisimilitude's +veritable +veritably +verities +verity +verity's +vermicelli +vermicelli's +vermilion +vermilion's +vermillion +vermillion's +vermin +vermin's +verminous +vermouth +vermouth's +vernacular +vernacular's +vernaculars +vernal +versatile +versatility +versatility's +verse +verse's +versed +verses +versification +versification's +versified +versifies +versify +versifying +versing +version +version's +versions +versus +vertebra +vertebra's +vertebrae +vertebral +vertebras +vertebrate +vertebrate's +vertebrates +vertex +vertex's +vertexes +vertical +vertical's +vertically +verticals +vertices +vertiginous +vertigo +vertigo's +verve +verve's +very +vesicle +vesicle's +vesicles +vesper +vesper's +vespers +vessel +vessel's +vessels +vest +vest's +vested +vestibule +vestibule's +vestibules +vestige +vestige's +vestiges +vestigial +vesting +vestment +vestment's +vestments +vestries +vestry +vestry's +vests +vet +vet's +vetch +vetch's +vetches +veteran +veteran's +veterans +veterinarian +veterinarian's +veterinarians +veterinaries +veterinary +veterinary's +veto +veto's +vetoed +vetoes +vetoing +vets +vetted +vetting +vex +vexation +vexation's +vexations +vexatious +vexed +vexes +vexing +via +viability +viability's +viable +viaduct +viaduct's +viaducts +vial +vial's +vials +viand +viand's +viands +vibe +vibe's +vibes +vibes's +vibrancy +vibrancy's +vibrant +vibrantly +vibraphone +vibraphone's +vibraphones +vibrate +vibrated +vibrates +vibrating +vibration +vibration's +vibrations +vibrato +vibrato's +vibrator +vibrator's +vibrators +vibratos +viburnum +viburnum's +viburnums +vicar +vicar's +vicarage +vicarage's +vicarages +vicarious +vicariously +vicars +vice +vice's +viced +viceroy +viceroy's +viceroys +vices +vichyssoise +vichyssoise's +vicing +vicinity +vicinity's +vicious +viciously +viciousness +viciousness's +vicissitude +vicissitude's +vicissitudes +victim +victim's +victimization +victimization's +victimize +victimized +victimizes +victimizing +victims +victor +victor's +victories +victorious +victoriously +victors +victory +victory's +victual +victual's +victualed +victualing +victualled +victualling +victuals +vicuña +vicuña's +vicuñas +video +video's +videocassette +videocassette's +videocassettes +videodisc +videodisc's +videodiscs +videos +videotape +videotape's +videotaped +videotapes +videotaping +vie +vied +vies +view +view's +viewed +viewer +viewer's +viewers +viewfinder +viewfinder's +viewfinders +viewing +viewing's +viewings +viewpoint +viewpoint's +viewpoints +views +vigil +vigil's +vigilance +vigilance's +vigilant +vigilante +vigilante's +vigilantes +vigilantism +vigilantism's +vigilantly +vigils +vignette +vignette's +vignetted +vignettes +vignetting +vigor +vigor's +vigorous +vigorously +vile +vilely +vileness +vileness's +viler +vilest +vilification +vilification's +vilified +vilifies +vilify +vilifying +villa +villa's +village +village's +villager +villager's +villagers +villages +villain +villain's +villainies +villainous +villains +villainy +villainy's +villas +villein +villein's +villeins +vim +vim's +vinaigrette +vinaigrette's +vindicate +vindicated +vindicates +vindicating +vindication +vindication's +vindications +vindicator +vindicator's +vindicators +vindictive +vindictively +vindictiveness +vindictiveness's +vine +vine's +vinegar +vinegar's +vinegary +vines +vineyard +vineyard's +vineyards +vintage +vintage's +vintages +vintner +vintner's +vintners +vinyl +vinyl's +vinyls +viol +viol's +viola +viola's +violable +violas +violate +violated +violates +violating +violation +violation's +violations +violator +violator's +violators +violence +violence's +violent +violently +violet +violet's +violets +violin +violin's +violinist +violinist's +violinists +violins +violist +violist's +violists +violoncello +violoncello's +violoncellos +viols +viper +viper's +vipers +virago +virago's +viragoes +viragos +viral +vireo +vireo's +vireos +virgin +virgin's +virginal +virginal's +virginals +virginity +virginity's +virgins +virgule +virgule's +virgules +virile +virility +virility's +virology +virology's +virtual +virtually +virtue +virtue's +virtues +virtuosi +virtuosity +virtuosity's +virtuoso +virtuoso's +virtuosos +virtuous +virtuously +virtuousness +virtuousness's +virulence +virulence's +virulent +virulently +virus +virus's +viruses +visa +visa's +visaed +visage +visage's +visages +visaing +visas +viscera +visceral +viscid +viscosity +viscosity's +viscount +viscount's +viscountess +viscountess's +viscountesses +viscounts +viscous +viscus +viscus's +vise +vise's +vised +vises +visibility +visibility's +visible +visibly +vising +vision +vision's +visionaries +visionary +visionary's +visioned +visioning +visions +visit +visit's +visitation +visitation's +visitations +visited +visiting +visitor +visitor's +visitors +visits +visor +visor's +visors +vista +vista's +vistas +visual +visual's +visualization +visualization's +visualize +visualized +visualizes +visualizing +visually +visuals +vital +vitality +vitality's +vitalize +vitalized +vitalizes +vitalizing +vitally +vitals +vitals's +vitamin +vitamin's +vitamins +vitiate +vitiated +vitiates +vitiating +vitiation +vitiation's +viticulture +viticulture's +vitreous +vitriol +vitriol's +vitriolic +vituperate +vituperated +vituperates +vituperating +vituperation +vituperation's +vituperative +viva +viva's +vivace +vivacious +vivaciously +vivaciousness +vivaciousness's +vivacity +vivacity's +vivas +vivid +vivider +vividest +vividly +vividness +vividness's +vivified +vivifies +vivify +vivifying +viviparous +vivisection +vivisection's +vixen +vixen's +vixenish +vixens +vizier +vizier's +viziers +vizor +vizor's +vizors +vocabularies +vocabulary +vocabulary's +vocal +vocal's +vocalic +vocalist +vocalist's +vocalists +vocalization +vocalization's +vocalizations +vocalize +vocalized +vocalizes +vocalizing +vocally +vocals +vocation +vocation's +vocational +vocations +vocative +vocative's +vocatives +vociferate +vociferated +vociferates +vociferating +vociferation +vociferation's +vociferous +vociferously +vodka +vodka's +vogue +vogue's +vogues +voguish +voice +voice's +voiced +voiceless +voicemail +voicemail's +voicemails +voices +voicing +void +void's +voided +voiding +voids +voile +voile's +volatile +volatility +volatility's +volcanic +volcano +volcano's +volcanoes +volcanos +vole +vole's +voles +volition +volition's +volley +volley's +volleyball +volleyball's +volleyballs +volleyed +volleying +volleys +volt +volt's +voltage +voltage's +voltages +voltaic +voltmeter +voltmeter's +voltmeters +volts +volubility +volubility's +voluble +volubly +volume +volume's +volumes +voluminous +voluminously +voluntaries +voluntarily +voluntary +voluntary's +volunteer +volunteer's +volunteered +volunteering +volunteers +voluptuaries +voluptuary +voluptuary's +voluptuous +voluptuously +voluptuousness +voluptuousness's +vomit +vomit's +vomited +vomiting +vomits +voodoo +voodoo's +voodooed +voodooing +voodooism +voodooism's +voodoos +voracious +voraciously +voracity +voracity's +vortex +vortex's +vortexes +vortices +votaries +votary +votary's +vote +vote's +voted +voter +voter's +voters +votes +voting +votive +vouch +vouched +voucher +voucher's +vouchers +vouches +vouching +vouchsafe +vouchsafed +vouchsafes +vouchsafing +vow +vow's +vowed +vowel +vowel's +vowels +vowing +vows +voyage +voyage's +voyaged +voyager +voyager's +voyagers +voyages +voyaging +voyeur +voyeur's +voyeurism +voyeurism's +voyeuristic +voyeurs +vulcanization +vulcanization's +vulcanize +vulcanized +vulcanizes +vulcanizing +vulgar +vulgarer +vulgarest +vulgarism +vulgarism's +vulgarisms +vulgarities +vulgarity +vulgarity's +vulgarization +vulgarization's +vulgarize +vulgarized +vulgarizes +vulgarizing +vulgarly +vulnerabilities +vulnerability +vulnerability's +vulnerable +vulnerably +vulture +vulture's +vultures +vulva +vulva's +vulvae +vulvas +vuvuzela +vuvuzela's +vuvuzelas +vying +w +wack +wack's +wacker +wackest +wackier +wackiest +wackiness +wackiness's +wacko +wacko's +wackos +wacks +wacky +wad +wad's +wadded +wadding +wadding's +waddle +waddle's +waddled +waddles +waddling +wade +wade's +waded +wader +wader's +waders +wades +wadi +wadi's +wading +wadis +wads +wafer +wafer's +wafers +waffle +waffle's +waffled +waffles +waffling +waft +waft's +wafted +wafting +wafts +wag +wag's +wage +wage's +waged +wager +wager's +wagered +wagering +wagers +wages +wagged +wagging +waggish +waggle +waggle's +waggled +waggles +waggling +waging +wagon +wagon's +wagoner +wagoner's +wagoners +wagons +wags +waif +waif's +waifs +wail +wail's +wailed +wailing +wails +wainscot +wainscot's +wainscoted +wainscoting +wainscoting's +wainscotings +wainscots +wainscotted +wainscotting +wainscotting's +wainscottings +waist +waist's +waistband +waistband's +waistbands +waistcoat +waistcoat's +waistcoats +waistline +waistline's +waistlines +waists +wait +wait's +waited +waiter +waiter's +waiters +waiting +waitress +waitress's +waitresses +waits +waive +waived +waiver +waiver's +waivers +waives +waiving +wake +wake's +waked +wakeful +wakefulness +wakefulness's +waken +wakened +wakening +wakens +wakes +waking +wale +wale's +waled +wales +waling +walk +walk's +walked +walker +walker's +walkers +walking +walkout +walkout's +walkouts +walks +walkway +walkway's +walkways +wall +wall's +wallabies +wallaby +wallaby's +wallboard +wallboard's +walled +wallet +wallet's +wallets +walleye +walleye's +walleyed +walleyes +wallflower +wallflower's +wallflowers +walling +wallop +wallop's +walloped +walloping +walloping's +wallopings +wallops +wallow +wallow's +wallowed +wallowing +wallows +wallpaper +wallpaper's +wallpapered +wallpapering +wallpapers +walls +walnut +walnut's +walnuts +walrus +walrus's +walruses +waltz +waltz's +waltzed +waltzes +waltzing +wampum +wampum's +wan +wand +wand's +wander +wandered +wanderer +wanderer's +wanderers +wandering +wanderlust +wanderlust's +wanderlusts +wanders +wands +wane +wane's +waned +wanes +wangle +wangle's +wangled +wangles +wangling +waning +wanly +wanna +wannabe +wannabe's +wannabes +wanner +wannest +want +want's +wanted +wanting +wanton +wanton's +wantoned +wantoning +wantonly +wantonness +wantonness's +wantons +wants +wapiti +wapiti's +wapitis +war +war's +warble +warble's +warbled +warbler +warbler's +warblers +warbles +warbling +ward +ward's +warded +warden +warden's +wardens +warder +warder's +warders +warding +wardrobe +wardrobe's +wardrobes +wardroom +wardroom's +wardrooms +wards +ware +ware's +warehouse +warehouse's +warehoused +warehouses +warehousing +wares +warfare +warfare's +warhead +warhead's +warheads +warhorse +warhorse's +warhorses +warier +wariest +warily +wariness +wariness's +warlike +warlock +warlock's +warlocks +warlord +warlord's +warlords +warm +warmed +warmer +warmer's +warmers +warmest +warmhearted +warming +warmly +warmonger +warmonger's +warmongering +warmongering's +warmongers +warms +warmth +warmth's +warn +warned +warning +warning's +warnings +warns +warp +warp's +warpath +warpath's +warpaths +warped +warping +warps +warrant +warrant's +warranted +warrantied +warranties +warranting +warrants +warranty +warranty's +warrantying +warred +warren +warren's +warrens +warring +warrior +warrior's +warriors +wars +warship +warship's +warships +wart +wart's +warthog +warthog's +warthogs +wartier +wartiest +wartime +wartime's +warts +warty +wary +was +wash +wash's +washable +washable's +washables +washbasin +washbasin's +washbasins +washboard +washboard's +washboards +washbowl +washbowl's +washbowls +washcloth +washcloth's +washcloths +washed +washer +washer's +washers +washerwoman +washerwoman's +washerwomen +washes +washing +washing's +washings +washout +washout's +washouts +washroom +washroom's +washrooms +washstand +washstand's +washstands +washtub +washtub's +washtubs +wasn't +wasp +wasp's +waspish +wasps +wassail +wassail's +wassailed +wassailing +wassails +wastage +wastage's +waste +waste's +wastebasket +wastebasket's +wastebaskets +wasted +wasteful +wastefully +wastefulness +wastefulness's +wasteland +wasteland's +wastelands +wastepaper +wastepaper's +waster +waster's +wasters +wastes +wastewater +wasting +wastrel +wastrel's +wastrels +watch +watch's +watchband +watchband's +watchbands +watchdog +watchdog's +watchdogs +watched +watcher +watcher's +watchers +watches +watchful +watchfully +watchfulness +watchfulness's +watching +watchmaker +watchmaker's +watchmakers +watchman +watchman's +watchmen +watchtower +watchtower's +watchtowers +watchword +watchword's +watchwords +water +water's +waterbed +waterbed's +waterbeds +waterboard +waterboard's +waterboarded +waterboarding +waterboarding's +waterboardings +waterboards +watercolor +watercolor's +watercolors +watercourse +watercourse's +watercourses +watercraft +watercraft's +watercress +watercress's +watered +waterfall +waterfall's +waterfalls +waterfowl +waterfowl's +waterfowls +waterfront +waterfront's +waterfronts +waterier +wateriest +watering +waterline +waterline's +waterlines +waterlogged +watermark +watermark's +watermarked +watermarking +watermarks +watermelon +watermelon's +watermelons +waterpower +waterpower's +waterproof +waterproof's +waterproofed +waterproofing +waterproofing's +waterproofs +waters +waters's +watershed +watershed's +watersheds +waterside +waterside's +watersides +waterspout +waterspout's +waterspouts +watertight +waterway +waterway's +waterways +waterworks +waterworks's +watery +watt +watt's +wattage +wattage's +wattle +wattle's +wattled +wattles +wattling +watts +wave +wave's +waved +waveform +wavelength +wavelength's +wavelengths +wavelet +wavelet's +wavelets +waver +waver's +wavered +wavering +wavers +waves +wavier +waviest +waviness +waviness's +waving +wavy +wax +wax's +waxed +waxen +waxes +waxier +waxiest +waxiness +waxiness's +waxing +waxwing +waxwing's +waxwings +waxwork +waxwork's +waxworks +waxy +way +way's +wayfarer +wayfarer's +wayfarers +wayfaring +wayfaring's +wayfarings +waylaid +waylay +waylaying +waylays +ways +wayside +wayside's +waysides +wayward +waywardly +waywardness +waywardness's +we +we'd +we'll +we're +we've +weak +weaken +weakened +weakening +weakens +weaker +weakest +weakfish +weakfish's +weakfishes +weakling +weakling's +weaklings +weakly +weakness +weakness's +weaknesses +weal +weal's +weals +wealth +wealth's +wealthier +wealthiest +wealthiness +wealthiness's +wealthy +wean +weaned +weaning +weans +weapon +weapon's +weaponless +weaponry +weaponry's +weapons +wear +wear's +wearable +wearer +wearer's +wearers +wearied +wearier +wearies +weariest +wearily +weariness +weariness's +wearing +wearisome +wears +weary +wearying +weasel +weasel's +weaseled +weaseling +weasels +weather +weather's +weathercock +weathercock's +weathercocks +weathered +weathering +weathering's +weatherize +weatherized +weatherizes +weatherizing +weatherman +weatherman's +weathermen +weatherproof +weatherproofed +weatherproofing +weatherproofs +weathers +weave +weave's +weaved +weaver +weaver's +weavers +weaves +weaving +web +web's +webbed +webbing +webbing's +webcam +webcam's +webcams +webcast +webcast's +webcasting +webcasts +webinar +webinar's +webinars +webisode +webisode's +webisodes +webmaster +webmaster's +webmasters +webmistress +webmistress's +webmistresses +webs +website +website's +websites +wed +wedded +wedder +wedding +wedding's +weddings +wedge +wedge's +wedged +wedges +wedging +wedlock +wedlock's +weds +wee +wee's +weed +weed's +weeded +weeder +weeder's +weeders +weedier +weediest +weeding +weeds +weedy +weeing +week +week's +weekday +weekday's +weekdays +weekend +weekend's +weekended +weekending +weekends +weeklies +weekly +weekly's +weeknight +weeknight's +weeknights +weeks +weep +weep's +weeper +weeper's +weepers +weepier +weepies +weepiest +weeping +weepings +weeps +weepy +weepy's +weer +wees +weest +weevil +weevil's +weevils +weft +weft's +wefts +weigh +weigh's +weighed +weighing +weighs +weight +weight's +weighted +weightier +weightiest +weightiness +weightiness's +weighting +weightless +weightlessness +weightlessness's +weightlifter +weightlifter's +weightlifters +weightlifting +weightlifting's +weights +weighty +weir +weir's +weird +weirder +weirdest +weirdly +weirdness +weirdness's +weirdo +weirdo's +weirdos +weirs +welch +welched +welches +welching +welcome +welcome's +welcomed +welcomes +welcoming +weld +weld's +welded +welder +welder's +welders +welding +welds +welfare +welfare's +welkin +welkin's +well +well's +welled +welling +wellington +wells +wellspring +wellspring's +wellsprings +welsh +welshed +welshes +welshing +welt +welt's +welted +welter +welter's +weltered +weltering +welters +welterweight +welterweight's +welterweights +welting +welts +wen +wen's +wench +wench's +wenches +wend +wended +wending +wends +wens +went +wept +were +weren't +werewolf +werewolf's +werewolves +west +west's +westbound +westerlies +westerly +westerly's +western +western's +westerner +westerner's +westerners +westernize +westernized +westernizes +westernizing +westernmost +westerns +westward +westwards +wet +wet's +wetback +wetback's +wetbacks +wetland +wetland's +wetlands +wetly +wetness +wetness's +wets +wetted +wetter +wettest +wetting +whack +whack's +whacked +whackier +whackiest +whacking +whacks +whacky +whale +whale's +whalebone +whalebone's +whaled +whaler +whaler's +whalers +whales +whaling +whaling's +wham +wham's +whammed +whammies +whamming +whammy +whammy's +whams +wharf +wharf's +wharfs +wharves +what +what's +whatchamacallit +whatchamacallit's +whatchamacallits +whatever +whatnot +whatnot's +whats +whatsoever +wheal +wheal's +wheals +wheat +wheat's +wheaten +wheedle +wheedled +wheedles +wheedling +wheel +wheel's +wheelbarrow +wheelbarrow's +wheelbarrows +wheelbase +wheelbase's +wheelbases +wheelchair +wheelchair's +wheelchairs +wheeled +wheeler +wheeling +wheels +wheelwright +wheelwright's +wheelwrights +wheeze +wheeze's +wheezed +wheezes +wheezier +wheeziest +wheezing +wheezy +whelk +whelk's +whelked +whelks +whelp +whelp's +whelped +whelping +whelps +when +when's +whence +whenever +whens +where +where's +whereabouts +whereabouts's +whereas +whereat +whereby +wherefore +wherefore's +wherefores +wherein +whereof +whereon +wheres +wheresoever +whereupon +wherever +wherewithal +wherewithal's +whet +whether +whets +whetstone +whetstone's +whetstones +whetted +whetting +whew +whey +whey's +which +whichever +whiff +whiff's +whiffed +whiffing +whiffs +while +while's +whiled +whiles +whiling +whilst +whim +whim's +whimper +whimper's +whimpered +whimpering +whimpers +whims +whimsey +whimsey's +whimseys +whimsical +whimsicality +whimsicality's +whimsically +whimsies +whimsy +whimsy's +whine +whine's +whined +whiner +whiner's +whiners +whines +whinier +whiniest +whining +whinnied +whinnies +whinny +whinny's +whinnying +whiny +whip +whip's +whipcord +whipcord's +whiplash +whiplash's +whiplashes +whipped +whippersnapper +whippersnapper's +whippersnappers +whippet +whippet's +whippets +whipping +whipping's +whippings +whippoorwill +whippoorwill's +whippoorwills +whips +whir +whir's +whirl +whirl's +whirled +whirligig +whirligig's +whirligigs +whirling +whirlpool +whirlpool's +whirlpools +whirls +whirlwind +whirlwind's +whirlwinds +whirr +whirr's +whirred +whirring +whirrs +whirs +whisk +whisk's +whisked +whisker +whisker's +whiskered +whiskers +whiskey +whiskey's +whiskeys +whiskies +whisking +whisks +whisky +whisky's +whiskys +whisper +whisper's +whispered +whispering +whispers +whist +whist's +whistle +whistle's +whistled +whistler +whistler's +whistlers +whistles +whistling +whit +whit's +white +white's +whitecap +whitecap's +whitecaps +whitefish +whitefish's +whitefishes +whiten +whitened +whitener +whitener's +whiteners +whiteness +whiteness's +whitening +whitens +whiter +whites +whitest +whitewall +whitewall's +whitewalls +whitewash +whitewash's +whitewashed +whitewashes +whitewashing +whither +whiting +whiting's +whitings +whitish +whits +whittle +whittled +whittler +whittler's +whittlers +whittles +whittling +whiz +whiz's +whizz +whizz's +whizzed +whizzes +whizzing +who +who'd +who'll +who're +who's +who've +whoa +whodunit +whodunit's +whodunits +whodunnit +whodunnit's +whodunnits +whodunnits's +whoever +whole +whole's +wholehearted +wholeheartedly +wholeness +wholeness's +wholes +wholesale +wholesale's +wholesaled +wholesaler +wholesaler's +wholesalers +wholesales +wholesaling +wholesome +wholesomeness +wholesomeness's +wholly +whom +whomever +whomsoever +whoop +whoop's +whooped +whoopee +whoopees +whooping +whoops +whoosh +whoosh's +whooshed +whooshes +whooshing +whopper +whopper's +whoppers +whopping +whore +whore's +whorehouse +whorehouse's +whorehouses +whores +whorl +whorl's +whorled +whorls +whose +whosoever +why +why's +whys +wick +wick's +wicked +wickeder +wickedest +wickedly +wickedness +wickedness's +wicker +wicker's +wickers +wickerwork +wickerwork's +wicket +wicket's +wickets +wicks +wide +widely +widen +widened +wideness +wideness's +widening +widens +wider +widescreen +widescreen's +widescreens +widespread +widest +widgeon +widgeon's +widgeons +widow +widow's +widowed +widower +widower's +widowers +widowhood +widowhood's +widowing +widows +width +width's +widths +wield +wielded +wielding +wields +wiener +wiener's +wieners +wife +wife's +wifely +wig +wig's +wigeon +wigeon's +wigeons +wigged +wigging +wiggle +wiggle's +wiggled +wiggler +wiggler's +wigglers +wiggles +wigglier +wiggliest +wiggling +wiggly +wight +wight's +wights +wigs +wigwag +wigwag's +wigwagged +wigwagging +wigwags +wigwam +wigwam's +wigwams +wiki +wiki's +wikis +wild +wild's +wildcat +wildcat's +wildcats +wildcatted +wildcatting +wildebeest +wildebeest's +wildebeests +wilder +wilderness +wilderness's +wildernesses +wildest +wildfire +wildfire's +wildfires +wildflower +wildflower's +wildflowers +wildfowl +wildfowl's +wildfowls +wildlife +wildlife's +wildly +wildness +wildness's +wilds +wile +wile's +wiled +wiles +wilful +wilfully +wilfulness +wilfulness's +wilier +wiliest +wiliness +wiliness's +wiling +will +will's +willed +willful +willfully +willfulness +willfulness's +willies +willies's +willing +willingly +willingness +willingness's +willow +willow's +willows +willowy +willpower +willpower's +wills +wilt +wilt's +wilted +wilting +wilts +wily +wimp +wimp's +wimpier +wimpiest +wimple +wimple's +wimpled +wimples +wimpling +wimps +wimpy +win +win's +wince +wince's +winced +winces +winch +winch's +winched +winches +winching +wincing +wind +wind's +windbag +windbag's +windbags +windbreak +windbreak's +windbreaker +windbreaker's +windbreakers +windbreaks +windburn +windburn's +winded +windfall +windfall's +windfalls +windier +windiest +windiness +windiness's +winding +winding's +windjammer +windjammer's +windjammers +windlass +windlass's +windlasses +windmill +windmill's +windmilled +windmilling +windmills +window +window's +windowed +windowing +windowpane +windowpane's +windowpanes +windows +windowsill +windowsill's +windowsills +windpipe +windpipe's +windpipes +winds +windscreen +windscreen's +windscreens +windshield +windshield's +windshields +windsock +windsock's +windsocks +windstorm +windstorm's +windstorms +windsurf +windsurfed +windsurfing +windsurfing's +windsurfs +windswept +windup +windup's +windups +windward +windward's +windy +wine +wine's +wined +wineglass +wineglass's +wineglasses +wineries +winery +winery's +wines +wing +wing's +winged +winger +wingers +winging +wingless +wingnut +wingnut's +wingnuts +wings +wingspan +wingspan's +wingspans +wingspread +wingspread's +wingspreads +wingtip +wingtip's +wingtips +wining +wink +wink's +winked +winking +winks +winner +winner's +winners +winning +winning's +winnings +winnow +winnowed +winnowing +winnows +wino +wino's +winos +wins +winsome +winsomely +winsomer +winsomest +winter +winter's +wintered +wintergreen +wintergreen's +winterier +winteriest +wintering +winterize +winterized +winterizes +winterizing +winters +wintertime +wintertime's +wintery +wintrier +wintriest +wintry +wipe +wipe's +wiped +wiper +wiper's +wipers +wipes +wiping +wire +wire's +wired +wireless +wireless's +wirelesses +wires +wiretap +wiretap's +wiretapped +wiretapping +wiretaps +wirier +wiriest +wiriness +wiriness's +wiring +wiring's +wiry +wisdom +wisdom's +wise +wise's +wiseacre +wiseacre's +wiseacres +wisecrack +wisecrack's +wisecracked +wisecracking +wisecracks +wisely +wiser +wises +wisest +wish +wish's +wishbone +wishbone's +wishbones +wished +wisher +wisher's +wishers +wishes +wishful +wishfully +wishing +wishlist's +wisp +wisp's +wispier +wispiest +wisps +wispy +wist +wistaria +wistaria's +wistarias +wisteria +wisteria's +wisterias +wistful +wistfully +wistfulness +wistfulness's +wit +wit's +witch +witch's +witchcraft +witchcraft's +witched +witchery +witchery's +witches +witching +with +withal +withdraw +withdrawal +withdrawal's +withdrawals +withdrawing +withdrawn +withdraws +withdrew +wither +withered +withering +withers +withers's +withheld +withhold +withholding +withholding's +withholds +within +within's +without +withstand +withstanding +withstands +withstood +witless +witlessly +witness +witness's +witnessed +witnesses +witnessing +wits +wits's +witticism +witticism's +witticisms +wittier +wittiest +wittily +wittiness +wittiness's +witting +wittingly +witty +wive +wives +wiz +wiz's +wizard +wizard's +wizardry +wizardry's +wizards +wizened +wizes +wizzes +wobble +wobble's +wobbled +wobbles +wobblier +wobbliest +wobbling +wobbly +woe +woe's +woebegone +woeful +woefuller +woefullest +woefully +woes +wok +wok's +woke +woken +woks +wolf +wolf's +wolfed +wolfhound +wolfhound's +wolfhounds +wolfing +wolfish +wolfram +wolfram's +wolfs +wolverine +wolverine's +wolverines +wolves +woman +woman's +womanhood +womanhood's +womanish +womanize +womanized +womanizer +womanizer's +womanizers +womanizes +womanizing +womankind +womankind's +womanlier +womanliest +womanlike +womanlike's +womanliness +womanliness's +womanly +womb +womb's +wombat +wombat's +wombats +wombs +women +women's +womenfolk +womenfolk's +womenfolks +womenfolks's +won +won's +won't +wonder +wonder's +wondered +wonderful +wonderfully +wondering +wonderland +wonderland's +wonderlands +wonderment +wonderment's +wonders +wondrous +wondrously +wont +wont's +wonted +woo +wood +wood's +woodbine +woodbine's +woodcarving +woodcarving's +woodcarvings +woodchuck +woodchuck's +woodchucks +woodcock +woodcock's +woodcocks +woodcraft +woodcraft's +woodcut +woodcut's +woodcuts +woodcutter +woodcutter's +woodcutters +woodcutting +woodcutting's +wooded +wooden +woodener +woodenest +woodenly +woodenness +woodenness's +woodier +woodies +woodiest +woodiness +woodiness's +wooding +woodland +woodland's +woodlands +woodman +woodman's +woodmen +woodpecker +woodpecker's +woodpeckers +woodpile +woodpile's +woodpiles +woods +woods's +woodshed +woodshed's +woodsheds +woodsier +woodsiest +woodsman +woodsman's +woodsmen +woodsy +woodwind +woodwind's +woodwinds +woodwork +woodwork's +woodworking +woodworking's +woodworm +woody +woody's +wooed +wooer +wooer's +wooers +woof +woof's +woofed +woofer +woofer's +woofers +woofing +woofs +wooing +wool +wool's +woolen +woolen's +woolens +woolgathering +woolgathering's +woolie +woolie's +woolier +woolies +wooliest +woollier +woollies +woolliest +woolliness +woolliness's +woolly +woolly's +wooly +wooly's +woos +woozier +wooziest +wooziness +wooziness's +woozy +word +word's +worded +wordier +wordiest +wordiness +wordiness's +wording +wording's +wordings +wordplay +wordplay's +words +wordy +wore +work +work's +workable +workaday +workaholic +workaholic's +workaholics +workbench +workbench's +workbenches +workbook +workbook's +workbooks +workday +workday's +workdays +worked +worker +worker's +workers +workfare +workfare's +workflow +workflow's +workflows +workforce +workforce's +workhorse +workhorse's +workhorses +workhouse +workhouse's +workhouses +working +working's +workingman +workingman's +workingmen +workings +workings's +workload +workload's +workloads +workman +workman's +workmanlike +workmanship +workmanship's +workmen +workout +workout's +workouts +workplace +workplace's +workplaces +works +works's +worksheet +worksheet's +worksheets +workshop +workshop's +workshops +workstation +workstation's +workstations +workweek +workweek's +workweeks +world +world's +worldlier +worldliest +worldliness +worldliness's +worldly +worlds +worldwide +worm +worm's +wormed +wormhole +wormhole's +wormholes +wormier +wormiest +worming +worms +wormwood +wormwood's +wormy +worn +worried +worrier +worrier's +worriers +worries +worrisome +worry +worry's +worrying +worryings +worrywart +worrywart's +worrywarts +worse +worse's +worsen +worsened +worsening +worsens +worship +worship's +worshiped +worshiper +worshiper's +worshipers +worshipful +worshiping +worshipped +worshipper +worshipper's +worshippers +worshipping +worships +worst +worst's +worsted +worsted's +worsting +worsts +worth +worth's +worthier +worthies +worthiest +worthily +worthiness +worthiness's +worthless +worthlessness +worthlessness's +worthwhile +worthy +worthy's +wot +would +would've +wouldn't +woulds +wound +wound's +wounded +wounder +wounding +wounds +wove +woven +wow +wow's +wowed +wowing +wows +wrack +wrack's +wraith +wraith's +wraiths +wrangle +wrangle's +wrangled +wrangler +wrangler's +wranglers +wrangles +wrangling +wrap +wrap's +wraparound +wraparound's +wraparounds +wrapped +wrapper +wrapper's +wrappers +wrapping +wrapping's +wrappings +wraps +wrapt +wrath +wrath's +wrathful +wrathfully +wreak +wreaked +wreaking +wreaks +wreath +wreath's +wreathe +wreathed +wreathes +wreathing +wreaths +wreck +wreck's +wreckage +wreckage's +wrecked +wrecker +wrecker's +wreckers +wrecking +wrecks +wren +wren's +wrench +wrench's +wrenched +wrenches +wrenching +wrens +wrest +wrest's +wrested +wresting +wrestle +wrestle's +wrestled +wrestler +wrestler's +wrestlers +wrestles +wrestling +wrestling's +wrests +wretch +wretch's +wretched +wretcheder +wretchedest +wretchedly +wretchedness +wretchedness's +wretches +wrier +wriest +wriggle +wriggle's +wriggled +wriggler +wriggler's +wrigglers +wriggles +wriggling +wriggly +wright +wring +wring's +wringer +wringer's +wringers +wringing +wrings +wrinkle +wrinkle's +wrinkled +wrinkles +wrinklier +wrinklies +wrinkliest +wrinkling +wrinkly +wrinkly's +wrist +wrist's +wristband +wristband's +wristbands +wrists +wristwatch +wristwatch's +wristwatches +writ +writ's +writable +write +writer +writer's +writers +writes +writhe +writhe's +writhed +writhes +writhing +writing +writing's +writings +writs +written +wrong +wrong's +wrongdoer +wrongdoer's +wrongdoers +wrongdoing +wrongdoing's +wrongdoings +wronged +wronger +wrongest +wrongful +wrongfully +wrongfulness +wrongfulness's +wrongheaded +wrongheadedly +wrongheadedness +wrongheadedness's +wronging +wrongly +wrongness +wrongness's +wrongs +wrote +wroth +wrought +wrung +wry +wryer +wryest +wryly +wryness +wryness's +wuss +wuss's +wusses +x +xenon +xenon's +xenophobia +xenophobia's +xenophobic +xerographic +xerography +xerography's +xylem +xylem's +xylophone +xylophone's +xylophones +xylophonist +xylophonist's +xylophonists +y +y'all +yacht +yacht's +yachted +yachting +yachting's +yachts +yachtsman +yachtsman's +yachtsmen +yack +yack's +yacked +yacking +yacks +yahoo +yahoo's +yahoos +yak +yak's +yakked +yakking +yaks +yam +yam's +yammer +yammer's +yammered +yammering +yammers +yams +yank +yank's +yanked +yanking +yanks +yap +yap's +yapped +yapping +yaps +yard +yard's +yardage +yardage's +yardages +yardarm +yardarm's +yardarms +yards +yardstick +yardstick's +yardsticks +yarmulke +yarmulke's +yarmulkes +yarn +yarn's +yarns +yaw +yaw's +yawed +yawing +yawl +yawl's +yawls +yawn +yawn's +yawned +yawning +yawns +yaws +yaws's +ye +yea +yea's +yeah +yeah's +yeahs +year +year's +yearbook +yearbook's +yearbooks +yearlies +yearling +yearling's +yearlings +yearly +yearly's +yearn +yearned +yearning +yearning's +yearnings +yearns +years +yeas +yeast +yeast's +yeastier +yeastiest +yeasts +yeasty +yell +yell's +yelled +yelling +yellow +yellow's +yellowed +yellower +yellowest +yellowing +yellowish +yellows +yells +yelp +yelp's +yelped +yelping +yelps +yen +yen's +yens +yeoman +yeoman's +yeomen +yep +yep's +yeps +yes +yes's +yeses +yeshiva +yeshiva's +yeshivah +yeshivah's +yeshivahs +yeshivas +yeshivot +yeshivoth +yessed +yessing +yest +yesterday +yesterday's +yesterdays +yesteryear +yesteryear's +yet +yeti +yew +yew's +yews +yield +yield's +yielded +yielding +yieldings +yields +yip +yip's +yipped +yippee +yipping +yips +yo +yock +yock's +yocks +yodel +yodel's +yodeled +yodeler +yodeler's +yodelers +yodeling +yodelled +yodeller +yodeller's +yodellers +yodelling +yodels +yoga +yoga's +yoghourt +yoghourt's +yoghourts +yoghurt +yoghurt's +yoghurts +yogi +yogi's +yogin +yogin's +yogins +yogis +yogurt +yogurt's +yogurts +yoke +yoke's +yoked +yokel +yokel's +yokels +yokes +yoking +yolk +yolk's +yolks +yon +yonder +yore +yore's +you +you'd +you'll +you're +you's +you've +young +young's +younger +youngest +youngish +youngster +youngster's +youngsters +your +yours +yourself +yourselves +yous +youth +youth's +youthful +youthfully +youthfulness +youthfulness's +youths +yowl +yowl's +yowled +yowling +yowls +yttrium +yttrium's +yucca +yucca's +yuccas +yuck +yuck's +yucked +yuckier +yuckiest +yucking +yucks +yucky +yuk +yuk's +yukked +yukking +yuks +yule +yule's +yuletide +yuletide's +yum +yummier +yummiest +yummy +yup +yup's +yuppie +yuppie's +yuppies +yuppy +yuppy's +yups +z +zanier +zanies +zaniest +zaniness +zaniness's +zany +zany's +zap +zap's +zapped +zapper +zapper's +zappers +zapping +zaps +zeal +zeal's +zealot +zealot's +zealots +zealous +zealously +zealousness +zealousness's +zebra +zebra's +zebras +zebu +zebu's +zebus +zed +zed's +zeds +zenith +zenith's +zeniths +zephyr +zephyr's +zephyrs +zeppelin +zeppelin's +zeppelins +zero +zero's +zeroed +zeroes +zeroing +zeros +zest +zest's +zestful +zestfully +zests +zeta +zigzag +zigzag's +zigzagged +zigzagging +zigzags +zilch +zilch's +zillion +zillion's +zillions +zinc +zinc's +zinced +zincing +zincked +zincking +zincs +zing +zing's +zinged +zinger +zinger's +zingers +zinging +zings +zinnia +zinnia's +zinnias +zip +zip's +zipped +zipper +zipper's +zippered +zippering +zippers +zippier +zippiest +zipping +zippy +zips +zircon +zircon's +zirconium +zirconium's +zircons +zit +zit's +zither +zither's +zithers +zits +zodiac +zodiac's +zodiacal +zodiacs +zombi +zombi's +zombie +zombie's +zombies +zombis +zonal +zone +zone's +zoned +zones +zoning +zonked +zoo +zoo's +zoological +zoologist +zoologist's +zoologists +zoology +zoology's +zoom +zoom's +zoomed +zooming +zooms +zoos +zucchini +zucchini's +zucchinis +zwieback +zwieback's +zygote +zygote's +zygotes +Ångström +Ångström's +éclair +éclair's +éclairs +éclat +éclat's +élan +élan's +émigré +émigré's +émigrés +épée +épée's +épées +étude +étude's +études diff --git a/marginalia_nu/src/main/resources/dictionary/latin-1000 b/marginalia_nu/src/main/resources/dictionary/latin-1000 new file mode 100644 index 00000000..b59ded2f --- /dev/null +++ b/marginalia_nu/src/main/resources/dictionary/latin-1000 @@ -0,0 +1,984 @@ +et +in +est +non +ut +cum +si +ad +quod +qui +sed +quae +ex +quam +esse +de +aut +hoc +nec +sunt +etiam +se +enim +quid +ab +per +sit +atque +id +autem +quo +uel +me +ne +te +ac +nam +tamen +eius +haec +mihi +ita +iam +neque +quidem +eo +quoque +ea +pro +tibi +quia +ego +nihil +eum +modo +nunc +sic +libro +an +quem +quibus +inter +qua +esset +causa +erat +nisi +hic +potest +uero +tum +quis +ipse +fuit +tu +ille +ante +sine +res +his +omnia +idem +ubi +sibi +illa +post +rem +ei +tam +apud +tantum +magis +at +erit +deinde +quos +cui +omnes +is +re +contra +nos +cuius +omnibus +minus +quasi +ergo +eam +igitur +sub +rei +posse +dum +eorum +sua +inquit +itaque +sint +primum +ipsa +habet +suo +illud +item +eos +illi +siue +satis +nobis +parte +hanc +ait +rerum +semper +propter +tempore +loco +possit +unde +rebus +fuerit +inde +omnis +omnium +quoniam +fieri +eadem +nomine +alia +maxime +hunc +alii +hac +pater +quas +facere +saepe +uerum +aliquid +suis +bene +ipsum +die +mea +multa +nomen +solum +uidetur +illum +unum +fuisse +nulla +natura +primo +simul +ob +una +dies +postea +quidam +factum +habere +tempus +senatus +iis +uos +tunc +dixit +licet +tua +iure +quantum +dicere +uti +bellum +dicitur +partem +genus +numquam +ideo +locum +ibi +pars +aliud +eodem +quorum +huius +erant +aduersus +filius +sum +nemo +suum +debet +animo +hominum +supra +opus +sui +causam +dicit +hinc +quin +uis +magna +fecit +sicut +plus +illo +ius +edictum +heres +arma +ista +illis +ipsi +fit +suam +huic +uerba +essent +homines +duo +facit +facta +usque +manus +cur +quamuis +quaedam +potius +ipso +omni +dedit +usus +animi +uenit +diem +populi +urbem +castra +romani +genere +manu +haud +prima +bello +uno +summa +forte +ui +aliis +ratio +prius +ratione +posset +adhuc +certe +tamquam +publicae +terra +filium +nostra +publica +bona +circa +es +meo +aqua +caput +uerbis +lege +super +denique +fortuna +illam +uelut +quamquam +possunt +sane +secundum +oportet +filio +hominem +praeter +patris +uobis +recte +modum +armis +belli +suae +pecuniam +tot +uita +necesse +meum +habent +corpus +aliter +caesar +suos +multis +utrum +diu +uitae +dare +hostium +paulus +praeterea +mortem +uix +animum +postquam +alio +omne +statim +totum +milia +dictum +uideri +adeo +intra +fere +uim +tanta +alter +mox +partes +alterum +umquam +patrem +multo +facile +iudicium +iudices +domum +ulpianus +scilicet +tota +magno +locis +quando +cura +locus +tertio +fidem +secundo +cetera +quare +corpore +urbe +eas +iudicio +bonorum +signa +deos +uiri +consul +homo +unus +sententia +legatum +potuit +decem +quicquam +numero +populo +fore +nostri +quisque +melius +dici +namque +tuo +gratia +prope +corporis +uocant +habeat +ceteris +nullum +sese +altera +omnem +puto +cicero +hi +multum +mare +rex +exercitus +aliqua +actio +uir +quattuor +pecunia +male +dicunt +annos +nullo +illius +fuerat +seruus +potestate +respondit +solet +diximus +dicta +iter +hereditatem +alias +decimo +iuris +anno +uirum +nostris +uitam +rationem +hos +dari +iussit +ipsis +ulla +paulo +debere +milites +habuit +ceterum +nondum +romae +procul +ipsius +castris +agere +quinque +quidquid +rursus +huc +alios +tandem +senatu +militum +uino +ora +actionem +consilium +bonis +omnino +legem +longe +generis +praetor +genera +interim +primus +deorum +tuis +publicam +consilio +uideatur +dicam +heredem +donec +maior +sanguine +ore +duobus +sabinum +alius +eis +exercitum +amplius +fructus +temporis +oculos +tuum +urbis +quemadmodum +data +meis +mi +toto +quippe +dixi +testamento +oculis +meus +fuisset +parum +caesaris +corpora +domus +possessionem +etsi +dicimus +hostes +bonum +litteris +imperium +mei +romam +aliquando +fama +nocte +regem +patre +utique +interdum +plures +meam +animus +quaeque +multi +uera +paene +constat +ii +meae +earum +pedes +poterit +consules +liber +ire +mortis +dicendum +terram +quinto +tres +caelo +hominis +populus +cn +extra +fuerunt +mater +tanto +ferre +hostem +diebus +eiusdem +seu +litteras +metu +quotiens +more +sola +morte +uirtute +condicione +fiat +senatum +mari +bella +malo +iste +scire +illos +sententiam +usum +tui +tempora +sumus +animos +lex +tribus +iterum +exercitu +uiro +medio +di +tuae +partibus +terrae +malum +uirtus +iubet +manibus +annis +opera +uxorem +graeci +centum +deus +patres +duas +dicuntur +fide +quarto +magnum +pertinet +nil +has +tuam +nomina +idque +sex +num +solis +quibusdam +domino +naturae +romanis +uideo +semel +alium +regis +placet +quisquam +mala +deum +capite +uires +datur +ultra +suas +usu +uolo +libertatem +aquae +nostrum +dico +plerumque +alterius +immo +unam +populum +longa +aliquis +ira +noua +domi +uia +plura +possum +futurum +leges +iouis +scribit +facto +filia +coepit +plane +ferro +secum +caelum +nostro +utrumque +dominus +solus +erunt +datum +mulier +iulianus +hostis +duos +seruum +legati +illic +unius +protinus +quanto +digestorum +patri +possint +imperio +debeat +bis +scio +aeque +romano +uoce +uelit +solent +haberet +faciunt +mecum +spem +accepit +quaeritur +dolo +oportere +sin +undique +diceret +oppidum +modi +romanus +nescio +quondam +oratio +factus +pretium +hodie +ueluti +plurimum +natus +horum +gloria +equidem +sequitur +poena +interest +sexto +uoluntate +fecerit +fuerint +nullam +periculum +putant +ciuitatis +naturam +equites +ipsam +significat +casus +caeli +annum +pluribus +tecum +spes +similis +fides +domo +pariter +turba +loci +casu +scripsit +gratiam +uideretur +uelim +uiginti +uersus +aquam +uellet +periculo +fundum +media +hominibus +uoluit +talis +poterat +amor +sacra +causis +prouincia +mille +mali +ostendit +uidentur +sis +primis +puer +fata +magni +olim +praestare +subito +septimo +ipsos +facilius +fecisse +italia +salutem +filii +demum +fortasse +qualis +putat +singulis +loca +ciuitate +profecto +aliquo +credo +petit +tulit +auctoritate +scriptum +boni +partim +minime +plebis +pomponius +cuncta +liberos +reliqua +uidit +fortunae +uolunt +ordine +causas +summo +quodam +maxima +multos +arte +causae +uult +suorum +inquam +conuenit +seruo +memoria +saepius +uenire +petere +tela +antea +finem +rege +membra +opere +pectore +frater +potestatem +nulli +etiamsi +malis +miles +ueteres +fiunt +amore +nimis +legiones +temporibus +ecce +placuit +serui +spe +dicat +liberis +actione +hostibus +oratione +potes +praecipue +uiribus +crimen +iuppiter +accipere +uiris +uxor +studio +officio +iuxta +dat +ingens +agros +sensus +paucis +uere +prior +refert +signum +uinum +partis +possent +habebat +eandem +sidera +iamque +par +uerborum +ignis +neminem +beneficium +quarum +uidere +consulem +romanos +totam +uerbum +eundem +patriae +foro +iniuria +maiores +romanum +moenia +auctor +ciuium +testamentum +familias +heredes +aetas +terras +manum +altero +legibus +legis +certum +uitia +forma +totius +dein +summam +noster +profectus +nihilo +antequam +quicquid +uocatur +uiam +utraque +iii +uitium +medium +domini +nimium +dolor +filiam +facies +nostrae +persona +ni +dextra +octauo +posuit +italiam +proelio +aciem +mora +uiros +operis +homini +repente +auro +ultro +titio +etenim +frustra +culpa +liceat +mente +fuga +possumus +maiore +equitum +motus +summum +uelle +imperator +operam +agi +quaesitum +dolore +minor +exemplum +faciat +uale +melle +peruenit +teneri +solo +matrem +terris +caesarem +sponte +tertia +quanta +soli \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/dictionary/swe-1000 b/marginalia_nu/src/main/resources/dictionary/swe-1000 new file mode 100644 index 00000000..1970dfb2 --- /dev/null +++ b/marginalia_nu/src/main/resources/dictionary/swe-1000 @@ -0,0 +1,641 @@ +och +att +det +som +en +på +är +för +av +med +den +till +inte +har +de +han +om +ett +jag +var +men +sig +så +vi +hon +från +man +kan +när +hade +nu +skulle +år +säger +där +också +eller +sin +under +efter +ut +ska +vid +mot +då +här +bara +mycket +upp +över +vara +alla +kommer +vad +än +andra +finns +får +in +sedan +du +få +ha +hur +blir +två +vill +hans +många +måste +något +mer +detta +utan +sina +går +allt +blev +fick +mig +honom +dem +skall +nya +bli +någon +mellan +även +några +första +varit +kronor +sitt +genom +ta +kom +dag +fram +Sverige +kunde +stora +hela +svenska +procent +ju +göra +ingen +sa +bra +tre +gör +kanske +oss +själv +bland +annat +gick +redan +se +inom +gå +aldrig +del +väl +åt +henne +kunna +helt +samma +denna +enligt +fått +olika +stor +tid +vet +lite +gång +både +sätt +ser +miljoner +hennes +därför +tidigare +dock +tror +ur +min +dessa +just +ner +flera +varje +hos +gjorde +tog +gäller +barn +tar +komma +Stockholm +igen +står +såg +lika +mest +sade +ändå +ännu +ja +tycker +tillbaka +bättre +innan +nog +ligger +deras +rätt +ni +människor +alltid +fall +ger +blivit +ge +fyra +ny +gamla +annan +eftersom +trots +kvar +vilket +säga +tiden +gjort +vår +ville +regeringen +samtidigt +längre +dessutom +större +bort +nej +nästan +per +mindre +först +inget +fanns +inför +pengar +fem +runt +själva +länge +senare +stället +fast +mitt +Göteborg +ofta +senaste +medan +exempel +före +började +varför +enda +svårt +egen +nästa +visar +tillsammans +liv +sista +hem +stort +stod +året +alltså +bakom +långt +haft +fler +fortfarande +plats +hand +menar +båda +väg +USA +utanför +förra +inga +framför +behöver +dess +dom +nytt +bör +våra +dig +folk +håller +par +tio +handlar +problem +heller +frågan +gått +vidare +borde +början +låg +minst +bästa +anser +liten +Anders +sett +egna +satt +svensk +berättar +kvinnor +bild +klart +känner +gången +samt +lätt +ytterligare +världen +särskilt +mål +kl +sidan +kommit +fråga +riktigt +Lars +innebär +flesta +hålla +största +egentligen +små +personer +Johansson +landet +ibland +sen +sex +arbete +gånger +emot +slut +kring +gav +hjälp +ihop +åren +omkring +varandra +företag +hemma +nära +ens +däremot +sagt +snart +Andersson +börjar +helst +vilka +polisen +Peter +därmed +vem +jobb +direkt +lilla +cirka +visst +ned +livet +liksom +tagit +miljarder +skolan +vissa +fel +politiska +sådan +ganska +sitter +vilken +dagens +beslut +kände +ingenting +lång +ute +förslag +spelar +steg +höll +ord +verkligen +tänker +möjligt +säkert +vägen +tv +meter +faktiskt +gott +snabbt +dagen +dagar +alldeles +all +Göran +ordförande +precis +mej +namn +hus +unga +länder +vårt +tyckte +skriver +talar +sådana +Sveriges +barnen +visste +tredje +mamma +Jan +klara +drygt +frågor +män +hoppas +tala +form +veckan +naturligtvis +kort +amerikanska +stöd +ekonomiska +the +hög +delar +mannen +god +gärna +EU +mina +Stockholms +spela +Europa +slutet +högre +visa +tänka +eget +tänkte +plötsligt +grund +viktigt +visade +alls +pappa +ifrån +stå +års +land +annars +vann +hittills +händer +verkar +kr +långa +bo +endast +krav +Persson +veta +dit +förstås +försöker +mera +ena +betala +enkelt +hör +sju +litet +sej +hårt +brukar +minuter +nämligen +er +låter +musik +slog +roll +huset +Johan +ungefär +gammal +månader +björn +rum +tycks +igenom +heter +övriga +väldigt +kväll +sådant +kommunen +bäst +åtta +bor +Bengt +trodde +börja +internationella +vore +främst +låta +antal +sida +vatten +åtminstone +kunnat +sätta +framtiden +numera +tro +kvinna +vecka +Thomas +äldre +knappast +försöka +goda +lägga +inne +maj +vilja +satte +rad +höga +fortsätter +arbetar +DN +bygga +håll +samhället +ex +arbetet +film +veckor +betyder +timmar +Nilsson +känna +utveckling +saker +sak +Mats +lag +skapa +huvudet +kräver +frågade +ansvar +lever +borta +dels +kallade +tills +& +Stefan +närmare +bok +antalet +skrev +årets +ungdomar +tag +dra +känns +via +new +försökte +drog +foto +använda +hitta +staden +spelade +fortsätta +klockan +totalt +svarade +extra +samband +tur +Tyskland +viss +lämna +staten +högt +höra +ökar +leva +förstår +din +ensam +behövs +köpa +möjlighet +historia +resultat +anställda +möjligheter +världens +slå +lär +stark +död +betydligt +visserligen +alltför +samarbete +lät +far +Ulf +chef +politiker +hit +Lennart +bl +Erik +beror +skäl +ögon +öppna +matchen +ställa +finnas +utbildning +ställer +någonting +försök +området +laget +leder +svar +hel +bilder +intresse +hävdar +arbeta +ökat +förslaget +Carlsson +klar diff --git a/marginalia_nu/src/main/resources/dictionary/word-frequency b/marginalia_nu/src/main/resources/dictionary/word-frequency new file mode 100644 index 00000000..f5f8eda9 --- /dev/null +++ b/marginalia_nu/src/main/resources/dictionary/word-frequency @@ -0,0 +1,1003 @@ +the +of +and +in +to +was +is +for +on +as +with +by +he +that +at +from +his +it +an +were +which +are +this +also +be +had +or +has +first +their +after +its +one +new +but +who +her +not +she +they +have +two +been +other +when +during +all +into +there +time +may +more +school +years +over +only +would +later +most +where +between +some +up +world +city +national +about +such +him +then +made +out +state +three +while +used +university +can +united +under +known +season +many +year +part +became +born +film +these +than +team +no +second +including +states +being +through +before +both +american +south +early +war +history +against +however +family +until +well +since +them +work +life +following +area +people +series +north +name +career +album +music +played +group +district +number +several +high +released +county +de +company +called +will +league +won +four +house +government +each +march +same +game +international +september +january +club +found +june +october +began +located +july +so +west +use +august +now +college +john +station +population +april +public +home +end +november +member +place +general +town +former +december +church +if +age +held +named +system +because +york +took +day +river +around +football +british +line +east +local +any +song +due +along +service +party +best +february +served +did +back +another +based +could +within +received +century +village +built +like +members +building +major +final +show +games +although +include +species +death +band +small +main +left +president +said +published +died +large +last +five +couldn't +what +me +order +st +single +set +third +own +those +education +according +included +long +very +park +still +road +army +division +book +development +among +law +often +french +moved +times +what +community +central +led +english +original +old +son +children +million +different +near +just +top +late +again +water +air +great +center +form +much +research +side +us +art +court +play +down +country +off +even +council +german +street +record +power +established +ii +london +land +cup +having +title +started +support +political +students +award +military +period +came +went +production +white +way +given +island +make +next +role +television +king +region +works +total +championship +using +various +head +office +six +do +player +become +father +list +business +western +produced +director +married +program +association +england +field +worked +election +black +department +joined +announced +created +point +returned +professional +union +written +few +you +young +without +take +described +site +royal +services +radio +together +social +force +northern +per +founded +act +though +society +wrote +further +women +days +lost +continued +design +william +every +version +project +summer +live +men +man +european +we +southern +position +board +india +france +round +railway +open +level +considered +control +opened +run +australia +recorded +important +san +once +video +california +special +win +popular +appeared +match +release +common +battle +areas +hall +event +working +records +james +formed +right +playing +see +average +others +short +similar +teams +elected +george +currently +making +example +awards +construction +story +living +red +originally +debut +race +language +forces +lead +la +signed +developed +modern +appointed +case +addition +police +wife +result +minister +schools +events +america +route +little +lake +canada +himself +songs +current +upon +how +points +rock +present +never +free +science +information +health +training +class +throughout +track +good +media +museum +across +australian +human +census +indian +style +personal +love +germany +available +province +tour +away +eventually +body +despite +eastern +sold +committee +performance +players +features +festival +coach +should +return +taken +sea +seven +centre +followed +designed +performed +official +david +less +gave +months +finished +daughter +process +refer +study +europe +institute +stage +term +range +chief +fire +does +rights +completed +arts +half +remained +largest +mother +character +includes +civil +private +light +leading +reported +network +help +usually +seen +groups +studies +featured +federal +full +episode +thus +academy +night +competition +women's +space +get +instead +china +must +robert +japanese +go +washington +front +uk +directed +tournament +my +thomas +news +books +brother +involved +campaign +independent +either +model +countries +awarded +able +japan +sports +charles +gold +section +capital +kingdom +close +middle +added +fourth +sent +movement +eight +studio +previous +provided +conference +above +soon +today +grand +magazine +canadian +replaced +aircraft +change +films +ten +medical +organization +bank +historic +coast +killed +management +degree +rather +industry +russian +professor +chinese +action +car +senior +systems +green +bridge +technology +almost +shows +big +lower +week +success +writing +base +data +families +post +least +market +primary +female +reached +beginning +valley +ground +type +stated +tv +operations +attack +hospital +saw +approximately +paul +culture +republic +size +previously +decided +introduced +hill +buildings +championships +provide +native +successful +outside +parts +via +theatre +placed +behind +bay +sometimes +los +prior +whose +natural +active +future +scored +italian +africa +spanish +attended +put +listed +brought +regional +structure +units +michael +possible +henry +municipality +higher +start +collection +regular +star +results +square +interest +leader +economic +especially +contract +too +trade +texas +goal +below +winning +officer +foreign +generally +operation +runs +medal +changed +taking +novel +staff +significant +real +standard +far +limited +traditional +african +come +initially +itself +location +commission +roman +me +artist +christian +plays +money +parliament +food +hit +governor +low +defeated +energy +student +strong +towards +notable +child +assembly +owned +catholic +course +commercial +ship +foundation +channel +allowed +represented +property +places +navy +unit +ended +annual +command +paris +km +library +companies +whom +met +ever +activities +spent +plan +numerous +blue +earlier +means +highway +dr +required +musical +additional +practice +bill +noted +mountain +airport +ireland +plant +security +income +issues +associated +manager +artists +related +access +brown +running +peter +individual +richard +older +victory +opening +programs +past +report +sound +press +woman +finally +find +background +policy +our +youth +financial +date +executive +launched +soviet +administration +historical +closed +here +mark +captain +basketball +lived +ran +better +edition +famous +contains +chicago +subsequently +move +selected +already +legal +rural +religious +studied +entered +cultural +lines +person +test +appearance +complete +increased +rest +men's +zealand +secretary +complex +seat +changes +matches +majority +room +loss +terms +review +empire +mission +virginia +angeles +olympics +italy +highest +stadium +becoming +goals +starting +wide +characters +writer +particularly +fact +mostly +mexico +thought +hours +stone +retired +recording +going +give +feature +cross +smith +author +operated +sir +recent +status +chart +theory +greek +islands +caused +entire +got +remains +nine +engine +source +genus +forced +issue +singles +evidence +meeting +congress +port +variety +pennsylvania +forest +passed +lord +uses +particular +key +supported +word +create +relationship +overall +hand +democratic +certain +castle +biography +nature +mary +names +fort +parish +decision +serving +score +cover +wales +singer +need +material +shown +florida +upper +referred +larger +marriage +length +leaving +weeks +movie +raised +rate +justice +fall +always +minutes +junior +competed +stations +turn +irish +temple +cases +era +individuals +township +claimed +friends +van \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/fonts/LM-regular.ttf b/marginalia_nu/src/main/resources/fonts/LM-regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6b4f6b8a250401ca8af4d12dd6277ec3151745ca GIT binary patch literal 150124 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�Cv-G`xDidpv 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 Ju)ADy{~xO_x*Y%j literal 0 HcmV?d00001 diff --git a/marginalia_nu/src/main/resources/fonts/STIXTwoMath-Regular.ttf b/marginalia_nu/src/main/resources/fonts/STIXTwoMath-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a4705fc7273cefa3f07108285e68755c81fd442c GIT binary patch literal 1518116 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︶MvA%`Rw@LRUv#2O-y_e1+qS+!lDEq(!%0Da?`6x%Ef0!Rvwey21 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

    6s#`)7=;6)ODX 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

    W#sUm+}gy6;5m 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

    ?*oOXq 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 (Wv{tf8KT)rr=okN{lF1bQ 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 z%0A3IjPT*+;f&3XFpnV3k?J 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 S00T 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 + + + + Website Explorer + + + + +

      Website Explorer

      +
      +

      Instructions

      +

      Press the thumbnail to visit the website.

      +

      Press ➡️ to view the next website.

      +

      Press 🤩 to look for websites similar to the website you are seeing.

      +

      Press 🔀 to return to the default flavor of websites.

      +

      Cookie Consent

      +

      + The game uses a session cookie to keep track of which websites you have been shown so that + you do not see the same websites too repeatedly, and which websites you would like to see more of. +

      +

      + Consent To The Cookie And Begin +

      +

      About

      +

      + These websites are not manually curated. Most of them are clean, but if you do happen to see something particularly + objectionable, please let me know by sending me an email. kontakt@marginalia.nu +

      +

      + A less principled person would probably have plastered the page in ads, as it's a game basically revolving around + refreshing the same page over and over. Instead I invite you to consider supporting me + if you enjoy game. +

      + + diff --git a/marginalia_nu/src/main/resources/static/dating/robots.txt b/marginalia_nu/src/main/resources/static/dating/robots.txt new file mode 100644 index 00000000..5199c74f --- /dev/null +++ b/marginalia_nu/src/main/resources/static/dating/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Disallow: /init +Disallow: /random +Disallow: /similar diff --git a/marginalia_nu/src/main/resources/static/edge/about.html b/marginalia_nu/src/main/resources/static/edge/about.html new file mode 100644 index 00000000..dcc0b95e --- /dev/null +++ b/marginalia_nu/src/main/resources/static/edge/about.html @@ -0,0 +1,23 @@ + + + + + Marginalia Search - About + + + + + +
      + +
      +
      +

      + This page has been moved to the memex. +

      +
      + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/static/edge/changelog.html b/marginalia_nu/src/main/resources/static/edge/changelog.html new file mode 100644 index 00000000..37d3d6b7 --- /dev/null +++ b/marginalia_nu/src/main/resources/static/edge/changelog.html @@ -0,0 +1,23 @@ + + + + + Marginalia Search - Change Log + + + + + +
      + +
      +
      +

      + This page has been moved to the memex. +

      +
      + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/static/edge/crawler-ips.txt b/marginalia_nu/src/main/resources/static/edge/crawler-ips.txt new file mode 100644 index 00000000..dee018f1 --- /dev/null +++ b/marginalia_nu/src/main/resources/static/edge/crawler-ips.txt @@ -0,0 +1 @@ +81.170.128.52 diff --git a/marginalia_nu/src/main/resources/static/edge/error.html b/marginalia_nu/src/main/resources/static/edge/error.html new file mode 100644 index 00000000..7192bf07 --- /dev/null +++ b/marginalia_nu/src/main/resources/static/edge/error.html @@ -0,0 +1,23 @@ + + + + + Error + + + + + +
      + +
      +
      +

      An error has occurred!

      +

      + Something went wrong while processing your query. Please try again later. +

      +
      + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/static/edge/favicon.ico b/marginalia_nu/src/main/resources/static/edge/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a1136a7f6ef20e65517dcc7f46705e46e7fff3a2 GIT binary patch literal 1211 zcmV;s1VsCZP)EX>4Tx04R}tkv&MmKpe$iQ>7x6f>sc5$WWauh>ALD6^c+H)C#RSm|Xe=O&XFG z7e~Rh;NZt%)xpJCR|i)?5c~jfb8}L3krMxx6k5c1aNLh~_a1le0HI!Dn$f_we!cF3PjK&;2=i)U3q-pGZ8*46{PKK|Hlt zF*xrNhgm^ZiO-2gO}ZfQBi9v|-#F(T7IZL1yduQB#x+>PWeLG zWtH<5XRTCa&3p0}2DAFgGS_JiA&x~XL4pVcRTNP|1yNdcQY<8CKjz^dbo>&z6mk{8 z$gzMjG{}x0{11M2Yvm@!-K1a)=zOv5k6|FN3p8rB{e5iPjT6BC3|#3gf4L6Ke3D*k zX^|r!v<+Nbw=`uBxZD8-o($QP9m!8q$mM|dGy0|s(02=TuerT7_i_3Fq^PUJ4RCM> zjN~bM-Q(R|?Y;ebrrF;Qgrah;V>MW400006VoOIv00000008+zyMF)x010qNS#tmY zE+YT{E+YYWr9XB6000McNlirueSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00NUqL_t(o!|j*NZqrZ@g}<2_+i_Z?l)ehE=%OM8u|h)p zJ_iWVP1U{ut<=|G(JHV41c=AzGf<(Xh5Y#X4vV->LR?gmLN`6qY@tJduujTlp z$7MpfWL!0H=?&DXF9!g;_tfKb?5a$xPr~p%Bg-<9B)u30mYPjV8&O#ci?yPXimKc4 z^0Mvh>|E>!jyqj?U0+mQl&`3aqoX6t%(k|+Xf%>}MxX`FhB!F<{*8l!11t=wAE@HY zF!4mWV#Rxi2I9z(#Mi9*vnw^{@VU(|!e+CHSzvF!&E9^ShItZl>LxffoDLgpweH}Y zV`F23-Q8XD-s7D6H!gUy_pU4ri|W7gN^x8vtlzHbFf=E&-P- z$PWM#um2{B#e^VL2{`imYU}Rk!Kz?>)*P6IGXWW@|970S^7Zq_QCrDeK~%*+gyE+~ za2C4V{>6^KB>&5#82UbnYV+m5C=UY0pGPnb?=uh~%LZec&yfQGMJhH^Q$q21UTxhi zbr8q##b}@;PO>xdVdRW)5ehL>x@JZVWUAEU_LU&p`Ya6UL^dGK z6|X{`=Ojsj_e0<3p+^W|9v6g?Naq}BnsU-P!Cc^aFCqy!Y7Vag1e_DRACzi$nqXuQ zbS#Q>HU`pq{rt8QoBEVC8jaaU(Chc*luA`cmibDyhKCNpDy*)m + + + + Marginalia Search + + + + + + + + + + + + + +
      + +
      + +
      +
      + +
      + +
      +
      +

      About

      +
      +

      This is an independent DIY search engine that focuses on non-commercial content, and attempts to + show you sites you perhaps weren't aware of in favor of the sort of sites you probably already knew + existed.

      +

      + The software for this search engine is all custom-built, and all crawling and indexing is + done in-house. +

      +
      +
      + Read More +
      +
      + +
      +

      Tips

      +
      +

      + This search engine isn't particularly well equipped to answering queries + posed like questions, instead try to imagine some text that might appear + in the website you are looking for, and search for that.

      +

      + Where this search engine really shines is finding small, old and obscure websites about some + given topic, perhaps + old video games, + a mystery, + theology, + the occult, + knitting, + compter science, + or art. +

      + +
      + +
      + + +
      +

      Updates

      +
      +

      ☛ The web design of the search engine has been completely overhauled. For the most part, this should + result in even smaller page loads, and better accessibility and easier navigation, but it may still + be a bit rough in some browsers, if you do find any bugs or accessibility problems, please let me + know. You can reach me at kontakt@marginalia.nu. +

      +

      ☛ The Random Mode has been overhauled, and is + quite entertaining. I encourage you to give it a spin.

      +

      ☛ A simple public API is now available.

      +
      + +
      + +
      +

      Publicity, Discussion and Events

      +
      +
      +
      You Should Check Out the Indie Web 🎞️
      +
      YouTube, You've Got Kat, 2022-03-15
      +
      + What Google Search Isn't Showing You +
      +
      The New Yorker, 2022-03-10
      +
      + Marginalia Search - Serendipity Engineering +
      +
      MetaFilter, 2022-03-09
      +
      + 🎂 First anniversary! 🎊 +
      +
      + 2022-02-26 +
      +
      + A Search Engine Designed To Surprise You +
      +
      Clive Thompson OneZero, 2021-09-16
      +
      + Hacker News Discussion +
      +
      + 2021-09-16 +
      +
      +
      +
      +
      +
      + +
      + This website complies with the GDPR by not collecting any personal + information, and with the EU Cookie Directive by not using + cookies. More Information. +

      + Reach me at kontakt@marginalia.nu. +

      + + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/static/edge/known-issues.html b/marginalia_nu/src/main/resources/static/edge/known-issues.html new file mode 100644 index 00000000..e14588bc --- /dev/null +++ b/marginalia_nu/src/main/resources/static/edge/known-issues.html @@ -0,0 +1,29 @@ + + + + + Marginalia Search - Known Issues + + + + +
      + +
      +
      +

      Known Issues

      +
        +
      • Non-Latin text becomes horribly garbled in the summary and title description.
      • +
      +

      Mitigated Issues

      +
        +
      • Non-latin characters are stripped from search results (Ålö AB becomes l AB)
      • +
      • The page doesn't look good on mobile
      • +
      • Still a few link farms getting good results
      • +
      +
      + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/static/edge/maintenance.html b/marginalia_nu/src/main/resources/static/edge/maintenance.html new file mode 100644 index 00000000..c8fdc227 --- /dev/null +++ b/marginalia_nu/src/main/resources/static/edge/maintenance.html @@ -0,0 +1,10 @@ + + Marginalia Search - Maintenance Notification + + +

      + Down For Maintenance! +

      +

      The search engine is currently down for maintenance.

      + To The Start Page + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/static/edge/notes.html b/marginalia_nu/src/main/resources/static/edge/notes.html new file mode 100644 index 00000000..e59b3bff --- /dev/null +++ b/marginalia_nu/src/main/resources/static/edge/notes.html @@ -0,0 +1,28 @@ + + + + + Marginalia Search - Notes on Designing a Search Engine + + + + + + + +
      + +
      +
      +

      + This page has been moved to the memex. +

      + + + +
      + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/static/edge/opensearch.xml b/marginalia_nu/src/main/resources/static/edge/opensearch.xml new file mode 100644 index 00000000..89b3efcf --- /dev/null +++ b/marginalia_nu/src/main/resources/static/edge/opensearch.xml @@ -0,0 +1,11 @@ + + + Marginalia + Search Marginalia + UTF-8 + https://search.marginalia.nu/favicon.ico + + https://search.marginalia.nu/ + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/static/edge/robots.txt b/marginalia_nu/src/main/resources/static/edge/robots.txt new file mode 100644 index 00000000..0c0833e9 --- /dev/null +++ b/marginalia_nu/src/main/resources/static/edge/robots.txt @@ -0,0 +1,8 @@ +User-agent: * +Disallow: /browse/ +Disallow: /search/ +Disallow: /search +Disallow: /wiki/ +Disallow: /explore/ +Disallow: /site/ +Disallow: /links/ \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/static/edge/style-new.css b/marginalia_nu/src/main/resources/static/edge/style-new.css new file mode 100644 index 00000000..a10fd4da --- /dev/null +++ b/marginalia_nu/src/main/resources/static/edge/style-new.css @@ -0,0 +1,477 @@ +/* If you need to borrow something from below, that's fine */ + +body { + margin: 0px; + font-size: 12pt; + font-family: sans-serif; + background-color: #f8f8ee; +} + + +.sticker { + ruby-position: under; +} +.sticker rt { + background-color: #ff08; + font-size: 8pt; +} +header { + background-color: #acae89; + color: #fff; + border-bottom: 1px solid #888; +} + +h1, h2 { + font-weight: normal; +} + +header nav a { + text-decoration: none; + color: #000; + + margin-right: 1ch; + padding: .5ch; + display: inline-block; +} + +header nav a:hover, header nav a:focus { + background: #2f4858; + color: #fff !important; +} + +article { + max-width: 160ch; + margin-left: auto; + margin-right: auto; +} + +ul.semantic-results:empty { + display: none; +} +ul.semantic-results { + list-style: none; + padding: 0px; +} +ul.semantic-results li { + padding-left: 0px; +} +ul.semantic-results a { + color: #c00; +} + +.cards { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + padding-left: 1ch; + gap: 1ch; +} + +article > section > p { display: none; } + +.w3m-helper { + display: none; +} + +/* +.card.rs-rank-1,.card.rs-rank-2,.card.rs-rank-3,.card.rs-rank-4 { + border: 1px solid #fe0; + box-sizing: border-box; + box-shadow: 0 0 5px #fe0; +} +*/ + + +.big .card { + min-width: 40ch; +} + +.card .info { + flex-grow: 1; + padding-left: 1ch; + line-height: 1.6; +} +.card { + flex-basis: 20ch; + border: 2px #ccc; + background-color: #fff; + min-width: 30ch; + display: flex; + flex-direction: column; + margin-left: 2px; + margin-right: 2px; + margin-bottom: 8px; + border-left: 1px solid #ecb; + border-top: 1px solid #ecb; + box-shadow: #0008 0 0 5px; +} + +.card h2 a { + display: block !important; +} + +.card img { + width: 30ch; + height: 22.5ch; /* 4:3 aspect ratio, card width = 30, height = 30*3/4 */ +} +.card a:focus img { + filter: sepia(100%); + box-shadow: #444 0px 0px 20px; +} + + +.card a:focus:not(.nofocus) { + background-color: black; + color: white; +} + +.problems ul { + flex-grow: 1; +} + +.card .description { + padding-left: 1ch; + padding-right: 1ch; + flex-grow: 1; + overflow: auto; + -webkit-hyphens: auto; + -moz-hyphens: auto; + -ms-hyphens: auto; + hyphens: auto; +} + +.card h2 { + color: #fff; + background-color: #2f4858; + font-size: medium; + font-family: serif; + padding: .5ch .5ch .5ch .5ch; + margin: 0px 0px 0px 0px; + + font-family: 'Trebuchet', 'Noto Sans', sans-serif; + + text-decoration: none; + border-bottom-right-radius: 2ch; +} + +.card h2 a { + color: #fff; + text-decoration: none; +} + +.problems h2, .info h2 { + background-color: #482f58; +} + +.browse-result { + background-color: #eee; +} +.semantic h2, .browse-result .h2, .definition h2 { + background-color: #48582f; +} + +.search-result .url a { + font-family: monospace; + margin: 1ch; + font-size: 8pt; + background-color: #3F5F6F; + display: block; + margin: 0px; + padding: 1ch; + word-break: break-all; + + color: #fff; +} +.search-result .url a:visited { + color: #FCC; +} +.card .utils { + display: flex; + font-size: 10pt; + + padding: 1ch; + background-color: #eee; +} + +.card .utils > * { + margin-right: 1ch; + margin-left: 1ch; +} + +.card .utils a { + color: #000; +} + +.card.definition { + min-height: 10ch; +} + +.card.definition .description { + padding: 2ch; + font-size: 12pt; +} + +.search-result .meta { + flex-grow: 2; + text-align: right; +} +.search-result .meta > * { + padding-left: 4px; +} + +.search-box { + display: flex; + flex-direction: row; + background-color: #fff; + margin-bottom: 4ch; + box-shadow: #ccc 4px 4px 5px; +} + +.search-box h1 { + color: #fff; + background-color: #2f4858; + font-size: medium; + font-family: serif; + padding: .5ch .5ch .5ch .5ch; + margin: 0px 0px 0px 0px; + border: 2px groove; + max-width: 100%; + font-size: 16pt; +} + +.search-box .input { + flex-grow: 2; + padding: .5ch; +} + + +select { + color: #444; + background-color: #fff; + border: 1px solid #444; + border-radius: 5px; +} + +.search-box .settings { + padding: .5ch; +} + +.search-box .extra { + padding: 1ch; +} + +.search-box .input { + display: flex; + gap: .5ch; +} + +.search-box input[name="query"] { + flex-grow: 2; + padding: .5ch; + font-size: 12pt; + font-family: 'fixedsys', monospace, serif; + + border: 2px inset #000; + background: #fff; + color: #444; +} + +.search-box input[name="query"]:focus { + color: #000; +} + +.search-box input[type="submit"] { + padding: .5ch; + min-width: 5ch; + font-size: 12pt; + font-family: 'fixedsys', monospace, serif; + border: 2px groove #ccc; + background-color: #eee; +} + +.search-box .settings > * { + margin-top: .5ch; +} + +footer { + padding: 2ch; + margin: 16ch 0px 0px 0px; + background-color: #acae89; + height: 20ch; + font-size: 10pt; +} + +a.underline { + text-decoration: underline !important; +} + + +.suggestions { + background-color: #fff; + padding: .5ch; + margin-top: 3.2ch; + position: absolute; + display: inline-block; + width: 300px; + border-left: 1px solid #ccc; + border-top: 1px solid #ccc; + box-shadow: 5px 5px 5px #888; + z-index: 10; +} + +.suggestions a { + display: block; + color: #000; + font-size: 12pt; + font-family: 'fixedsys', monospace, serif; + text-decoration: none; + outline: none; +} + +.suggestions a:focus { + display: block; + background-color: #000; + color: #eee; +} + +@media only screen and (max-device-width: 1024px) { + + .card { + margin-right: 2ch; + } + .card .utils a { + padding: 1ch; + } + + .cards { + justify-content: center; + } + + .suggestions { + display: none; + } +} + +@media only screen and (max-device-width: 800px) { + .search-box { + flex-direction: column; + } + header nav a { + padding: 1ch !important; + } + + .card { + flex-basis: 40ch !important; + max-width: unset; + margin-bottom: 2ch; + margin-right: unset; + } + + .card img { + width: 100%; + height: 100%; + } + + .cards { + padding-left: unset; + padding-right: 5px; + } + + +} + +/* https://www.youtube.com/watch?v=v0nmHymgM7Y */ +@media (prefers-color-scheme: dark) { + a { + color: #acf; + } + .card { + background-color: #222; + color: #aaa; + box-shadow: #0008 0 0 5px; + border: none; + } + .card .utils { + background-color: #000; + color: #fff; + } + .card .utils a { + color: #acf; + } + header { + background-color: #000; + } + footer { + background-color: #000; + color: #fff; + } + body { + background-color: #444; + } + header nav a { + color: #eee; + } + .search-box { + background-color: #222; + box-shadow: #0008 0 0 5px; + } + .search-box h1 { + background-color: unset; + border: none; + } +/* .card.rs-rank-1,.card.rs-rank-2,.card.rs-rank-3,.card.rs-rank-4 { + border: 2px solid #fe05; + box-sizing: border-box; + box-shadow: 0 0 20px #fe03; + }*/ + .search-box input[name="query"] { + background-color: #000 !important; + color: #aaa; + border: 1px solid #888; + } + .search-box input[name="query"]:focus { + color: #eee; + } + .search-box input[type="submit"] { + background-color: #000 !important; + color: #fff; + border: 1px solid #888; + } + .card img { + /* White images turn into an unpleasant death laser if you put them in an otherwise dark page */ + filter: brightness(80%) saturate(140%); + } + input { + color: #fff; + } + .card a:focus img { + filter: brightness(100%); + box-shadow: #ccca 0px 0px 50px; + } + + .card a:focus:not(.nofocus) { + background-color: white; + color: black; + } + + select { + color: #fff; + background-color: #000; + border: 1px solid #ccc; + border-radius: 5px; + } + + .suggestions { + background-color: #000; + border: 1px solid #444; + box-shadow: 2px 2px 5px #000; + } + .suggestions a { + color: #fff; + } + .suggestions a:focus { + background-color: #2f4858; + } + +} \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/static/edge/tts.js b/marginalia_nu/src/main/resources/static/edge/tts.js new file mode 100644 index 00000000..586ae10e --- /dev/null +++ b/marginalia_nu/src/main/resources/static/edge/tts.js @@ -0,0 +1,101 @@ + +if(!window.matchMedia("(pointer: coarse)").matches) { + query = document.getElementById('query'); + query.setAttribute('autocomplete', 'off'); + timer = null; + function fetchSuggestions(e) { + if (timer != null) { + clearTimeout(timer); + } + timer = setTimeout(() => { + req = new XMLHttpRequest(); + + req.onload = rsp => { + items = JSON.parse(req.responseText); + + var old = document.getElementById('suggestions'); + if (old != null) old.remove(); + + if (items.length == 0) return; + + suggestions = document.createElement('div'); + suggestions.setAttribute('id', 'suggestions'); + suggestions.setAttribute('class', 'suggestions'); + + + for (i=0;i { + if (e.key === "ArrowDown") { + if (e.target.nextElementSibling != null) { + e.target.nextElementSibling.focus(); + } + + e.preventDefault() + } + else if (e.key === "ArrowUp") { + if (e.target.previousElementSibling != null) { + e.target.previousElementSibling.focus(); + } + else { + query.focus(); + } + e.preventDefault() + } + else if (e.key === "Escape") { + var suggestions = document.getElementById('suggestions'); + if (suggestions != null) { + suggestions.remove(); + } + query.focus(); + e.preventDefault(); + } + }); + item.addEventListener('keypress', e=> { + if (e.key === "Enter") { + suggestionClickHandler(e); + } + }); + suggestions.appendChild(item); + } + document.getElementsByClassName('input')[0].appendChild(suggestions); + } + + req.open("GET", "https://api.marginalia.nu/suggest/?partial="+encodeURIComponent(query.value)); + req.send(); + }, 250); + } + query.addEventListener("input", fetchSuggestions); + query.addEventListener("click", e=> { var suggestions = document.getElementById('suggestions'); if (suggestions != null) suggestions.remove(); }); + query.addEventListener("keydown", e => { + if (e.key === "ArrowDown") { + var suggestions = document.getElementById('suggestions'); + if (suggestions != null) { + suggestions.childNodes[0].focus(); + } + else { + fetchSuggestions(e); + } + e.preventDefault() + } + else if (e.key === "Escape") { + var suggestions = document.getElementById('suggestions'); + if (suggestions != null) { + suggestions.remove(); + } + query.focus(); + e.preventDefault(); + } + }); +} diff --git a/marginalia_nu/src/main/resources/static/edge/wiki-clean.html b/marginalia_nu/src/main/resources/static/edge/wiki-clean.html new file mode 100644 index 00000000..a90b70b4 --- /dev/null +++ b/marginalia_nu/src/main/resources/static/edge/wiki-clean.html @@ -0,0 +1,76 @@ + + + + + Marginalia Search - About: Easy Read Wikipedia + + + + + +
      + +
      +
      +

      About: High Readability Wikipedia

      +
      +

      + This is a wikipedia client that strips away most links and almost all visual clutter + to provide a more book-like reading experience with fewer distractions. +

      +

      + This is primarily a helpful utility for a search engine focusing on similarly text-oriented + websites. +

      +

      + You are welcome to use it for general article reading as well. This may be useful + if you are on a low bandwidth connection, since the download size is typically reduced + from megabytes to dozens of kilobytes. +

      +

      + What's taken away is all the design elements that your brain would have to filter out + to read the text of the article. It seems as though overburdening this mental process + causes the reader to start scanning the text instead of reading it, which is experienced + as an inability to pay focus. +

      +

      + The cleaning process is not perfect and will occasionally produce strange results, + but significant problems should be relatively rare. +

      + About the Search Engine + +

      Limitations

      +

      This is a "stale" copy of wikipedia, based on an archived copy from January 2021. On the + other hand, we used to abide printed encyclopedias that didn't update at all.

      +

      + Be aware that the cleaning strips away a lot of information, including most references, + footnotes, quality warnings, and so forth. Refer to the original wikipedia article for + that information. +

      +
      +

      Legal

      +
      + The Wikipedia text is available under the the Creative Commons Attribution-ShareAlike 3.0 license, + and so is the wikipedia text forwarded to you through this service. +
      +
      +

      Further reading

      +
      Blom et al. 2017 - Comprehension and navigation of networked hypertexts
      +
      https://onlinelibrary.wiley.com/doi/pdf/10.1111/jcal.12243
      +
      +

      Have something to say?

      +
      +

      Send me an e-mail at kontakt@marginalia.nu. +

      +

      + Don't hesitate to let me know if the website is somehow being a nuisance, + it should respect robots.txt and reduce outgoing requests, but the format + isn't super-standardized, so occasionally it doesn't understand every directive. +

      +
      +
      + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/static/encyclopedia/index.html b/marginalia_nu/src/main/resources/static/encyclopedia/index.html new file mode 100644 index 00000000..1b3f81ed --- /dev/null +++ b/marginalia_nu/src/main/resources/static/encyclopedia/index.html @@ -0,0 +1,24 @@ + + + + + Marginalia Encyclopedia + + + + + + + + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/static/encyclopedia/robots.txt b/marginalia_nu/src/main/resources/static/encyclopedia/robots.txt new file mode 100644 index 00000000..77470cb3 --- /dev/null +++ b/marginalia_nu/src/main/resources/static/encyclopedia/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/static/encyclopedia/wiki-clean.html b/marginalia_nu/src/main/resources/static/encyclopedia/wiki-clean.html new file mode 100644 index 00000000..cd928003 --- /dev/null +++ b/marginalia_nu/src/main/resources/static/encyclopedia/wiki-clean.html @@ -0,0 +1,71 @@ + + + + + Marginalia Search - About: Easy Read Wikipedia + + + + + +
      + +
      +
      +

      About: High Readability Encyclopedia

      +
      +

      + This is an encyclopedia based on Wikipedia's database, that strips away most links and + almost all visual clutter to provide a more book-like reading experience with fewer + distractions. +

      +

      + This is primarily a helpful utility for a search engine focusing on similarly text-oriented + websites. +

      +

      + You are welcome to use it for general article reading as well. This may be useful + if you are on a low bandwidth connection, since the download size is typically reduced + from megabytes to dozens of kilobytes. +

      +

      + What's taken away is all the design elements that your brain would have to filter out + to read the text of the article. It seems as though overburdening this mental process + causes the reader to start scanning the text instead of reading it, which is experienced + as an inability to pay focus. +

      +

      + The cleaning process is not perfect and will occasionally produce strange results, + but significant problems should be relatively rare. +

      + About the Search Engine + +

      Limitations

      +

      This is a "stale" copy of wikipedia, based on an archived copy from January 2021. On the + other hand, we used to abide printed encyclopedias that didn't update at all.

      +

      + Be aware that the cleaning strips away a lot of information, including most references, + footnotes, quality warnings, and so forth. Refer to the original wikipedia article for + that information. +

      +
      +

      Legal

      +
      + The original Wikipedia text is available under the the Creative Commons Attribution-ShareAlike 3.0 license, + and so is the wikipedia text forwarded to you through this service. +
      +
      +

      Further reading

      +
      Blom et al. 2017 - Comprehension and navigation of networked hypertexts
      +
      https://onlinelibrary.wiley.com/doi/pdf/10.1111/jcal.12243
      +
      +

      Have something to say?

      +
      +

      Send me an e-mail at kontakt@marginalia.nu. +

      +
      +
      + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/static/encyclopedia/wiki-start.html b/marginalia_nu/src/main/resources/static/encyclopedia/wiki-start.html new file mode 100644 index 00000000..f0f86947 --- /dev/null +++ b/marginalia_nu/src/main/resources/static/encyclopedia/wiki-start.html @@ -0,0 +1,26 @@ + + + + + Marginalia Search - Easy Read Wikipedia Search + + + + + +
      + +
      +
      +

      Search the Encyclopedia

      + +
      + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/static/fonts/LM-bold-italic.ttf b/marginalia_nu/src/main/resources/static/fonts/LM-bold-italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7b684ee68e2ced91e2a7d9dc21c167d2d3c93db9 GIT binary patch literal 191324 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

      pr$t<*SX%rwXJC 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⁣T((^Cbro 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 literal 0 HcmV?d00001 diff --git a/marginalia_nu/src/main/resources/static/fonts/LM-bold-italic.woff b/marginalia_nu/src/main/resources/static/fonts/LM-bold-italic.woff new file mode 100644 index 0000000000000000000000000000000000000000..d54af37161eaafc4c9bf02ab5ec723597f7ba5fd GIT binary patch literal 66172 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
      =YJ@B3!XaIs9h7+ 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 literal 0 HcmV?d00001 diff --git a/marginalia_nu/src/main/resources/static/fonts/LM-bold.ttf b/marginalia_nu/src/main/resources/static/fonts/LM-bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..17624d45493593ef6d88704df6c3b2ce131010c1 GIT binary patch literal 144572 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 literal 0 HcmV?d00001 diff --git a/marginalia_nu/src/main/resources/static/fonts/LM-bold.woff b/marginalia_nu/src/main/resources/static/fonts/LM-bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..318a3ad2b8d19b77d82d53751e22c90738bc3dfe GIT binary patch literal 52888 zcmZr%V{j%>vwmaS#>Td7C!1tr+t$XmZQHhO+uGQ6ZoXUJ{dfCR*O|t1dd^hMR9E*I zS9vip01)7Zrc(h3KNB!Z=YORCkHo~3Wq_=Kjd0KkSXK zMEEYQAg=@f^#17JHUI!t85RMx263f7LI6M)Bme-C3IKq2lWJaO$Sct^0RTXYKREdx z*0V$?8ZorixBIbw005wBKeOB^i{!4UzT=Os^9Kj`!2=+_zX2eoR)0+Z0Hi?xAk`TF z)KqHDR>*B;tZ($Q-VYAI`onCu=4dr$Kgu6n?2k!5{p%34(gF#<{H<>KW)BPuB!LzpB@q#hwMLD_8*}qh2d`K&lD& zPvHOaVKY@5?Ck>p%Lq(D2ExL^>+yjAFb{#K{?~4j-q#r7m*nSH7D4In=hufL#5~Nn z%HRO>3X2PinA_XiJM9Z9)HU$w>kr+dp1Sn4!I-HxW~{8LraL#IJ%hhbNU-gW*tuuyXsY$-;zK6j#ivhG4WV9fE8i@HiSzf1n5a7Ve5 zR*cL^#7h)s#=K^!)~f`co)BIg>d1@oyn)D{JQpku}Ae;%lwSGz^?3}Aa;7HNxm7S^%r(Yt_dr9(JV6Lmm(E*nJ$ zwp5}IVf=<&Lpa+GyQ-s~R`jRaK2U$mpf3!(HUGxyewCha)p$hMQaUP|fvHzDCK#97 z)gM)HzUF#7JF2~szA{|9{+fM-nJd3w=P7D0+14by0$p%fnM4O)*T;+cHmg{KpW4+n z(_ANpSqE-@RGP(k+y-9dt^>P>1$&s!Yo=iR0`-P}V$9h9TxCU@3VM*RZS;4Xc~8j@ zWnvOnp-}m?4?QvXrg7l_3BxdEVukV8Xn>f9TNnO!#D9T|6rN3Yc7Oayvud!#eDv zuf6tKeI9Yz5%KXl<&%Az*FrTg05hcKx;6q>@nB80+c(k!#QpAxS8^cufS-Ut1g6)o zLA-j;dwCd)@3JRfN|vXmr-C|VeT3w27v2{WGOPYVy%o}nfvq7qG~14b9zl37m~-ZE zX0+uOa+b}kdpyXRg*Ul$k9!t!>Tiv-)7mVRCzU*MA>c=cvht;eV0erX2{#1 z>{w)*kFXy`vDtfVwEo#-*tQv7bF=Al1Bn3LnQ0AwzP05Hc*m{9K8W2RhwzmIZqD<1 z!ACC$VEQ|v&E#^dKH8nQ&IUxBIrfnEf=@X}9+z;^BZ6^PIOL`G#=d{7yR5w0fpfQ|f|9f+{->(tMJ&%| zzpZOoHwwX*?m3DE&~}plVDRBF?8Np2f>}{@m{wO1%y~Wo#_|le_r6oLZJ2!m#BY-d zKk13QJ}Ej$WTu)H(3^uY$)23jx*WKduTMNiZ^Pr;Mw#iy3E7!EEUJjWh%OJXt;_k; zzu>G;in-?@!rwoiwX9h`M4MQXzC*K!9PHt0w5~(mUT$4iTfkoS%)H-PxoN>T)ZS$s zwb;EE0KyERO=0ZW_m0Q6E76?K{GeEm*hSHRVB6&;T4|WR43`p-aaUU{Jm*Nn!f-LF7 zb9pQo(cn7L5pWyKy{OF-L}NAt`sFo#D2-QJrZ7|9ZmB zkf`@X^LP76=Lb3B$jQQleiNHSaOVHXM7Pe@)l~!yGg6mx(VhPIj8$QKyLb$D&Sj zJO<{FrbTJ_54^1)Sxc9r~19STAi^Hd_14&UzdllQ1 z$^25}_Kp z8j=nwo)CXggS@g!C823#-FQi}q6*tG?vFm2`fKtAxu+!G70wmT5zd+83$fQ;_g6PV zj{cXyQ#zhW$KPoy=9$#0BN^tMNyY}*7S3(lgU@;H0nIDgd%62$w}|V?)&!r92bBZb zx6i2SbmSFoHcmD!3p`6T@{Zo=1{as)rn-a+4({RTS`!!FOG8;J1WTiiHJ9byGj%*o zJRknA5Vr~ME$=BElbPC>uvUUOygJU!xo6!kTgU>C?@Y)qQXv6?>Jy*MxfJu>VDI+j z2XPK?Jwjr=5kgool6=G{OjtpheMAIs2;|t%yml!#2=Vx@B)QhuNYp}NxKm8OILx3q zH(0}1zq-*u4kLXY{1MRBxF}q=*a#k(L6XA)KoCX05vn0{wg8kABiaH3o-u_un8~i> zyY4VPi#~n2J?!-^s@y)yx*fRbTr_;< z2$+@#Io%#p^{%$okcS%q!9IxA0C{UPU3!RYsTiEfg5+wrij@d$)gXH1oNJH&K_m7| zP^L=$6}^k)oWQz0Jb@ubd2O~zod4_>msQEDS~SL7XaPwuY@WIk&r}{hmtXsB9G`tc zt2t$sXvB%puRe)Fl;1U$Ds;5H(r$1abqF>40Qh~xrvo_LIjouk+RiBUTVE@(nFDo2 z@#c7Ut6y+NIBRzhn~jmbep0mmBPxIlpcDua$RFqm7#NreSOK^Ycog^w_yYtUgbBnQ zBokyElm%1=v=s~#OaLq#Y#$r|&IGOj?hc+0{sf^25eqR7i3rIB=>VAzc?CrRB>?3B z)d{r^O$Y4>T@AetBL!0l3k%Bu>keBD`vxZm7Y^4AHw*Uw4+@V7PX^Bi9}7Q@06^eC zutO+D#79&>bV4jgyg&j)l0tGvDn~j(#zvMwu0>u(0Y#xh@kQxI1w~~;jYQ2xeL%zg z8DeOOXkX|;=&|UH7$g`%7>O89m>ifYm`0e>n46eiSX5YMSp8U=*qGRS*b3NA*s0j7 zIM_HYIQzH`xXpNwcp7;5cwhL6`1J&k1O^1P1Yd*#gt3JEgbzg6L~=yFL>I(N#BRim zzd(P<{Bro!Pl8EeL()!)Olm}$NqRvhM^;Y`Pwq{AM4>@3Pf0>)McGXGLM29JN0m<1 zNp(TZK^;szN&`(JLK9E3P76*eMH@;xN(Z15p-ZP*re~sep`T{JXYglOWfWjcWZYyT zV=`f?Wx8YLVfJLMWdUd5U~yw@`~}=@CNfv@)7gt@Fntn@N4iV^A8F@3h)T{ z3N#Cx2*L{T2^t6v3qcAw3H1tN3M&dn3y+EbL^wn|MY=^XMeRh##puL}#m>Yn#M>pv zBn%{0CFLYrr6{BdrHQ2@r9Wf@WPD|sWnN^}WD8|4h6(|$}6()ZZ z|MvbpsEDd)rdY4|q{OD=pj4}Lp^UGrq8zEb{0IAw(Vyx+Pb!KkZYt?2%c_W~TB?a^ zz-n4*-RiLF?&^yg&>DIg`LLA$1K_`*KF48#O%!+-<-`{&fLM=*F4&M(gNOs!9vR-(PGr%%aYGh z!?MtF-HOc0$*SJ!&>GsB&)Utp(E7nf!Y0$^%2v#_*mm6x*G|SR+-}ky(q6?r+y2Ob z!okj=(c#Du+EK{S%5l&M)yc-G+!@rF&$-h1%tg&5*%jJV&b8f*%FVzn-fhtB=C8uv zdUs@ZC--F!J`Zn?c8@1dKF@S7aIZkGU2g^NN*`RGcwcN^U*9)BIlpFqZ2x%wrvRIP zvq1O2lOT(r;b4|vx8U{=!jQm_^-zY;(9rELwy?;sk8rE-$q1SV_lVI*cp=knWVs^ zhh(|r$`r^Hla&5cj?~&T%rxJ$<#d{K@ASnC+zhLXrqtAg}`yF#79qaydB z(PDw(;Np*x+>(=0jWW)r z*ndZC$7&~VCu*l#=XzIS*HJfkw@r6xcXRh(_iFb=4@Zw@Pk%3?QVs+wWl5H|}vV975N^dHC>TsH8 z+H5*}I&}tpMqoy7CVS>(mUz~4c5)7UPGPQe?rUCUK6QS3L13YDp??u`F?O+Wad`1? zNp5L>nQ2*IxpW15MQX)+C359uRb(}L4R}p)EqSedZF^m3y?TAoZGE%L)l+LJiG%OC z`?QN)wkAD}<(F*IA8Rztp=9e#kD5_5rAT`qoD2gLNTNVcNGKS6B^fB%L8w5IMxz8$ zPbI>@3!8dMD4?SwJ(5W(+ey~^_ec<*i1DLTz6=>I>>4>0}3k@5NPDQ@$l0>0916>MN z!CY9*zO5lJ5ULeW4#ddr3aFwVJpTd#ym35KLy*UPamy0*!-njQT~zZkaa~vu6Z31fmvK+Yj(gr^kmlBd2Yi=edFNFU{enfz42?SEjpl2;Ux{smQ zjxCF&4e&KB&KKNI)-Z%CS25>mA`Ig$lGlhutChs-u{`*Yx7Lwczkqudzq0> zCcXOa@j?UX{QbOgJ6iX-$n^2UZ0`48Cdw`Tl~c4f-sGFok89+zh0Qjz2P$(W%56SO zC-=={#FMzsjOwE6WdfLv{4e~soeT&gf>5MZAz1$WcMtzqLLy8=8VpF^-@s}w?m*rc zY{vjOGMXbJ;)ql0Q6E$Ave=!HVi_@vBpD-U{@-##gA>qEQuM*7FV@YbV-{0)SABRJ z$6_EJ)jUl3a}=;RY4q(XGM#g1Ok7LI01}H5##OkJ{+XK)hsQwG3e~VgA)?N4+%VH- zn~i4cd-(T!H1Ms#gvpR9!e*e_ZiaYOtf?C zESNq{!USF3S~HPCsMQW|4-fitTWBc_kCsZ!PW#ihZ8f)d*$F=K7^OwH?Cs`@{5H*# zx}ugqRr3H!`fXvrwQ%2t5y6q`UWTX#{P)&NA?W4Gb1gU`)@L`SK!4eg7Ap~rvlvXg zq@7I7D4hfC+Zj!P&U>4J6Yrz$+35H~+lb$OSfobbC(q~GrK(nskjQdYRJJRYdp6Wq zj|%bdJ9EaIdEF>E3Y~AZ3)X!mgwv!Fe?GraHt>G#sbEzgPkNs|C`qsGY=eouq)1G7 zfsakU1r#*^ssySZR&{d{$K=_Rlht?mJwmubQMW>og9m=xQ>0*#OY3;L4OgS+C^U`g z1DVItLZMQdfr14SPAu$ckV`aRjMCOJfqU-xT+WPiXa`0uv!CbRmWa>HWq!}fBIvvgh6KCUVZLsJm>~FRf((mPPjGgFTfO*A+^)_? zX60|$I)juc+v=D}{2px*m9b>$s4s!WF$~$(fr7D6K~f}XaTgpzeA@{`kOV0^{09JB zyq(}|#*;t(TS)HZ2iaZ6(8)2u92c@5;ef>}KXO-h%AW&Ja>hAha50!tA`rL}KevPh z;y1B82~t%|kzL4p@Gph@&Q@)@gwsW2(ns?)w7vJ=vGR+m}{7clHOaNZYSxadH)@c(KWo*m5;uN#ybDM9Zw z!S3DT`;Sl{x(VY;a+RU{imnZxER^6GR5~p(rKExv9e$w+8)vu$ z&V!2=rx2P6C0>{66yY?PSA&w!iY+0C1C$dfqP^^?Q#dS02iB{O96vdUqnTJr2he&O zUH)sq+|Xpcm36KDU8;QDS+Fv0^j+`jxPr`Ibff+$=+o-6Hf{VRV_G`bqDG@0zAW|z zT|T#dXk?VqPmL5MTBO8S4)t;q7qEPRRHOt+(LK#pz&s`@_;_28ATDFO^}7HV zP*k?IRT(Rp6rM|Kq!`h6$4QGv6w_coGk&Q`b14D;&MHsG(OSrl8TQ82#8=VK)MsR4 z=BVbYTnvT?oG^njFUbbBcsV^>+ul{s|Ba(Yo?sbyC!4|6b;}?6=CQv^86=^fzcTo|3BzPop@&b% z6$*XYi&r0rN-P827>{}0*ckm0&t1!)8>_>v?=CR6R-uAt$3{^pD=2OiMr*Y&76=%9 z@O}?()uK!hcESey+m90+fX-XrspC6^As?aZIa+d7y*_b$Db(qiK=4R(Z=tnd{p)!O z?%1JNtl;abMTW+OE7X-=#!ATfuLpY0??781RzGi;D&m478_#zdIcyb*!Op-RWE=00 z$Nvn+CF?@cNY!>GGV&!8K4-wj))I@ABi|4wdI$6nJW#M^dG6GTW&GQCB1to4yZh~E zuGen-6k)ZBFOL`hWZgEk=R3JhZ`nkx*&vGCvetN1=NoZ&mmjKMWM=;Wp~`m9R2NoK zxfvouT_XB(tq^A43)(R8kXf>+v#%U=To(J^uln4J9Cqr)@K%0!iZ(5m7PM>&1<9NX z-0K_y$Je2gNf%7rujqV9qGY811_IVHBKaD{V(R&47|Am)A}mL&77ouS2bbs=mMX;}gN3TWd8nt1X_d zUr&?8ubN`PDJ6}?Y6!LD+#(h=Q?{De1wE!vn2q-+mR!f>8ely5qOp%*rG00-F6$k!ss9{V(C^OLETj{&S4t4?tScV{tEN=ow`X(XFQMfI3Iskqk37MRpV}%OJf7t_!d>Th6&;5Xuj@ub3WFeW3a`CL0%HER zSXOiv#%;ysra<)P+zMsKzpL3LNDw%GTnx+%83z>{?Nj`Xm-GM!a=6UKu0dcpQ^xx! zRa7(GL)=-MELqgx#<6a7Fjx)Ln-bWiL(69U(&cuq?@4mHeQI27VcH+4UUri#g-Am0 zda%XHy+{V`dF{(aah6{!7c;3B@IV!yNT27EPj~~_%YEZdNP9wd2Rypn7jp^jc239| z7$|HY*-FC0dVy?zh2}P5zMPeK(ip#_QKYbOl7$f@Kq988K;kdhLE35EFpi7R+$!VB zP_0_RT%9ztv!Rzb@sI~{-h>}Bw6D+(R2XOwCR3fr;bmj%4jO6%?tx-$*c# zqBv1`g01WR6X@;Xip~~@zr&*Kn ze43*Bk=fy>Zz(;oH>Ft}22oVJPsf6jlV)?xC<1?mdQsgiZW=_ijk znT4lc15SuZQyfd+C4@8Y>rBB!C&*L^P%il%q|JlVe$YkkM{nS4-IG!pCwj|JsQgWR zXn}-il~0e$6FQv~L!c5q1X!E3Z-cH@868w8%Y0dF@lEr_n4k-KB>DhkW1N7=38EC_ zbdKuXlypsU9sr~6+o1eB5ZE%i(9UzDegruk*_P;-0CM;&jSja*wBvutBD<#^$I)zV zUZ3=Q=hED8$S<3#Zn$*ewpdqc<;j&0y_nIfCp}SfK9@0UbGGNa)w#({1AKuj^#=u+ z-iro|q|4CMrh}pJwIQHf54V(%r}UdI7n-)b9(v|ETE(c+a9os&_?iv|-nz%L))O{p zKy8Y&-%IN;J)rBl*e8jUPW-G=L+Q>Q7_i<~3BnW2HA>+mp-+%L<#2m7sA^a+JRx0y z1L(BkOLic5cxAs z5QYx&z6R5d-mf9^9#{NKu@kUT{JxmqKfZ^B0+NkiglPpn`pDY}%!vkLIsS6-($p9**U&HK$EijUAsmOq;;bOMEM--$djm)=#2@g(Wszf{Qk1w?7hnEKkEuV~Jy&vG7=_IS*=6ax_7G`1+ zR&Eh%Lx%fskDPJ!VYD865GyhEavnf)Z_Wn|-DqVtxaTy|H#ORh{keZV^oYXaM*`#n zt+gRrgyjO0l?8#O;t6g~9)Kl4jPN6F zAU>%dKB=In6UdQdl0WeEl7|fF$NLkm!fXqIu3CFuC!_UTR}239M*Ve4a|;Vc+a0+T zd25LZ83=cxK9=dS`09s=*ASXJ>Us@Y8F-Kt9wzx<1j5ucXFKcOAKaU^;Ab!Jt%LMu z_8O$Dt4P)1bR0=%%hU6)ncDE3lteDH&+BA!M;>M z?gQ$~NwHre`=f}_)(e@Hfe{uyz`-t!Rv-mhzpLLWq6rkVOpr2e{*1WwOX*jN|AvEZ z{{d!Xm@Rga5EgFQOD!ZvUZf4UYn^UA|)=)`DDZ`g|w@gFN1N zdo=^x3VI@PWb%idP?F(GqP)Z}Z&?R14gFwzu2r~Fu-UNt;T=b`q%BkiX2ubGs!%&9LJw!q4YkNMe7ZZKI^*yrq@bY9kS#^rlKR?Z$XX+k z$+76TV*llCH{nH-KU5mXx^HyDTdR$gO4cpri7-QnVm|VP_qB}aQfC1MN**XnNG_(8 z>rpLgT-JhN6X*%S7>^kjS+cVA7}uhQubvZktoO8J9k{_!xz`u7bY+}Z8LnjW4K|}B zb1w%vB~7-Ep<5ISr;J;Owkp5cjlmpm!$Pd*7@mZL^i_0^B}QA_(Me=UT2qJE0EA!k zWYnTn=oXdsE5V6VMTJJ_t|LhYKyR)GFnYpxB3=Ua-cxMV3Zb4|KD0*qZhAK+sUS^tILTfcNJAY2j86BP_x@{DMA3`>_ zSS`rJh+wx!)F{|uxBRLXUNZ?2dXChgW1Q46%2~jilGsWY#`HF_>_ylM@DKh-8=J#x z;~YhJ=tnpZsvBEC%JPv9{&G0V2Ah+M`7Jagd*iqpW+q)Sz;Wv=R4bTJP(bu60ll0L99DllJ0#55HuzLcgx=F#@L1WsXLXCgT?C1s2VGM4C_&o1 z@7iUb!{ymGf*vw0EFBL3K8AD=itelw8g^?)eg1Vr;PV$U9u zv^5lZ^_!88I*PtI zd*S%AX99j4b%!q^LhvtjKVi!Pxt59i_|B(kle>tRtnu%D)252T7#Su8P0U0CLN{{sXADC7NJnR|*+@co8GgM=-3$_iB9 z?{$>+M=QM#ANF_KR@(Xt58V)A58~E6!gv&cF`)WrpP-d@G(Cz(CB0lDj@0O9y7*6= zxpo@|Hs&}M2NJ?E@o?m}$Vn`(0`L78svu=(n;!iYIeRTAqAw8wjt!RC{e_A^M2-_( z;UKB1z=HD@ZvdLgzrh7;#{zmB67Q2RNk+*2XFFJx!x16|BWf@LceCT^n9?0*-48c= z*yNXK2~FS3VQ>=xoLb>ug)@XB%GJnWz52H^OI&SQa$8ks<<3lP`}=gX?$OqYG%sWg z$wTsurgs@;MIn4=#ZN9Uy@xycz|DO-P$Fi(B^6NN``Ji8`9A%CS$c`?<9;1MV6Gv_ z=*U@Te85IiMs%9fH(le9EvjlcT=08J0a7C6(lpznsm2Jo*MZTCh_}OXD8UYI+iY>k^>&cK? ztzVv{S;mcpkvlukC{0%{`rvLd|gn-Gh`SH zRO65t6b`1Ixzb(#^(b{vygb6K8lA%fG^pXxz|3P74L1X~?$dT}7Q;gkK#dpPn)vm1 zqt2Smbo_h;4A+NXa2vYV(CqHl4Sc?IDEB93aE3!2BiX#j6D8O6cHBBnh$ehCpZ5jN z@TM68gc2KCr*o7%#c%Z*R1G1WjazG+naZJ7t_K6v+6~M1Wn%}VVT39$RickcH`W@G zELW|0*znM4{fj#kOTko`-aj)=^qU?7YhEy1h{46nrUnD|z%JlpVEfI@2b!+<)~{^C zHv2#!`JV+>%5K~N#CatUdr$cNQ(=`!z!#%U(L0uhA1e&%O2FGn92voy)!mned3cwX zR3M)P8|%Z3#AWmCf=;3>GZdYQ1~BfEE4DkH!o^+=vd8Aw;y-GL0 zNBE<22yrt|quz%75N_{}PDAYGHCi<|QG-eo+7?p(QasLJ@Q4c%>nO$bJC-KlxYJ)iz z(}uXfQT!rah#}`VLqRuVg*yBpJ-~-6i7I9fEH>+%DTrFQMTNC51Kg{jO#ymL%F5rIYjOgw4+FX@eZI6vR50*sj9K2EfG zJO;W?@CNMOdT?U!=CytQEI>5b0*uCHv$e|g?w%z> zTz6irR-DxhXi`1SmpYZGk;v*;?sMw2ccysFh4$eIY8dIpcC2Ke$Q9$3KP0#@(vrZ< zDALDa-zI+!$K+l78?am4%V^PG^+|OU@|w`LMGOW7Se95 ziJokl{f*YUXAj|_vlfOJIg_;ylKK2*QT%HX;5+w^*A7i%CDgSLvL@vS%&9?gki2lw zu}`ZNx@w8J`Tc?RT?2h>A+B%a_)>?5o)dYcGj#dY<)}T2J6bFNhP)fJvxSXTx5NCi zxQ(ozpi(+V_}tBri*5lXRyd4v@MM7}eYZ`|Qh^TjRmp{PuDEzPv}Bf6^XKDotvovsf8O_+)`2 zjBcRf@?j);{19mik|=!MH~K4Z@w=;;Pw(^@+~E5J-)6_rewyk{c^HME#r_~uY;yzQiFb2r5H%CH{3I6I zC{TWmOr%~lOW@T+CJ#A$ibtQY8;7D)kaV#xF)^+DFJy4<{D(;%-ss%@gYF~Ap3plf zv5%#>1C&KJ7~xA&SyG)IKPgD|G_Ux&D=lg|m!=ZL2EtXoz{qeZL8kB*?WcN>O#UJu zJX>#DOVl@CR{@wxaQh1$62KIH&zFv6ov9CM7`Vi$562MSm3{)$waPd^sNr6xYqIC$ zj0}I>Mb_z>K+~P;V3pAqZ9K6YCb zTP6zcmW(>_$@zQ`SzUgX7-Gbw0`;WE_|y)QD$aUm35a4sXX`rAK+43fx!ngf**zY0 zmPi#&fzQzJ3<57efa({4_4Hv3?9niIi3~UQjd+Hq0x(16O2bz??5i|M3IzGB=fb5! zhxhEJ%+~hi@$}!0cI7cL*P+f@7lQ!l^@|r{pOqmoq0q7%c%pdsZi$7yU zCj*tSatKh!TMYo??HZ+1;Q$y-%5(fJaZH`O5nQS7TrYeG?||{Dm3qh%cSc~xA%POq ziA!x+{%{G5aKL#BR5w_LK>UTRj9R9v?{p4>f1LC|fGn0_?HtZ!Dhj?@@lh8@NLiPUFpW)UA0eYwSCDL?4gfWRaVv3vO|)azSz)#tkxK0+}3l z!S51iToIYy3oR3A_vh+2@TmEpvHG4MP-72g;DcP#@iZ9Z49B0jwbAx|BwQDW#NwmK zsVKj3mNu%_J0QAv1U89;eeDCi^%2evcct<;ck2eo+B9fw}IwqD9I(qlcTYHzmL5l8Hz zFL&#xgsWQZ#N2!dPORgis!DcBLyBZ+v_Qvyp@iHA_#32smNU9hR{(d!6&%?<^pL}3 zATJV!;kRT1cxn`~G~@=nm?Q9)_#!LM@x5cP_SDs$h{52}1@5V4quX>W_ZhYnNpQ-= z+hDbJj_(xR!vqn#ds=`u=YBYsII^{Hb*V=w!siC}&CUff>fqmXn0=m5i&cZC;{)DR z=>enZAum`-;jrGB1NP5`%OQ-klNlKL)o#1Dzhs~fosV<-n6pF6yYGWAe;A zu)1So2=u?GQ)(0Y&}B1|p62Th{W2J6`52}{3(K&U0*$N@>R%QR`Y#$7ZhUF@e zBr0e?ORR%j@|A3D;9dBM)$TW|>Vie9>gLh!^LxLz#;sA?sPEOo{5dy9O%&1as{Z?V zr6(;Sq-O=V+#R{G^v9fkx`&JmnkyHHAFQ|=^Lh16!W?0ReAyP^h+^j-(+1{OvB2Ka zheJ~{ww-YE6M|f6(^8;v&WPOZSgFL&s5Gh2kmHvTv1s}zY$~d$8!CWf_j8nr+ssvT z32ov#CrdI8gOqS$N*FRo-&%W;Y*TI*piQaxNB-`ohEcD{GmrQyKpbf^Vb@e5J|i{L zoxqWJk2x2GMCOpHXt5<%lQsz|VM8SU5(*mc>`3OwRH=LJ)QDW=H*%fXBV*dr?$s7V zIA6oxdaJAG7ov>o>#Uvsgm;K87i9CJp2pR#CYQP z@u(JzHg$$uo3Dhn3K9p@Vq~KDC4J&HEl~$nvgQn8bd)>L#b)J-<`PYu7@s^#9R|dt zP!Tw`yjypv)4HN~mza9BQGAs1Dd?X(!A8L>8WV^CIF9kEA<#J8cYOIMe#dep9aS9F z1bk=64)FsDfA|GB@;c{aYS)`1wZGE};=OkXQWq=%Bin#= zvf-Yo^h#5_8*3zo3sVQO)aC{CiQHf7O8NIR({veA+~U8mKOEL+DV>`lJ_w&qZCW;c zg%fZOa}=N18?0g3I6zgqi0jlyR+et|-Y!dvYIdVCKJqRjYW19FaTX8w1=}sQb*7S(F;ZREs?2?%xWVH%tGZzHU0gE@~ApHV0-MFx+fP0XKgDauQVs2R$+z=!%%~ zPZ$GgaE(Ug)b1MWZZCljtS#om?|Gmk%_^6y7dOEBCZ$Iv3SUMUY9bBaf8&jhr z`HT7YEBNHH?<2}?De6p?jQ7PRD;EfMOa8OnHQ zIa@`ML%DOw$xxZ@r=U<3C)ontYS(+Nip!QpT9%!zw!a6>3__}D}2CvD%GdsC@Qct8xlhE(6Z`ek& zs$DO_w{BQC7P}L_Q!8^MiD6YYB&!e<63SeCa9&DXeiJ`G{zJt}3eVxBvQ<4USv`w1 z1vw-ZRiIv@L77SxDF8Q@wltuuwFLg_r0{`i>9}fK8&XU>7dm!=MXZhiC8rVDH=au)+DLo;S@ZL#rMx0f?3=2?%fTmGUfLbr1oazq=uiy!DGJVap^oVuv7L zrR_i{ZF+eXsk_>h5I2VX={eghePN$vjI@#54NejRY;mX63J1+9#%I@3isMr1jenKq z2&!ph1pOcD2Y&lUu=Dy|P+3CL?irM0%gsDDpuythxek)H|4ZP+UG>Wx0Y6^Jv)-`Rg4AK?plDA9wfAcHZZ-&BHx6bik{2ZVF>E3&CX z!cm8d2@_VLUrPGee+{Kwg5@ix9(n)0C7S$aJ)kQS7dLKsBbsjqXL^e@UL}}Ta+9up zK%p_9jEpe2-XAj2%FF=q(xR3vE0=4^8{ro0A=X;LSCuLnG*k4g;e>qXtAf6eUBNpS zzfq=jDf#;oN;2(uDfgya;i`vUHw3t`N?;-}fA+d%{yM<|)g-Fv|Hu;o`;|Lf({67wcZQHzbGeGeFhk5 zJ1XmBX~ijx*mE2*=aoSS&}72(z1A;^4Un>{>z~r9F};GhzUn*AxuyK2HrvKRm)%Pi zCl0r3dluKY3D7Z|&RbtL?TD!+4XWB(+RrbN@zlEx-&a#*Go9$_*6MRly{ZkjY8Q8N zZpZG=wiuU|d8}z@S50`p&D;7w>jboee3iV^d}_6p6(yq!{FJLbO~#Wo$F@hO1K4z{ zOH2A*9eN%s;AdN49$5j86#lI01eKH3HSmYjM(-=&TDm$I40hI}e5$V6v0&k@+Hf*6 zcP+Zswb=*PqNAHL#MvQjQq>wvw*g?VHR;Ln@~F(62Fcz;73!K#@J7^XX)?O3ZJqiK zSE(OZO7W`o>%x89GGST5CBrj5#<<_lb)PF+=C01Vb%Mq=x2wHq9BCg$-NCj_*T!3W z=x^4g_D;;Vu9+Cw{ury1kNIYrTxd^2filpL@QrCsqUOUg{}^QObdpm4iDovJvEuMi zf+G8Vp-jNrY;T)w@N;munGIxbowk8cXKS@_{K+D7sjC$wYjqMQUIt`ljO1tLX${}_H&X|%+ulDry?HFuwN*R?dx6f7KTo48<$pD4 zB109$dBZ4hiJKPMhn9&0C62G2oIJAj!X<*!^pw+PYC9G2%YFFfZ*Ru@Eu|nB^Lx{stz%OsAYw>0WkAYE)ZM( z$%JseX;OP^&>j)6F!Pg$rEfmRzAjoedbb=k!qOwy8w|8^;qoz9@rs1t67*04TJ7_7cig&ldek$I$hbPhb7H`ocA#tlD?^cPsSk zPJ!`qe_so89D8`&jr#lG;ViFPvL0P@9Tj29cL0Mk>2f+1vKdx*dm#u}wU%~-K5SRSr|&0GnbqP>mdS&vcOa4yMp zwDEHH5`v=MZ}~oG^uO}jzyaJR1cjv^$$}eJjGb;?!J_44W2~ zY${B#`r`EAuEelwZg=v`HJzy;*W6jzFZz;=vl=weIpO-Y7rk|;Awp4imGOXgXLiRa z{guW8u001W&cN)TXLsPXJ&p8t%niP7KcgdXofFyvE__N>sdVCR#i>P4zszmHvgJ<& zCq|uX^S9vC#qY{*y|SwpoGkhEUt*%u)p5t$Z|cfdk!@EV<#+Nv9<$y9?M0s4$-kwN*ylkmpQ|MN%*2*~Hd&!l%pDcj{(R z-wFRK*BPL&z>{KW^iVEtSPzwoBZoCoihi8W%^HdILV?EO@?$(Cehhj~3x>*# zFX7VS`vuaI1)Wdi2C;^&EPUIbN{5yNFP2cgSqqrt`=Bh{jv~6X)<-wSW~hZ=HM77X}@s=x7$H0U(DiG!t#+u=1avaelQD3K2?o6zOD!(NrbQSU0 zn45*d(BvzObyWjR@;!koQsrs_MJ1|S%=dVnfvjXLHOk7Pk0NQQLX|`3&LL@;wyYpn zs>#aZk0WWRKo!{XwXY#*Ve-zXHbfH~7jjkVY)jTH-QV^Wg7nlbw{R<+ib?haM#!~{FyOF}ZSP&{i)Gl8EH%jOD2e`%-WcWkPCfDnZeTN5ruWpLRyW4^Xe+&-ME@x!-_V1&5v?WX z5gU6|GYr>Y1FuTL<69|y4EQjKzc$tu1IDF)RU^Pz3`6-)>qshx`+aN{O!Zt;o?%v% z28VgZSd|>hTl3ZzM~@42#Oe7KZVi!f9$e+g^DUl1$kFLIn+H|3Lv_uWsBbg1ZI!9h z%js>7HeX?5_4P#RtJ39Z=KLs2kW*a!T^QM$xK}s6h@$*C>ya~zt9S1n@oC)Fyiwdi zJF0f`W{pK`6Q^1$we}`)?gZ&47h|G^$RnO&y=QU(ta2^pjGYm?p}=<6S`Xb^w&R?v zF{R=g?(Rklynbt`26WTNvBoEE^&D&FEnm!#q{x8cj-I%u}1i%=;!Aq=JBQK z>Ibjo+0uw#_aT1uO`l)?l<+e+-u_Ww5{wBp3-;u5&ABpMv4k}F zyTlv^SqwNIk^i{_tcPBNS0fM4FJGbtt6Xi$QGuU{n%6MFgE=58&hdDctLrH@&4a7+ zVejgx8wFkrN%an|+;vo`KCjp0RxQ)*6~4*^l3;FbL=(PiErB;KPA3?fyt#M8Ev3?4 zQ!)0%_>Yf&L_g0%A6#hNieI-fKU?VmuwdUl0l&X(q%`&g{;H{DpIhvtKE7#Fsb06> zEzVE<0@Lmm;ye)WX=*^AVdfkaR#lb0_b#lhl9bDV$klLNEUf&1lmCv^+yL(qC~;T# z!d#Zp3v#(A{A+b_P|VA>$R(u|-lId@ixzwl`Q93wQ9cDf?B*6=V`1-ga=?Fs_|S0j zb@ZHl$cI+qE<%Mc;TwOsHSfducuTwlLZtMX8LmmGha*Ljul277*4zpaZP32*6Z z!MO&*XlYkGw{({d<`CxbcyVeEWRChgij(z` zjdVAeVboe;icyv6M1^ocVVPM)e0g3}c5IOl@u3|?k^LhO>$rB-^j}y?03|b7?l_Yb5-Hu`n(islT^lkpoX|PtArrl-(1|IoDLc!F zVoPeuk)|(y-#~hL??Zl?1M99zJ-vI`JjzlXy|)qvs!U7oQ8rNmok_?JV`fMVO>2sj zsI(1M$80J3e6bBy{2c%9iFbJ`+6~=cyI)~(c9+JPo5Wob%NQ?uT!`9pxg0(QpDz=9 zisvQd4iFB5Gr6;q1skl$pPg;!Vdru)aFC^o^k)uQQ`RD3=hU6UkoGw@Fu*?IdZ}<) z-LG0h^FS+x_N61?JLs?Grq;1*fF)$MiU6SJ5w(NYVeiGVV9C}&1Z%e8G6e2C@Z1Rf zJbu!DN5WkbL|6PZmG>8rK1gF-Hr*P2ezjs9rTcV=I@Q`r_V1pe0~mkf1$2%!*OD7u zkbPJpBl@nwT=p_)&;<#I%sI!*?3;E!gfiiVdWiaP@#de^+OxuULvzVDLjgL-KC40D&j$>>Xrtyf(}nfr`V zbi0aN9aIp^>)E>lKq)1Wh!;O3g7p&-x4y)lQQWvVXs`!Y^mgwF z9AxeD+x>y%*WC79uww0fyMn2puj{o%r`I+HCjL8TM5Sl{E8Al;34KUr8VhTDE!eOu zhconzudw)h7QI{4P=s=P`zO6ti_feJ-%=81UY`gL>BHcw-X{WQNtW%rAR+hCeg)H`CDFF|^`v`u~TKZt*ZxG7j8RSRL%c1)QKj)9b z`$7$M7W`O$g-Qn=3>C=mVIB&u=f*>9UzCpO)W8i2(T>7-hh_`^LwVYK-=JJZg*`I4 zRd0#D!2eK|QlB*&RAZENhuv)S+j#HU*loT(rG>42FIOc~7TgMSJX99m3RL|@If$WP z$>fK^{iJXAY-XAtuMWzb#22wt$);=JRE*;0oM*Bb;r`xsvVlk3 zafvoE+Iicm!Bjk+8eDZpbAQZjQi^FE5ULuQ{ku<}-re6K6=FL`B>v84KK(tR)j&kPEdT#9MSh0&wwj$YnU zpk}5wh#5ww=om5=4e6K>!Sb0;Mk`2CpV^eOXkJszU|*yunD-(I1 zE<>57z|&(S_%U~<%JAq49e7^hM#f&Nig>*v3>#XSY*V`Fh8@$J zXc(I1(oj>ndq_;v8FBc3vQKf)VA}hX9B0#1xFg(IcB%I%KI&$;PsxcmlYROdHljl2 zBBOo!A$*Wzg0sn}rf>BL;AGmiKiMFRXGlHCS!`^-^-wZB-rnE)FP`wac%3?JPGm~`Ac z(s>+Tiq|wE!mp1wmyVMi~E#v_3?KxT~w>p$Cv3IGNop{bnlP~wd=*b#5lTo5MA5qqbrW! zRj1-3Wb{)_MC(cA>2#QhB37dl4(&~)%1XRuc_NrodA#pq>-OuCCthxFRs3J#h1Y5$ z!=DOkd>MLNsI9L9Kjc@Uy8;iE^5i*J7;j|gw3)=r=GE8bwfr1|8oGbJTA~SCZ3Ns{ zqWK!vFXC|$v23qS$?~n_rqcB{B|t4t-&pD1a15OvB6J?EkIv=+^P8vT&Ct{j##Kl6oO(0X$EltvT9DN~Dj)KxKBYM89&s zK`w9L<+9n0C+{f71&-8fPOG%`e3F|fsh6GERG#+CT~7ku0A9rQkSL$wR*B9g9#-%g z44TtyE~f8*UCw3V;YxW++5pOtH4Y6f24<|UtdoBeo<(*^oSB|2VOo7K)3X6HJe%vd z8_vIF(~g3~FBF~)8ZC0sZ$&DfN_74_d3k@i|Eilq8y~+Z!oK;kb2zZozL>s>&AWn7 zqMG;;a^}_GMY;#SLZw^Tc8c8h6i+f|2ALP< zh~224_@hc8m4e%E*tJs}S3-zC(Fe{p4Yc_5Dj{4`m@wv`e+?BXwGxi5-}(VmSd|k` zYs`(lY|sTGQ(|^xwmRuW8L2W|y&xc0dD6NnA8;!(be$he0(xqHo4lEs$_YD zNlL0fj g`oqkXAM@NLoV63G7T1!)ytv(6qldM;C+--r7)P%v*7F#DuL9P? zTpWSH8P{I=tKiIp@~dF1CqaO680Q>(KMh>0|+AJZ9 zPHuFVH6#WS1Y z;V_P0mo%Emd&Ykz>DzBi+z^Y4m1b}2U`xK9H9in^s6=r$+%rB7^x@Pjzgq?DnObZxrP7fP;uB8`3UCVYa0;O{$dJOacu9;Vw^?`iiYb_RbPQ(O;`HZ>aAWG_kX3?Y z2K#eHiP*!;M8;8`EI++jf+ud~F22dNYbKkaTwAx%Pj1_~b;~2yuJoBi9>3q?2t~Xi z`PO@q9lLhVN~Ic>C)!ptB*Bu;Zrre8<7Xe-v^gX9h8+#zR&R8!N+{oS$6a$5FCMz{ zb7PTMEHa<%>DdMQkSsCxixN^R<@Xgr#<&J~h=_>-yJCUl;Jhmi&Kn$@cX0cF-ym`a z0&ZvMvlh?LJdefVnK$IIfcKl{DZ5v1??(UIzI)E>q{7{pX>6l^veVbv>f5<5(AE|p zHr|KSIHCzMUg!hX48jxG^55Bcg`QZ_T2JleEh3l9S^mv+1WX( zp&@fxrA{J7HpxV$;4-D!i5>Ljp=T}BxtixG49Bti@Fuc*b~ONk611QdYn~1>Q>yE9u`Dgrc~3VW7$3>ib`vSlBA1pX^ZRhf{CpMd(9J=|D6~2&O-$ z5MSukIH!>4p(;JQu4E%nm#z;z){*sf>LOaJz8+n3IbBwLb`@5eg|n+Ty5y=tGORwX z+`AV)C|8-2%TMF#9j@nk$J9MT2y?Xj3?b$)DVL4&nS3vqdit_r9#zOzBFI(9sQEp2 zw>pCvDj4e;W~lIT5vw`L&v3M;yH6&`LYx}OA!I1;=yEBD0wF^OVL#h@adbvgTk4&K zQ#iuKHjT+Rd^5Q??^wvtk~+oq0PU)~3upm8Ra=6>0{Cld5IPlRh960mAN@1qN3F72 z$Ous#lFMfRE5#h$53zJl)lK(oIk_$*xSqgS8=-TsT~c|kcTQCSnRiTMXceE_>x^n>{EG_UuZ<1sr_6{XJzT(dNe>tHj+N+sZ;F{&hG$v#Q1~eD6Wa^C$2M&N zF|KI~+sx&}pGfnTclg~}^h^hr6A!1{m#$X}*;94&Y9V4;Tce1@>_PC4>D7Wa*(K@n z!Q?bus{S6%eEw5{a>K77*=S{~-zni%4%3qMiTf*e;}|Qia5r}MW2Gz=u;k<4P-*S? z65e>T;+uC)wOQD{Oc2j#abde5ST+P)vWL3sirrS3f)9|}vUR+)CG*y*qcXiCAM3TW z>y_88C~4HxUo(pB!ICwjgpp-xM&B#yd2|l#qk$Rp&?&GLYTr(OEI4Ls+g5G-W0u=n zFZ>_&(czacU5Xvn!*A?;iJGzei5Eo^F+QEJH8+cV!HlPo7g~ce7)@SOY+B!-@@ESy zED_@?Qpa<%q_$l8X;Ot|nj1@IxRYFV=3a3+Q`|uTr>8a2eZ$nc zubEO$uk|Y1)F($T*%CeMZ9L_#m=72 z#MlB(*Ev3RX(5m67$yrnQ~1lw^mu`t>5;8{sz=d$nOY`%hzn-TR3G*MUglp=2-$eq zL#%F{Q**>Dms5zo(g6Oa8slh&Ftto+h+6bkx$FVn`zl&gZl%Ul2D8m?G~0LZ-sflW zy};_v71NKll}D~Vl{s1l$NCiIm&&5d^i+m#ApV7rFWXmBx62C1RqcU!cNv7LG~)eT zIShmaGvmm1c^sBS~hi*>UY`QIxd;5M1olRDS*3B(>j^=VRS zC-Zr#RsdGo3uWq}I?731g-WU8Wd(00`Er$@uOZ0y>oSPc7T}v-Ne|YSFIVvtni|~u zL>aitPuu!Ud00t*Pf{;4C|GkzY9~rJu9$8u#q-5k=F=;yptqN-(+tsXlA4(jwrXa^ zwQMEQ=4V!OakLo!8I)hp&y?!J6-E5Vbr=my-gwFSgE9jl%GV>%?@y^xEmfOv@tvuC znM{Z$$Ka7FH40QsH3}Q5*C^2cI)x~J#OM^@McfZlRI%G#sn}eZs9fn7IzFw4pdmXT z?e(#F6lMt#EeUwV38B%~eTC@Xw_k7e1Z@(% zR267`R%+PXp1EOYi97Z1KRRX)1hYX0xC!1)I`9#$@$&{L;%o301UGG1chYS%cw}0G z&99Sz^*v^xL}?KlTp9n;t>8EFwhdX4qSh&)$=YPn-+VORg!U$Dda$@n{;Gpg6BtQ!v%jZMwg8==_ z1!NNn3I<5L&Mz#uz|YwSg3F*z@E+;GnCrgy9=}Z@;xXugG8l!A3siU#Cny{10)Hu$ zhrg~=4m%J$I+*~ULf9C1xyZ?U{2bhdL*gO(a=)63+T+|}d#_d>Vq){?8Jl1P{1V=U zGbTXkMy|gsITsaPH?_9qESLcAqVrUR`=ypoF9*@`Iq;{-_Y?v2axgL3hMtdj!yD)M z!`$pn=;IH`1@r^x?~ky5&x=yrm=8a|8}g9_K(~Ycnfx>(A56vU_978Ne-nj~$wwG^ zl`b1vS+YiGD!I_J>~L`MS$G&(A_81mGel~422L#;{ z401`4uNKK5?Sr<-N8v;0Z!~C)qRUG9e8J`Wbp;8tfIbZ9$(8VVoSn$@Yr(e^==>$A z+97^~g80CX1VV10Qa7RF&Za>e(P^z6E$uEJJmFSpEIOT9t!?p()mBO-R2uabi{9f_ z=`f53p=@#^w@=E)gnv@=HbKu^hUDO@)#Z}@^EBnMP;QW3*kD(?)wh!n~akl>iYNfMo^A4F3DiOtw#EC;uOo7jc2Stj_iJqyL$H zbFy*rmsr+IH#+-S@xgt(g#CtKn~Y3;dh*v8Hc1&-cJTTb7yNx0Tz;BE1@FnpMU#(A zULbJRu_xEDRfD1^sTxH4adLd}*~tg+ew1+g@e?U_ua^BCWa5$VITDYoEf!f@ypgIp z=aGq}eE`o3^h_)#$e&M!ooQt1Bg2@9su{R>Wj#Xp!O5%XR^%K%ap=_A#jCG4FSjHD zPv1Ltw887^fALvg+KH^!=f=jdtGi{mW2suQ9spxFVx#{yM*p=x+C;yt_qc-ETz>CK z_y{>ea+WFAY}yM?#zhKScuwrlndbZN2#t@!ZtzT`t=(yuc$!$5Be=eH7DDC}qM<=x zX0c#1pnCvAwlJdFDW&@2@8vRy1RRhVbqa+Ry?OJ0|5qs$0ueHv;c1-|DwGq~t_M%h zV^eaEt50aE8cVDkGbDWta1ME{A7=T?`Kr0&6|%-zyh#5=e~lggEB5-Y(B+@&#nz2{ z&)Ryig%O2K{&doWsEw~Q9{-ouo_gZ@x5Yg>IA75Ehh+p)Ba)8^qANH}p#TzAQ4k36{UCET8Io|qfB5Fe#xFl8Y_XVw>y#UVdW%Bz zTcKF3HmF2@T8{bl9eNq3A|NGJ!ZmW4SfQS{jaf_BfbdV|*3$Wb*cZmFw@Y`7L>sLE z?Kt!sL|b7);LPd3IG}&8h$`s!K~b;BcT`HX=>7MlI>RrB1i?iU_gHOk)x=vx4I=TD z{1epaq=YRz*-cNklVv?UEbHlBiDVrQoBaXuv++5NH?$v12JQxjH?TqC4JUCTgBQtp zmM`e=mAFPWkG^HTSdN=jpU}uA4$08Nr3=9|V^X;giTeXc+&9L-FZz`dnWpC@I`;bO z^VCY2dIYJMAH?Hq9*CRWXH{d-W#i4|;>rRZ?H}QF&gbh5Zz)|tr5XN?&i>-gNp|Oa z+iHtgtP1*Pa+Tc_FmpPUM(g+(or*MS5HJoh)0WSG{a4^61wpiX;(Ct~G|<12tM%}> zJ1%XUyRiHFzm_QJUjS9-*ho|gg!DzdO#XMUL7~)2Kj>cAsHT4_6U*c@tqLvb&DD^w z@s@JYmI`r|l2H`4?+v0q)}>jlM=1%*sUc!*ZUw|`ig^Ce$E7}5lFX#lZkbGGL82QD z>CIukHhQL+zR-Loq6s!ybnyTMq*A?1wrpV0wGF^^-RQtF7*)ujPN`5(3b$=w3;h!k z@QrR5w0o44S|^i1C|9IcUp0Pc?`o#+CQEBPWwC?t+a>wRy%<0)W;)m2*(DU|@D-#B z25|IE*8?Ff4|eVwIn#XKz0GH?dEkL-n0Lebx`J|gbs*WGr6kr4&?|Rz#bR9!utFC~ zNB!bm7_8mWo%imPlH$=_Vt*tZ`VvTpY|g0Lglk<6^|UlQMfCSYPCOtF%L3Jp;GC7W zxUlVKPV#g|){v_-wBV0<6)(Zwpdm^1`;VWEEII!W5Ci|Lz@fEKr`u&wNnU$rGx^2G zPUU~$v)WbCM33%idQ4a0KgV1kE9`N z`ZICMPLQw|E%{#GnQW?uLSItd#s-Zz;O;xjQ1jUAE-2QkES{m2;A-Zp6F{YFFsOz+ zP^Y0p-L0DDWzKFnq2+Gn+&M?Y2EQFbt^VY5esF=DPW}GJ!m}J23c-lZ2TFltvKv>P zu~vSXR~SSFf@8&p+xp`;a;gX7kTZh{am_MWN)deCplXe!lj~PyGi@y@!}GzoN<>Lq zjv;eLFc=Dr-S*sVW1(Ou*l8MaxFwWa^2#exL*J)vn!i1nY)kAKz3bG7LHbIs8a*^V z63eV!zIdp)d1!G{bWNspv{@pR+L3lfURD@+%^$$OA{oG4G4z8(4$l;BCo^=-%!JZfW(u3Riq1+?guP_tij#w{;a~_)j^}W7RGOcR`?v=#=~IQXw>V$F^z^MC<`yn9eT!R|V*8PACAcSR!@fA;RD}vAesX^W zVo=J$+HQ)SM>bAq(pC#qiXkga9r{c$L}jQ%8Tk-lIf<}x60eO?{3uCfD*di#E>syB zjTX!;Zkdd*GSgBEbtDCCZc0(+@**h8&?OIraB>3af@g*OWQG%|!7ujgDi#kFx_$It z%hx^ei~E#iD}{uUZz6cMQ>Vy03&WKO8v$I=@XMc!`XMbdiXMf~W587uo3@ha!u=*=QOS;0BhqyaG8NDRf zlr<%wCVz_HzLIJra9hhjPA0n*LjM#uAFcoe%0qN`<77MQBX>^IOXftQ4sV%WmLEq} zgVTI7Z*B|H_tY0i|Cx@md$-iY0SmjCbISJ%cq2P98wPo=cmToS>rI&}7LE?mr%N0L zCFT)q+&C~lcQ9~sDh`oquxWbtf!z5}$0U#^*~^aFo&8U7_af}9imxDBtg(h!6|nRn z!Az@ovlP+z(<&D6c*)rhGoY2=A498)f8+Q|>r50L=XF{u+^EM>TAv;cT$i`-t zL6$SAXOu0LA&^f$H|$Ve7c;%EzrxsWRWrdtoPI8o^>v)l{}pn)e$Uh2SpK;F@Be?G z>%^-(x}vj!>Fr(%U11fve!D7NX>EQcHK8jy7a6)zLKS7HEh4LwNKN~YI~${u<19YzviOi5s^xJxK?J{K65gxCUe!&GgGUJt zj!M0O@eV73zYRaS1o&T1<@*SpV>6QL?<1Hmbg!$Ops}cEzvi}rg_ZAIZ^0LkEj4F) zy#<5$jH0?44AS4ut(`A`5Z8c<-u0cy|A}_E)=)4sD}{~R~B|OteFpm%?&A*4-XN)VsLt#n9`52t}QW} z9|)*V;0h-s4=q3i(a+(;OWd5dR~{4#G8({nM?G z%-w9%RShX7fYw#}_?=vgoT*d&Cql>JTB}rRNUp50T2)gf2eVoqxQZE?`TlUv6Mk%% zcExH9h}Orea2b5->nCTfgg#q)EF5Kf_06iiTc0G{3+nIS0GMe)hw5qCARxVhPZ##+ zVEQ!dW_$WAoIzK=Vnpg=(vA}N)K^;k&{Xth%9j{Zpl@+OH>is*S90CS2tGCOWdQ-% zLBPM{eBmQ99ljLeGabGZpqJyz0+ufe>*LF%(}fJXemOmj$T04XDteLQ*ROVXM!d-ZUT5c8 zQYkc6zqJS+B*jG+|1B4%;`Ytugzh-MsYbf>Q1mQ9Cmm(Eh*v(q^}dnL!%D(5r9=W{HdFTW&wzT{TRt{UhuO+lz^(Q$Gy zo0W@s^~=S~tQfu|B&K7_96m>TYq5259K0yw*$0fhbAjdE!b`Dl;qaI=-7ikgIp>WtyF0Tpn}arJgS1+O)hg$N z5JD0l1QL<}115`X1IE~3j>9$>gArMPu??6rVBZ-7Hr)A~e%u-30Q@YoZ~xu>-ptPI z&g_l^_xs!*v@gw*QZ;>_MEG9!uSqNWQ!*NbDug3@}C9%82Z|3<@!JpCh zB3=xYgOXN`>YbBMPnA%yvvUqEoqcjl%a~~=2tNnPLrg29K6CQ=%@WlqjhuGbY&FTs zq!-DgtsI#YSBY~<;mo4RQ0I_=qi_b7<@qgy=PhONyaWWqTwDgRq8JgSnSAjyZ7gE~ zGQ)_k5)#|on1C}EBIji5!6J)~bHD(;TV%D7%3=W0I|*;AaE8lrxI2^Pxmjx~LUU=x znrD)G+Pp0=*~{c@;R0e=yq#u_e>UFc(_AiZ3u&HunelEKD;H(RJ zWifw_Ma`@%cFD3BITcxa9{O7r(R-(?#?57|*=FVEC-R$j3VP@C^!mXWi+1H|cUfN3 z%-F1aI%WBEosu3pJzXzJ=!*#dQ|0o1YLTu?j*paK{mC58Ei5vU z4M=kGVcs^B&)ZDdwjgliEgY8So^jeDVK&#*y_7C5`f3X8p`5N)5JEFpdZl41H`eT( z3F7D`;6*l`8b{F=L}!#mP&SNamVN7749()XXbB+MNth9~nf%FxxfJl=%&DM^X<>G# zo#L1oFZ{L~A5=U#C@1x7P3lOA@tmJ~mU<}&;M2e7mEVQtqS@I0=$z62xp`+v6o4;E zExrU6z?6>(YU3pThEnpbDUBwZn`)Itj+Lrg8YfnKZ)$!eVej5c>N{_4w1aZi*GlqY zd#Mq!WG=PYT30r|+6lt5p4oU-)|9L?KTCClS&(?2m}@*Id`GsRl#{NZoUC*+JQqr* zs4He)mtu0X=srH-EwcTj+;z!0ve(U+0+eQEeG%GbVdOvwyZ2mhwo(6jSz z`r7@e^RK|npNoHmw9U!CnYO*ezlAdY2Fm5%XX^j4+3n=n89c`ty2S5YGQS7Q;P+?K z3H7tqWln+IP@aD9%kpU{TR#+C=bl3%CG6nY3NqOLlHvzR=n^`8Q>L&yqPb&#s+?L;Zz@ zEiLC$zLw-u?mVrG$^}DJ+3A&6O&hezOSTLzEgT?dIrs8&B=>Ujsb*g;$a7UzCT3Nh z<9iuNnV~Y@*Zs5ex*%a=*}VQ@LC{CpydEk{cKBI&Jr(a!9;+Lt;ylXWG+Uj@_6wa zd$LF-yO14|&B4bTq|Os3m>D)A5~;Hwkzk-#TSLZ@rvnN?C+S^_TNeypc;VtMDbQBC3Ffx**trnmSl7S$!Pp^`$ZYbsT|>b zRLX=r1xfCiQ}0T$?M1R3I4#-6ikp&UNb}92M&zl8vT}wUnU&uyNPdH-DZk>jWH}Q1 zMsc(F6y&vQnnfwuK)aAV*&wfpovM6twop0!>noBmbP6K)^xiz2iyCATJ%sGZq=Y(q z>b%c^Zj`0m{v_Fyr@-EcTXXi52^qkP(Kr&z+Lm||mE-9tfZV)z`?Lh%TZlJ7FSa_ot(7uip{VGL}k@qYhk4sQt^w zYL7O6tAhIi2PZ(?x~DVpcA<$SW(SB$wiYVR@1mD?S{S zCx_=|=B+|Je-NwyI-Cn9f`Nush_8YA2RF`n5B-B3AdK$;41?4$@d7%>N$0&aC36<#4aD7$25QwtC*ZT$e7>=03}=g!Ao8)%MNsGP>v@>~q)Bbj*i_#~vXMN3Pk5SA zRwJ#`^B5o3VxSq;$>|KVH4t?gy_40O0yc8j)zlO8xn2I?K*EQwO15IxWSY>Oq0Y)>q`6dlI27c64BFX zHb^-ui~KA)%dfx*&TaEF=; zn)Qq!LKxz9rp>0Qt2Q6u$S;)^yF)-y>~8V6y@58ri&NO`7NyN!c$L)~{bJY}Fd7+W zqzzy7C2O3FfpE>C%D>2K3DW?GQ48TXxCrM?%UR0fD-W++c^HK8=Srl-mLTT-jk=TM z+OttqdO9vtL7qrZpZ~Rc%4tmgM{!j1{65pM zbd^rJQx!hzyn{nJoyr8RYKxjZS{?vi>oG^##p|b?I}6Wm*s^u&)=k^DY?HsiFO=Ge zfZnKK)eHmoTPqW3>5g-B++>*e-q@(!oAx1Tr@i)(6$NMDzY0!qTNhAUe~Ftf^DsmW zQERB()KTij(tP}!c{&@1x%o3WJd@WPEX8a1k2!dKioE|EWH5(B0Cf@iXa~Q*dE+AH zY9MP#q&m{ApuxiNR?w~$6ijj}%Wi9+m4eoqP8$r0qbq5iV^ca}d-km%5=@p08uoO$(KKXO_ zYv@H*hKGux`4gJ$g}DS$;L%QiW}7}_cv*G8Mia5)>>|9}i@iWPKyu7^PQYJRdEa@* zTjP5heRzKPwy(BKkr%V2$^kw|zDES5=;O-u71c-u@+c z)DguG{e}E2`U~>2N6D|EWS^Pv5E|N+B209n&$GX_zX#nV&UU>#92Uu}vtm>8%~8+aH^`f4IhTWs-2G!+3CALR|SS|KSW@YSxctGzvFQ)?8g z${DkO#e!L1;|cl#^<7HnsjBio^Tves7TV~Q?5{6QcEDwm7(b0-(Lk&Na+o3>m~p?s z)(KqZ3N!^APF~}kx1!0R)2Zust*N&=prCS$*&=2GRx`Xg5i|w`pHX4aDdn=E7cPT$ z;0B82WS2-jm ztBvH-36bMg07fl}>V<>VY=4c}wt!78Y7c6yCcdkqGftb8O3S4Kjf!7|wf?T^zf^Z3 zouCdc|DYmm6~aBW@6~1$04M~L?yNV`JrW%w$bMdd=%|+R1d~KVWFIFciGds=O7L|L z_KmV2xmdso?eDBceqMX_rNc>u!L0bBMAStWnT)+P@76YD-}Y~*=I{+Ua^zf60Ep*ur2)`SH?_#n{1!q7s$Nt_61V*9cfW~MC;Wi-9f`aAL z#<%KXTsVb3LXH1SKFK+JT^Z;XyAMDeuVhVXWhGSZ0yJKmF6k}JjK&jR$#Ya}P&1+5n=gMPj1ZO0QDqjFq zFQ6}U9AP_K6)MA}4TM6=Vy*}Ug$ zOx7T1h>ONTjY;wm%Wj=Z>IA!wT5L&6$SQr1?CWeaR0X8;-f22Iuh|r?aK%lHob||- zhT-od>S*gXZ`TQzUIdP9U48^9HNQQvp}ik`|5;A^z#Sg2YhKf0al9^WwuH=9<-+vh zn+MKh)e5V^O;(U5I?dK=*AF?dItNpkI)6W|?v=j# z@z;}U2sG-Nd*$yZ@x}XnSK_bzqY_T)=nF9!F?IC$1u}Lm=bPs<-RWCrZ1I2(rSHFL zwGwvf<1#P)h@n&eD1GMGgZTCOHiDx1`VTOMs(3NxP~Ffg=8Q!nx%#&5#yW=KwOYFxa(%UR zW6lr{BILeGd{2}P{>F!#V|BHC98}x2v=;gT8+*2>c(c;&cH^%tJsSf)sJ&9Fu5GJ~ zCYLK%yRxbZe=bi(E8A+-+Pqwo0qoOG74%sdVmsR}i{_r(ftkHSN};q2A1^DFhjJ%k z^;X<2wxR>@)XaR8G(}B*Oi#e4s1|B~T88*9LF(c$OWj zt!aFJTU+n>m-eJH+s1gK5nQ}`cz$!Tp9e>UmWUVqNxs41$$v03{2rx5*7aiQYk9uP zcvU5YvvJ}EPfKG#Mhh(lG>pcmm?3ZOgeLuIIdu+g^U^WYb4ZF?lLyvY>Kp6Z+fz|u z2PGTmU-h)L_Vyg;iMqu9au|$2YyU%SSFOryrR^4zIH~Ari_fq1`)XCSyMDzfc$x+3 z*wRJ-RL)c=(Z5#X)VZDDoZ5vWHObMXlQz3f05|(;YXVl2sxIOcz0NTBn_b-Av977s zUtR6DnT_kfI|-U+)zH4+>pS5)-saYDi}klm@5Mu%b@lBQ^*g zMD15W8AxtT%twDzPi#qCd9xMS>~d?CGiRe)9LZT|!L12C__Fw$mtVf_`j^`7d;h-s z-cNTYM(PZQ40UzK)15s~?Y6hI+C3^Te%;G21NX}>iJ#)9?gMKg@jVs*EP%1prTqr0 z@)D&@-fvz-#`>xJ6w#QxWYdY$tCq@C;gi;%Ey$X)7RgtU%TZwze7vC4D!iIvnJTzN zNr4Vn$z<_$oV!> zvACjGRtRE@O(lh3<2OxBV&EoZ19XLaLOv#{A{0~4@0E<0<=fxL52hRhK_!Y^fbpQv`j`V?p zT0n9f&{7)-$~g`oM#Ki$G+H!fIX!C{t+dRmAG&GI6!X=t_P^?19rwjd9PQSCo8hH| z3*etvt%kcdpABlZ`!YemT;R-z2hi*HM|9#nn0&BS7xnv&iD%;DE#mb$sd7laOO<1l zV2N9(z*d zN?J7+b%_}j0>7J;xFWTYG1UJ7PolYWREkOCrGz*@+M2-Z4F$}&l{652VnqbT$XpDY zDe$CVpTEAh@sTszyW+!Jc4oe^a+_n~7m41RtM6!qDwRU1{UV^*zVzb8b>4coUr>hQ zMs`X3*rK5Wi_*5iW#4vQK7aENZQzw3GeTy2UtLXA?`64s%uGA@TQ-|Uz-mM9B&5YK z&N}YP*iK^FjoMFN-0Z9d98@rd!y|nc*w76=<=jlCD*mB0Y(4;V!`B&s{`>*jr9QA3 zm+hZyfqrB^8qi4;4~Wr5G@--AqNWf1m#^`9*Id5xO0W0ImEbPdmWfW2PG^G8Z*jF5 z_b*$v-@J2!v&YK2itw{3Q&Xcl#5 zLdN{M2p zL|vO}3w$0^JkhzuCG+x^bUSdMF%I#PmEy?NDZZ`( zm@?K?hA0pC8+c0^xzMi@KtrbUM+7F zvl7A=9;sq{0%U9*Fk|Xa;8harU9vv10%=3|B&*?6N`qQpk+$PqypriPa<|nZwt}zF z0vdN%7SX{iX7o&yf&b>!1o??O^)+UvLSPsL^dI#%YPK4bzxpkwHL#FYIy6Q>g(?q# zTk@-KnEV~H0Q>-VK#0GbQlk-|9`QeyIVssdmbZDNgs@d0Po$@8K%XSvzb5>Np-B7q zc=sjnDEingpLZSGw&+;WeBP~FubVo0HWR6>7;Ed_@<9KVtEA)YH*~eyu0OU_{+8ES z4$ zSiqz|$r&^{p*47=)?vBQWV+JwBfYo=-0RgDnd;R;DuYJNcP_b`ui@`r(#fke2G!8& zYR0Iu^40x!^;h#(9PPREQaKI*({=&L9Z`#CF6h;kDN{tYwIs`z}Nsm~C{s zV%5sGZ@+5$YF?``C{~l>Rf4l|zBo93_Ql8d;g9)nhf`R!{i@sFUb$Lf&}jM9g1wDafpe)F-nj;%X8|y( zg@|~b2?&ri0%etWi(6h-bp5E0CV9AhUcT9Mt7PhMa`tiGI%RwMdbXaR!f1 z7BZ&771!)p)*%FhgqIk$ZI-%+hsn_ytt)n2dQ?g+9qY5k2m2$=_jX*=p^PZNj~&E* zZCHd|-N)L=$?_ZP4{w#yOo!e7=u5>2&*~;`ggWZ)SZA_Cw;ECn7DE7kLY+oz)i}X7 zof|b8uu!8Bp95VQ4YAkq_9}H#yOmaX#v$)odeRHh=>_uM<+J!WEz9Yb@F+ZvJ;|ii zKp2a2XdL6sR=iA{TMNNH7&yxq1CN?CN)3yqMM{&(FH|A5!JpGVbm4hJi!(lrmWF6D zoN5T(uC)k8US+LLy4!REwD?Kb$QT%|cX-MC{^2nAR;5Fh$S8pS8r^!1hH@pYD zO$DhGHDAWw#?uvSn`L8U$QlP$J>Nk&Mr@t_o1;|2Iqj3_KTNvBG!B=(;42!J|4T_+k9*?JuwUdRk3y-7hw}+FStYpdLakBec5!t)UGxIC^V;tP;(5Rio)!C!Sb3@77RcWH9Fc z^o;#|tsB;~v?;-*tIyc~g;i?~D8W&2fz1p!VEeP*Fksk!8PX_v2H-39u>;lj{=xzl z-4d=CW;}dj!*xRc(9MJ4NX&=&=+Q|Yd=Kdio>C#!1PlQ@YX})a2Jk)c$Dr|yGwxgR zclr&Uvmw$~CmR>m`W#y; zHGKyU_Wcxp3=hXg)+B5FoM5Xxp|YKmbonD{#U=kN2*~OZJ*-&|T~>E}%7^B>3K%%- zRbE1pAg&X?E`2AqRCb083do9$x8X(11k3STf2QCWaP3wjqxafO46C#!Bby}M|5NZG zcplzCA=OX_Libc*ILp$47!@Gi4gWNGkp3>_WPXJ5WmAD9^tn^fA%Hp?z2^=1P5gX5 zSSq?U1s4jxnU|6r@t*vKz5xBkQTVIERNHTq1o56K@bc+7waJw5yw59tTYkp% z2Xp1m#>WC^yf;dMYESWT8hFQSd@Q%@XG)h%=st+bPE0Gi9O$Sw$^eO;s@M&4_?Alb zy!L;RmS@VNrLZrRYA@i!Y=c41Lo9%&XMG-m-17zPEARPj`H%lK*B&9Y5z+G=J%XMO z6!4}LoLoLP8j6PaPRZpewrWNk$wnlL_vj6u=lh;1jR_5?z2JA?HF!0}q7y!rv1Pz( z4?U#6`)+v7wb$BPS}3Vc{vX7%$MG{Xd1l+g51a40i~jl(Pgpl^E>;eD3cNY#DeSW7edL*$ls^A!|c|;eoI|9-;GKTciBk&Pq;}AM?3;W2} z)q(so@JNgvQ=6)6M!QmRciu4+?DUAnWOanhR_FG-W4ZhZRQ_|HrF_dQ<%?cqYaK$i z7Ac>erTj0HC>~9UOBRplpNHDX1l%Xh2aVIbQGZg9fON1REhI%tzoi$G-+E^Ly1<$$ zC~BHDm0xgS4q2L72_4{f^cTu{$8Yn~jKacq9Gc}FQa!rKJ1j-sL3DaS1z2ee$S3@G zc09<^d~T`oGNil$VvSOHhl-U4#UQPke2BiSaL%4BO?)uFAbR(-+?{jDC_i$ppo-fI zW90IyS{M*Fw~X@NQBdvC!gm}h`3^uW0l%X@fR|t&Yh!`0G+ZXf4g&EGnx4$iU&YU4 zgT-E!ur~K*pP>#TeQ=gM=gFrFoK;ARv+(`ma?}s&zQU>@!-h1t4q4qN*g zB%k+l`kK5cCT#)P@=rjONj*l#{0gPrXsdcI{{)_IHEBj@`*x?*9CG9xlfE>DHVe42h`%pUEoiXcVn**7RNjN31?ZqIVIf#C&8a!JR8#_-2*2v%rW&a zx_7;NFVvU>*T4HNxkK*3K6LMb{CluZzBkMJL?@k@e4IWL-@|F<2k(-ZNkLc%cplZBT*UedK@O-D~G~Hx&25e@t$m?~!;(mwfZj zi@j@6F;*hE68jAyXw8-7FDxsOXVgpsI19R@ZRLp#Fjdhvesscs>;s4fLKMl zChw=Oz-1Ljt6EV!GturDm;iwK0O_um;q#P{L>e(64);F14+mc6IV0x)=cqlafL&oz z*(2~0g+ZZVG#zYYC-4fI7|BcBK)0fKd5X&5yyk&uNDCx{M4L6c;Zsijo`T$XRZE;k zwb$q|2qwL}qaV$$W@Yf)-dM7SQ?0mGeMhDsipiUV{;qOzNXG)`j$LJ7L1l@)Gv4ElD^${QUF zZv;tB#R#Cm%J7auW)=!cUeG~xTuqx*3U4>m3g`!rHLq+|GS&tlFe=UfXfssZq=Fh= z$uLS@Lr%;zkowPV@F^UH$0>a_QZL$=BrTx>G{Vv0Gn?1m8Xg=^#Fx*9$6HraY+W92 zGnw0J$!{-*G1vrOr8sF_!v`|3>8-cSb+`Nge(k~wtIeHDrTyJ}*<^}I$0)bM28-17 zBy)hvI#yxUVn$ArVe^L3)zVHj)>SHK#X1e+^h$G>Wsn08!yi$0yn|@c0^v_y42N|geIk++tr0GU>Vt0^@MJ}|ddI!y&bjoyClm8CL6 zoSag<=P4>un*1@wS7Cz}Z<(1U;)+6yuCB?Cm}lSFpSc9sv3K9$p*Rj+;SGX<1$1&zZ>YbmLLVDg-4jfoyK7rtty;i$bv&)= zAF1taYYX2numv7D%^&`F`O!>Vp)x#XQbLXwQiFc={CJ{5r`DUD!R7^H=VoeDD#LN3 zk~ejYI=d^X(t=NS?&xX#O7iy*{|ep(f6K<>QMq`$T=Wm|6Q>aeNr;cd%$v}-lm$h@ zhNKNFB)#gy;zf~E@dB&f+f#xv4p^=$GO^IGFT#Zr`^i-z+hOFR49~AgFWhXa0|0> zP4q(eG3pmXs9l9DByNfe#d6_Ixfnwa$V_hzIP%6obV56jf<$srPV~Z2E#GfRUGCDF zwRe2`;oH!c_*v{SyBVI`9+tZ*0@yja2rzwi(Znx8T3ra{;91 zZb$k$gvZiA3`xQ|O~uK+yt&*m1@oz!MtvHU+cBlm!cNPvTt*eoG&4EmsmrP`N4p^} zwTyLIo&`uH5M2#O|N3y$Vo|#C60j8LS?}6_TIq65krZ;Yk!&pHy!~oZ$eYNK(q@2D zxmlJL>QLL{M^K0O<0ttjLxoWd9C3k{oWC)7h1scfi;rwyerDX~N!V2CXmzkw&sw@ur}o_uMt3H96OC^uChdv6s$$6(|! zvTf4X3Gx}D+C9H>QONBI*=_oY5L7C4vB&$Scu_U9tiH+>4mXR@O}h7!Ee$4H@(p-3b-sI&(dAej;A?+WF^0lftOf3kn9h-vbMOz}d~h zRR?LkpjT*kfu8vJc^9xYok6keSAU%Nx09;4degQW(d^D69ua>FH40tV)oRo3qpbr% zM72w_y%cXbzlUBeE(0T4t`0uRtA8$j<3td!kYhNc-Wi4ZCt&b-696Cz7|7Bb%M0TB zAmCoY=#)AwFFuw0;s@Y1sE5w&AORlGb%A@K5QIvg0OC)(#g9PMOX3Hi`PJYpQ8aHL zeRu=BkoF*3T}A9c>T!B#L7)Y@LB+0jZlO z+hAK3^CnU@<_+lj+(2(2bv~lOcN#Q=K1UjkOjd#u)RofM9WmJOhE4i%uj zirRq>wF4dTDaNiWSz+6NkF5*~4vr7IoDsNyaf=f*_l81hXV@q0-K+=OsdwRzC@rQQ zwV(vR_zc)Of0v7e{R)-|I^7|7HLUx~58GTk(Ox6B%Bs-}$@2gsu!4EuMq+2Ou?Uza{!$IXV9Q8+gxr4!{1v_cufwr1 zcufQKD*}!HLqXa9RPwcjFWA&d*7@Z9H5?~s?7;j@5EOXL%>zvkbv_FiY*jcENG6+X z5P-Uf^zJw+TMw^A+E41mdqcZH`m~s#>~Z zy<^(EPzX#x~$)niu&>Dia#BbQHhepCfsu9pM<+c zyWbRVeDh7P`;Oz{E60z6^zkFRmY;v^wdW6Js(4PI8I1~cwZ-YB4OP*))tOZ^&?-1^ zi}af}#eH`^_Sl_w96vtVw(jUFM{8m%uew^pGRn634YfNqCNlHdRjkyAHYkC z?BS2)@PDP8p%+kUyc$uMGZb%q=5m6*`)+;$O>1f@2dW?Gd{dDesQBlFIZjC#**PbC zhLl0DLFisI|835d5y*>rmP!lc&wq*<+GE^b2`0GTD zC~*SNGI5_y#|}j!E8I>mKy}XK@d*db%O2@6*GOY6MJ3#R_;8=wm08z^zAbeRgQmR; z7xwKaNu4%by>$2uLSQA~{I@prEEFf`83|V)j!%S_B0k!PEhK00mEb@K3PYv@MVI;} zzK_^-!(BC3Y>%|Z{224_v03sX%hEHsYJ+K~b$ZTwIQbvw?duVr)KqX8{_?`ARzDgc6BhWjWBGLC_yE3JRom(|Pdr70(PweD)K>lZi$!qPEt4m~S7|3@ z$Lkej&VdycdUI03DIprP6fX{UH{G?Qe*`3Mys^a|P6Ud|;ZW1i#b^YE=Py2y5dV9; zHj#v*)7t|r^JWX;O$e{xl;BQyn(NRkd=b~@dn*;?$CnCxq3}9o2bxpwC5Q`TMr{c- z%8Qdpix(7|Mf;xmdiZP4JpJ|XSD&3eRR&AH^c;8mIQ!g}@Av`t_~YF7uPdAsQ*xYh z2eLknpm#=P%%|z^EJ&*BJ+wEx=c0@DgwH-ab9Vdo+QaPAPqT;DZoY(j@=5NJ6*FeC z(%d_Q#w{1T9@#q;IJ%k*#Rzm08k%BB8@6^>WNOBw+9JW5aD1Ol+Bbx*0Z+@?hDG&Z zgAyd$2fWSMlZFw4ChgS+YlSzJZvus)6Y0Q8RXpyFb*C0oc)u~SHj(gD^fWH4@ZLRg z^67}ruc!!$kFEy`c7HWhA6HdK{1`*DH6z;GI9DoV*0PwgR0rGOW%wLLSpndt2%CY@ z5Jp7Ebl|i4MO(ovi z!~|)Q)fn$<3=tJT^i*M+D?q>;(=THD_LBZmLTsHr}*arO~|mu2HX0{LAi5ZA(M6DrgTyLb0B)9b=(%YqTP`nB$k> zcOzaT(Yt%{Rs(Xl&vYZf&>%po=$##G;kJL8pxy9Z8YYiiwCQNgyy}+pfVax50zhr4 z@ztmlf?5@EL@inlEbp#uiTXViXRX+F&a#%|U`Y4q!hK77B9&FaR(;U!sdHJZu6nl( z2nsz0EU^UrQ3Jm*tZnjC#C+{b7adq`R8=PNkc0a!tc1tV+|H4WiApvG8l~wG8fYvb zOEPAlVP)vZ<e5oVmFkqMsg)!tDPbk$& zB}6lNpk6#QI;di_p_X;YU@SRu=7E>jp1-}v9TWl~ZwPj%jXp<}-Qo{6x~xrVg-Qj& zN`=6<-HcIHT^HuR5ZW)dV=TKcMzV`!qwt9D*c7g!G#BuYNN0}JfwXLh?M9fqyLa9C{3seoqE<-VfR#+mH)E&ye-R)t-5kJJecx? z!cKdTaVElTdrS-VTs0EOI80;XtNkjg)#OM^wpKNw&jIfxzEId!C3FxI6)$69U?Q?3 z8OkJaq$Ws02hHD+)GA$eHKVlX)b}eQihtgu(yJ0^`b#U7ireh)3oecDXzQ+?1#xeP z1H9I4{?Uu7Y859e6_K3?X%6Y0EMKhB4nkI*nW>d&G=`GpF4VnzAZ%j$hwUH*E9~?b`2h;}5XvFD=c@E%-zH{i_Xibq)A~ zlJp$-0F`OXT0vZ>QNq?hofdrX{U2Xlzx9j#j7}}suVI)37}X^FK%nj7g;IM$;|OHY z@2h1!iLn}qj)DcwNPaRgN;Y%?$oj;E5CemONBo`|leaEz_cU0%HoO0a)MRVZH=cEH z>c<{)usD>Q1Lv46G1Sy6O@hYgviW@W9akkX&Jk-hiFHmwEqZrP-Y&q+ft=t=Eq z>q#>1gFi@mk}a(#y*WLpRAlw!3rJDklmP#%W{{%Xp1A1+P$m8x@u3;0Aj&YZ69JQEF9gHh{ZCc2Xes3aibK zn@K%1F}BO^bB046 z=gu7~&sdT0>wUV!^8r7b2|In8pa0P0R)t%27wiRhHFWO1TM_1})i&SH2hOv!Hqs#3 ztQ}wKs)#t8k&F$n-tqY(8;z|g2$D^X8$2;p${9{+|GVvq*M9@x8%k(9;T-4^PekfW z|LTps4+Wr@#7vQG{yE39=2IHah`dUUXD`k~kB>#!#P2e&+Ek`Tul0 zt48BlWf|jH}D|zZ=i~_uKu=&{6)l=J-6uwf~(qztqP*%eZzrHvj33 zYmkagJ`Uc64B}r)4u6v@$#qKpb4fA~Wx=8dcVjdG%SWAp#*Y3r@UGyj7_7D1RIEv* zg{pxiM-D5=p>PrQ?l&`zB`(kEAj50yF9s4;HLN_3u|)$8y1`^rC9V5LHe69_G^)a? zMc9kqZ3K*XOu0nK0w?W@wEFEk6FFQI_&$7^O3AriWqKrF6TGUJlp-yP!J`njMtPyS z5&aClA92{CHZ`YqR5%WvHy+-0I17f77+qOEQk5CYw29w$MFz%x;ecB`0l(L8fSkFp zVrwD`dol%oQq>vKhe9b%+1|csry(H6nP7TXB6<^3E5Ky!v5>SJVw_A}Cwc^~M3gAa zk4gm9jjn1~FcNKw)c7k0R@?->(}!Q%>QLqr5H!rM^@hWqgx-jVU^wxN9M3QfK8#5g zm=!!GNl(LyAymvji0ON{01&1K3e3a>#c*FFT$?W;TH0`K(IOlvF0~@;0eOO?zl7*h znFy@&&P6zGZ0+2%#1u^sos+FY z4b_T~N2hbNXcc7k61LJZ+3a*&o-bv2Emxv$@yfcTwS(u&Oc$;0u-;c6cE05R<0S-; zSC!77zM_JN0E__b%56g4b`|gxS6TBqfO$o^n2l)OO zK1gLqUQcO{1!*=p%n56IljK>?7W1r+7(z7t=lH}=7C*eh)e%{@-{N$bl-!3S@Q^RT zC=8B@me`@r-dG}0(bIXfy1Uk6QSzcca)4~S0Q&!`B#?Pd1Dg_m4pujKoNDpcvKRe? z)*oz3R4l?9T0~xv$&<7f`~Yd=AqlaI4Bv7{6Lyl%Ob=`F0U5iCxYB!>oG(3wS>w^r zh6ERr1MSkZ*Bo0|f6hSDb)k;dP-kPjf9392kOVb7y1R>`*`76<8yeJR@ohjG+^v-r z1Cnn&pf%g+a0wZo+z+)&kduGwo;F`y2zyhagPjvG40d-(*s0e_f>8}fz(zM;v2Z$K&j z8h)dA41Zxz{Gq}aen@qRZ_x|k6KHn6Am4X8Sk!fY+U`i!VH*_P`p$W8KmF!Ak4^L1 z?%(5UZVH{>lk?m@K7Bh}a^imJ_OJXP-)XxphTXJl^+8MvMV)K@P2Bp)(4zxA(xrQ%3e8mB4(4wdb+TEMCjMdk*`225TYqcR{RDkTk zzGLSt99yc+MePL0_VV*%7?qAQyQN9wF0lLCg?6IF(0 zU)zd6G;&4M5;p7g=CCDuAU@R57EL#ywuNyHQ%Hxl7J4H}6A`2rgK49Y?c1@f~?S@g2F`k&ud^cDoGe zK9V0>#%9AN5pS8kR}Z36LSt$3Ij92wqx}^J6`_|!&bp#9V`S-{Y$@2e-^VB%f$;8* z3d4Z)L!ecfqaN|W>1`I(K~T{^xAdoZd-%_RMpReG;uGUb;$0g4!D2Wi5%DcJ3?Gwn zffmJ@leVpABFpi`9Z!a3d^vld?}7t(NBf!Sh;qLyLO+I;GdPdrZSiMrDL|5&Nj#AS zycEfz^j?2)5{vEiKQ*1GV%z-U*+u$2q!Qv=AcA-}bFaTRk8*qc{Y5yD-R7TBVL&a0 zec(@WJbo_boEuVu9yET%$y|gE1oN{@Cb98D7d3uj z*=e~n6J++p#rfWJXZVpb(`lRbuA9H| z?Rcw?_{N{4*CJaehP2oGd|N1*9b;xOdZZN*d4HUt*J8WpXkY(D`!B!Zyz__V@0-KS z*$1_*Ol&x8w`toWfqIXTgT&q$9=`aMqXPpaj2o*X*dFyZ#h$Eohs?1^+AYT@yhyjh z_bCSz#k=`(MulJ;*{)fDl#C6H5t2_NHr3F`8bK|c#DI1{y8YEhcCF|H8a-@lns0yQ zd%MTF{OyYd-|gPey2Ix&&<-1T&=e5w>DYXv%Vwj&vO8X^SgBOO<)NLMQg^&kxym+P zfA=GqL_B`xaH4gc@Aso;*NVFIxNl916oYUJ^uh{c2l&Z4pG03QH<;&$!*hOHCPgOH z9W?Y_enDHqptrw0dBuU&)PT3YIrBg;S<&p$LC=Ku`vG+5*21?p9x?1U?+xt8Z2yAs zJo8@fS^I3Ba96~?Gko1y72=ClSFkMGH>Xo*5p93M8boE>9E zc8}t^Dx34e;Z@VcvVAB4&FMUr;;BCRrfUQ__lMeBXa!pm&+cUYxP z5G{{A1cU@3hZQR!R0p&IEgpZwNF=@`N)W*|S?mDUw%y|yc`|Lyv}3q&b*VD z;1d1e%^ddVviHv6GBv$V4p-=w_swCSbosW!m*`gcv%?$ovHZi~O}bP0?(hw|=Kt;R z7O}ryVB~YzKcjt8G^5v)QA`txNE1_&>Ug&4{yE+O+M@vD5eXX66j2MxF~`^wl+xJR zj7QgwJ?l~U8lSsZr2iSXwc@9R2Omg4}W`&&@R`)F90CBOkEnvyO z%Wj<^D#2ga__1X&85u6djUphXM|47curM9kT+B^7U>L!g+DN7<9>!xGBuy&_W~Y7an1{sO&N106De{z=6O1n8S6;$qm%xC4^^tU6OUw?ORiR#b16B} zdbV4u>G>p3*`yZ2C$A-iQ(iN1)x)Q~LACRE|De}D2=pt>p~`qHwTP1(7wSMCOwVxN zQ>z=M2J}UpEV-tliB5Y9`C4E9KP`MF1q)a$MG z$&(kocBgBJ8}(|z_MdS-J3n`NjGcPdQ#r;vytos=OG5?5_srhR*aFygU^NF$!DoCz z>>9-kU3>uF7Coc~=sJ3imlfk37MD2gI}j;7Qt25tf_l)}X`hOzR5}cjcnEvYXgz$e z{XcJayw){E7lr3Nqt-g-ci#vTGXh#xW~tz1OnBa78vDX3+ay)=t+0KS*$T|pSW;V^ zqLZxtkF7W%($HvAS$VE{B9|j=DrF>QEJly4GclH@0_ql8XYp`uT~ehX(?Ws5o{`#b zm2%&S^nxm>BXpc{SD_`+LYZW%$*XEM>bz3auaf5Vx}S^NgKd@=Tn)cN9wUQZ_Bqk~~C57US3%4n#sEERdinw>tp2Ld21@%xDT@7~?LyL%G=0`dEGGVBgx{Mao1Kp%sak4K{!cVI$ZWHi1oHGuRxqfGuGw*c!Hh zZDBju9(I5oVJFxbT45K6K^wF~2TX?ttm=6nJAuNJENWx;+2l`_IJgVW zfsf#LI2}%bbKwfO1R0zRufQ>IHr8Q1oQ4575$=OGFbL8x1Uv~J!w@_RPs1~CEo9**F-s05`;qaAVvAH^t51JNOUw+za={ zIL^T?oQnyZhuzqNy*M8i;6k_#uE#~#he=$F`(QsV!2wL+zVIXb1n&!ezJ$O)Oy-UXNA-FOfF1OJKl;(d5OK7bG6L-;U0 zf{)^3_&7d+PvTSfG@Osm;J@%$d=8(-7w|=V317xn@Kt;bU&lA_O?(UA#&_^td=KBp z5AZ|$2tUS8@KgK@KgTcdOZ*D|jsL;_;@9{MevALZ@9=y40e{4w@Mru5ewxjK72ilQ#qMfOgcA*%xQ9E_ebecgkX%@|E~Ja-V!DJbrOW8AbU9r?SJGAVH@cdx zp=;?nx}I*J8|fyxnQoz5={EX1-A;GVopcx7P5010=$~{i-ADJ+1N0z0L=V#=^e8<> zkJA(MBt1n>(=+rhdX}D}=jjD{kzS&g=@ojFUZdCP4SJK_qPOWCdY9g#_vr)rkUpZ1 z=@a^tKBLd+3;L42qJPtW=)d$eeM8^U|L8mVo_?So=_mS`exY9(Fl59St7G*nz=EuS zg;9xIR2K#lP8h1BfrRv^3KHq|1(o#$)$RGpy0Ec3ZRGQc z!(@F-q{rtc=i$1ZLB*`g@kgMytmzpwpa~M}bu!VMOnPM|nk-$f6SF2M>s>LQX)9Jn z=KFeC>EXr&{5aUr(>nlU30LsawcXuG9G=&?oH+(!S4cU+~-a zN&A8zEK$fZwU7-aoh%I}%cRWG5G0z+S%s`(T6tZu%2C751^Q*brtG)h->>QKw_o;a zO0q;+l>~__@l~tj;f8^}Y?%c$6X=%xWd#W)othcW%3Ud2yJab|vJ`Q<^b#sA48{|ZDyp0c$&#*= zauORJ4#hKrx(FepxG3Qw)kVE7(p;2uk>R3#7nv?fxyY&@v7NTJ&30&~L*oveBWcI1 zkm?R*DV-ZpqIQNkMISP?Y>ux4htt)evM}k0#T?q^&~}G*ICQ#0XF7D2LuWg*)1h&P z&T(j$L+44_=J;=S{I)y(+7k|S^g5h)Ivo9u_WHP9%qewq)Us0Ft?-2$OcYBRpSpF+ zxrimWcp%47T`!;4^$V0@QQ@0delVlZB8&Pg+E3%#ie@iGTCB1DoKe3-%jOj}pjg3_ zbj23uG}guci%VMFppq+x=JHiCsOrW@C{sbLctloZsaxL>7cF-^ul4m& zX^p!w#qBqqGmK$n(BL-##f2?#Zxa=rwJ5a$pN0s|uOS4JeHyOg$;uc)zb|om_BvKI_X&(UUbRY)Qws}vbGjRZ7q^x)xsF77RlOL7`3%Xj!_F^j9Q56gFIOc z4+^OWN=oHbq&$a~2UA{XmIqT_l$IA| zsya%mI?9w6rR7DLs*2L`bAe@YGaHqs`!YA?Q8(sgp8HX6tt^vkWmH};`N7y8Yl}Iw zt%+X}_?chk7c!l{H-z}RR({mfluXoAhxqnaNb|QJb2y?E%BpD^g9^WA3)7Byl1`7c ztg`sEc9375ht;xuIhf!Vh`U2WhTqH#t5B9UGp0w=20uCT{GwG>%+WesGnM*+!k=t} zpUoM5wX926rcqZ^OGel}Lo}W>PoiMuRnrI+twCKY<@g=WZF6Y5LpvIWc(`TFD6}eiIjWR+wpwYpmD}bTc+R@2m}#Dc zl2>_4NJ#8d!vcvMBZIn97%q+G2Mt|#o!;INHi~Lt(9(5PWl zN??zGuXxQKE8Mc76~iGh&05n+G2+~}Y#b7sr;VnSib`5t;oP-W_;jX>*%I|h+Y?pX zJg0}5o+#0e&@LQofCZ0HW6>MN)qw4jOn{$T;ClbaeepItjoqt-p;d*)An%- z=e9Vm+Q+qxjcXelQ@?Xu{mwD~Z^wjcXf=x>riyxXu!5L2QaXsqj)&gwAz}&>8WywD@13eU9eTnsu~? zNA2y2rAP7+{#QzT7&59E=9qV- zl@-0Q^1f*s)bTl$@g^3RRL#$h#)Lm2)yJ?UE0#pE1%7s!rdlp&A=hUxYbmBu(5i=u zH-vMe#hfb29&upNG>RdKd00^w**cBA{=PLI6PPc_N6QoD*tzn$S@qD3AU zkF%Jw{WtSNw45`nl2XX5<;PX?D$6L_Z})>ri4R%6hA*{L@w_8f!LB$ra23RfFovKs zQ3y(WV%0biBEdJAYF4w)H8o=g*-)~IMLsyCma?YGMdgg+Xs3i(AtHPgJ4 zsq2bt%_crIH2K}J)xMOd`HWH$bqS>^hfK(-py+)6mJzk5nas&vLf&$>M^^I|SESd& zp&ob3&$(!!i*y$axTw62Y<4hd8d-i(2pW=x?SX3*-)=$z+7qh5{$_21u>4*vCfSf- znGVucOIbrnTO;*Kd?m@r7*<7ELb1T#Qw3@fuFHI`>zPuSFR*6&i?^S*d-U-WWXC>9 zi|D^fV29!NVMpWlA%pTuWNdyXB1E5{fscjGXUob4!-;*82xS#tl4bjwkr?IXjG^m% z7pYi9MSc}de9m_nJF^PoeE)aNL{1)OC*<1M*|>Hx6_=HHxP~GLmlG4I!tra>5dUlZ zuh({}RDjZMGFUd(6q9H;I%g>Gi8ZL~AZ9r@!cIbkWE6jG;UvX(V zz&TfV+y4O;6uWc)00001|Nj62c-kG!F%E)Y5Jchk&sqr$6&893kO0Bks0R=VI!*M- zDm4?bdC6DJl7QUJP81}r?&cGBp7x8k{o!l>_}hyrl4_li8g+KB!M^BWj~eYM_N2%n zLy<>mAL3}AqV>*h`^E~2V+?`-c-oCrJ8WE45IuM1dDr{AyWZ@_FOD6vG7&-uEDJ~! z5Jf_Q31J;umK8-QtgO6z^LZVhvfCc) zv4^|-JNIRDJSI|Rm%Y$z#`73&#UH$f3-8V~r!hsfiI!n(IA^#R&wP9tU&XW2F}|7g z`tlWz-_Cpd{H3x;a8&h+$7q7wb-i9K)jjx;xqfnmllYgj}#>Ub8z zIF1uIgK=EICA^O-Silm##&UY%f}Q4YBU!t45=-*QTF$5Pr&Ru#NF7Z4bSjlgsl1WO zTdBOC$RMpSSY*Zrk<&c?k&E4BH{)iRbBp^qyXBS}a8J2G_q66UVn%0J#d%E8<3ncu z2^R4s^S_4c_zpkdCVs{*_!YNt2fyJi?%{Vlz#sSvf1~Z-GA{4R?jU0xanHG7cg(%! zUWaNy9?CxuEow;YmK9=2R*45>jaZg8u_Eils%#KzvPtZbEn)!`&qB?!(BoN%Jd3rF zg$W_P~Ar5PSu?M(UM6lBfPSd{~ zpnjISAzmgsM41&0;tNhpnUzz@o0Om1I`We1$X-j4GnQDFQmdcTs=q15imdwu-hH>0 z^njMNqE)SFk4CiawH4F%=jS@dpR+oI^Uy+`H?oC-Rg=c1M};TVxQ^J3Ugq3K|19ec zWkptHP1>?98?s4e%f1FaDrB4Y;%w%+XdOazKgNDp(_7NJ^)bSPP0QXKs@#V<6;%zi zLo=GS&Zt!Zt0HGT30dV4d;9+7bHcQr+FKY?&J>YT#udlgaKL(0^_^2r9>2j}>q|9y z(*HkRd>eunOye8CsChka-cJ_m8T0D5&g2t6`G|OKU+;N(m08aa_b>4k8w=Bvr##A0 z&yrWU?zVb4XYvwrEBT*x(MBmRa=(;vuNwF24)^$u_6oIQ_ALw;t%N~2nylr4-B;xi zNnN^Lc-rlo32;=`m4?r`k`RbZ0*OTkkZlY|Xt7wtVv>j%OguJT0)l|dWHb|pn#>TK zN~Tgys!~O4jL9sSnZzL}I|;Y2yn<_HVr&*~4VcaB5LO#_tZoB#2u@-glXSlO`nKF^ zNk}p_HWOFZ*XOxA0s&$!#|{GwsV? z|K`^v_n*J=joFg_&98pvD^eJ~BjK}#f9Xz1Ca{H30j?JMqXJ%*mXI5t#VZWb? zoC}^ysHX6D)9%kB|^;=lHo%e9a-M@ixX0>z{1iI%rOe{Y0}ttRZ6yk$+^+Fw)3jH zRq2z}bISSZNWC7E;`=kQxe)93XE|xV-bi+pYx=#SU;5@36!M|Xhz2jkNV!}HQu1Lh zwz14A!{!U}-sk<5%^%JoF8MSEYUMqUy>=7jyqDd|*{1d5_;g3!x9;9Xm<*wOcJZWX z%gAtKwlHw<$IJLg-&3#kbDeo|Tji<#Jf3ldJ8L&Svl-0Ban_<-{@M}@_VGBZU51X| z-9qxbiw5<4r(R~GT-E~0h6=P-%xLjLnV;E;xkrI2m;#8_WB{&B zO^%M{M)YFkLq#=b%4Mrte26?gKQzq)?6}=FhBdO)knYG3{g5F=9hgG&H>A<9_x0mh zVMSjzWH4(~Vg0);ePrsqJ7Yzb$f`=+@E~uHLF8<*&F+lXoW~KUk@iPh$X4f5TjoMF z1&4=hU5a_w0T9M@s%3hO_APQ@He#a!46 zB`F#qI)51|FQ4IZ-f+{wCf4%?Jk<)`59!^%8!HCe~*77P%E!hOz~ZZXaVd1aDYAS`t^0A^#|)dr458+@_41uIv?4DJVFMO zWr<3kmx=|SNCdlfo9O1ic1DvvjI9*!2fLzjV7dPf_ad6%ahk*v@%`XpKf~xf!MPgJdeV-V{5oPTlEc)<;v zMzD4G(9LcJF;j$pe$tDQb&h~qsQHIH1LbT)e&#!4&X8#J>?PLL_(;GP8r>O!&DTx% zx%FG7!&aqQeoI!#g;Vr9){EZx{DYvz7=^K{b93Kp$h$U+ouT3^K>(&y|IbkQ0jf73 z5HD-`P+rg1M&+VU3koXf#wDH|AId*AoUmM=uTag7)!5-n@ER|92;ki z=-?a!TZ!bfxO^YAZ??K7v+zV`~=y)>oj5+L!q3~BW4g; zfl84QuQlg8=q44=Hm|>UxrvCCnQm3;55lr34OtbJ|=m&$Ho1dM|qxY@@h-aOeot@|MVj(3!Rg^RQ4`rmbKU6P0H{P1v@o z&K4Gyv8@??qLTupg3nW7Aqpuuohmhh%JW+ENr&&}+Nlrst;NXn<;(o6&+MYs^yR3m zPw20V9(n1n>%LSJQ<7R0M%XI7PFqiXyxwUBi7e4HB+-}fGOT2IY?FBhEST2jjT`aY zq{+XN0khT|PuBDCc@{YquQv3?1|M+(sR4kfig{tskrg$ig!Ur@*jH985R3@S!o)OK z)M6qUCh(Zx1`9mKC!C{dV|(9?Z}Hh`5n$n+Shv9Y837yW9yKn0n;It*Hkf!6riKhN zofWA4aiy^L+IM?PNIyZ`EXBU)CnDK2MzeS8k%otKwqGt^4xL3)Hl3(jX!EQUo-WJqP#cU!TC`@()E>F1`(~Co z^}BpIRI&PU8eX6OwI_AT5O@`3w_;fyFm=3IvfNow5}e4XFI@sT{n-{29@c%{YclzL zMy^iMU?V0QF}(Xy43%#0D2e@y)ud31`Ye)Hu*LP66eq5+P9G(ElPXfbV&L(>oo!V& zXwP|`BLvdmhIk~cWY>c(zx!qm{xM2R{mC;WkL~J?5w3s0g#L~CQyjfaA2R(*R0}HJ zyC@8LzuFAk@}^@B#vjOk2s6G4juYK1G%rsR{aGZ6EGkKXY7xT@2t&fYi$FJGY?HQY zOh1fY^?F7tkb5o6GOfA&0XDC=t^;1q8NfrXAhx^&T|EF5dq~gn@RlI&sCXrmTs_Ac z_ix7>6a8xNFBtjZo33&rVm|qo<8AV-bTn z^CvoJ-ot~}$oK}RSQ8!^6c5#g4%7}e&Tz#GDao+`Am7SZlcw7z+jz*?CuIr2LIIPO zuO1J7vTJbM;avS3uV{|pi`dhw%$yN680mXS_T2S!uMCz9fJsXT)x7;#Wr1+GG#!wb z@`B*yL+}9J5?Bi-N#z93($!}X;7XoOn}Ldq;FvGee7)pVW8%k9eCrSEF!CJtJtoIQ zC2{|qS4yJs-RwKk%0u8i)n13X(v+2@;9xathf(uxR|^5jUYLc@nqz@`lEf$dnqJn4 z?phGM)Zji|X>e-zbV2aF=7_Wg_;XaisKKN~!@r>YV>iyR=`_eP*5TWr{32L6r-CVZ z)f@CK6;`xZ4f{Yb%`UInzc-!#%pc=94yrdfq*zaXpzc3!F2Sv;DHozSv&RdNdhu%v z%EdT0)7c>tBy5m^`RlE8vZs`uVzSF5hV|l#EuF3BJksF@nYi2O2cFiyn8d5B_hh7Z zPX?uD11581*n!(q-Wr=Y_4L!5xM*e?IaLGg&EK*(;U}CAFib9WE=_05frk#$7d;ny eTU%R)OOaUJQTc~^m4m>8`@EObbg#Nzy7X^Q;K!=~ literal 0 HcmV?d00001 diff --git a/marginalia_nu/src/main/resources/static/fonts/LM-bold.woff2 b/marginalia_nu/src/main/resources/static/fonts/LM-bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..c14c6204f5f0252f986b7c4f4b1decfdcbdcecd1 GIT binary patch literal 33548 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 cgd3f5#5c`iRRAav_>?$2VCQM2RO&ZnOO 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 z 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 literal 0 HcmV?d00001 diff --git a/marginalia_nu/src/main/resources/static/fonts/LM-italic.woff2 b/marginalia_nu/src/main/resources/static/fonts/LM-italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..166d6e60b8ff0bb412a7010cefa255ffcff2523b GIT binary patch literal 39148 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�Cv-G`xDidpv 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 Ju)ADy{~xO_x*Y%j literal 0 HcmV?d00001 diff --git a/marginalia_nu/src/main/resources/static/fonts/LM-regular.woff b/marginalia_nu/src/main/resources/static/fonts/LM-regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..eb9fec0a84425a3ffa3bc0e73e1db1c9e97ecfde GIT binary patch literal 53760 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

      oYy2k%lR;3 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 R^?-IVc)I$ztaD0e0st56LgxSg literal 0 HcmV?d00001 diff --git a/marginalia_nu/src/main/resources/static/memex/ico/doc32.png b/marginalia_nu/src/main/resources/static/memex/ico/doc32.png new file mode 100644 index 0000000000000000000000000000000000000000..5b9f3731a8650210e1a9308e30fe3c60dc128cc1 GIT binary patch literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}8$DedLn2z= zPPxc+Sb@hi-_5?tU;~fLyY=tq&AJ(C<+`}`gM%W+w&{~i4zMmP-20e~;T~(1I7frZ zc5b!OFEz{z2Od|ldv40|Jk+Lac4on~jJ{q&53k;AJ) zH@&ldWG`U#<%tMPRVcSR!+S+yw(*3$?7P@rWG4L3l9NmL$9nmIi2-j+vEbDSS@DJ3 vSsJ&)8{o>!YizxaRu<wHFDk+i&I7xBYv5x=gCz~JfX=d#Wzp$Pzl&t;$h literal 0 HcmV?d00001 diff --git a/marginalia_nu/src/main/resources/static/memex/ico/nav16.png b/marginalia_nu/src/main/resources/static/memex/ico/nav16.png new file mode 100644 index 0000000000000000000000000000000000000000..bda9f33f1c7ff0bbc876dc5027bd4a73408d7137 GIT binary patch literal 301 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1IggaDrqS0JsV zVB)G_lV)kyA7*{OQ0?Y|w0DPR{r+S&^JzfVJ_GY9dES}&LEb7xHkS7GB?bvi(RmM( zXaA2s`{eZPKmY&#pS^Z_OI>?rR#s$KP?=NL8Y82d;mOylclA&A>dH6xoxJBf&<=%? zAirP+plW R?_8kK44$rjF6*2UngBQthwK0V literal 0 HcmV?d00001 diff --git a/marginalia_nu/src/main/resources/static/memex/ico/pic16.png b/marginalia_nu/src/main/resources/static/memex/ico/pic16.png new file mode 100644 index 0000000000000000000000000000000000000000..9c5a562e21212e64fad60cb434ab95a4043f4903 GIT binary patch literal 296 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1IggaDrqS0JsV zVB)G_lV)kyA7*{OQ0?Y|w0DPR{r+S&^JzfVJ_GY9dES}&LEb7xHkS7GB?bvi(RmM( zXaA2s`{eZPKmY&#pS^Z_OI>?rR#s$KP?=NL8Y82d;mOylclA&A>dH6xoxJBf&<=%? zAirP+plW6P)Px$l1W5CR9J;$Y^iJi&p;ZTkAV;$}!Q@W+YJESiyiT zv{~ro07gc5=>KP6V1RSz5(3qY6%7CX|7ZCBA2YepEedb}lqgthY)nu?@Y%CebpXjB z;NVa?unwTMBgjqz)N%w>v!TV*d=&nP?1=>RP%RgrTaHVNW)4USe1swP^Na{VeN>GC zW359d{9|W=2H92w;r@avecR8Zby``p>|CCl`?Fh%~A74B7h);e2DQLkv^1&ttQgfdSGc zV8H7Da==taObd~LVPZ3!ePKZzBh?*1q{R#j^lT9!TYi3iErvYJLV%FP7!E*g4N={Yi!K0*4P-s5!jk*=xP`l8DXiMpv7=KEp_ex(K{x@v<=Zg$hmWHHn!F; zvLhLg5+T+!KuQ!KI~JeCxB$h85aw8NJD3CBFBotKz!C%9y69;3G5`SoV|=4x1dZMR O0000?rR#s$KP?=NL8Y82d;mOylclA&A>dH6xoxJBf&<=%? zAirP+plWzIBxPg S#TfyOX7F_Nb6Mw<&;$Tn<%nef literal 0 HcmV?d00001 diff --git a/marginalia_nu/src/main/resources/static/memex/ico/shiba16.png b/marginalia_nu/src/main/resources/static/memex/ico/shiba16.png new file mode 100644 index 0000000000000000000000000000000000000000..e41d0f608fbcf7431e7a13fe264e1333eb8043e5 GIT binary patch literal 316 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1IggaDrqS0JsV zVB)G_lV)kyA7*{OQ0?Y|w0DPR{r+S&^JzfVJ_GY9dES}&LEb7xHkS7GB?bvi(RmM( zXaA2s`{eZPKmY&#pS^Z_OI>?rR#s$KP?=NL8Y82d;mOylclA&A>dH6xoxJBf&<=%? zAirP+plW(di$YZHt%|EYY~r=evPMW=8HH;-BMfeFZSFQ hW~J(ot4!Z(8MAs>?rR#s$KP?=NL8Y82d;mOylclA&A>dH6xoxJBf&<=%? zAirP+plWgE{-7*Q(gNn@*Pp&V0n7h`8-Re)35*bw^BW|^R3TyZ9Dey z^{2996CIyC*=yc#a(3zS8+$zeEMMI4b?TAQo>S#Zm=^MIGfc>FFqF9bPy677EQS)M zvwY{%rM9X{U#!e5i|BYTS4H5%nw`7mEN3@c^WIqLgY!P0FUQ!0%edYzOIbbx=m-W+ LS3j3^P6 *:last-child { + background: #3F5F6F !important; + color: #fff; +} + +header nav a:hover, header nav a:focus { + background: #2f4858; + color: #fff !important; +} + +article a:focus { + color: #fff !important; + text-shadow: 0 0 5px #f00; + background-color: #000 !important;; + outline: none; +} +article { + max-width: 160ch; + margin-left: auto; + margin-right: auto; + display: flex; +} + +article > #memex-node { + line-height: 1.6; + margin: 1ch 2ch 2ch 2ch; + padding: 0ch 1ch 1ch 1ch; + flex-basis: 60ch; + background-color: #fff; + + border: 2px #ccc; + background-color: #fff; + border-left: 1px solid #ecb; + border-top: 1px solid #ecb; + + box-shadow: #0008 0 0 5px; + max-width: 60ch; + overflow: auto; +} + +pre { + font-size: 8pt; + overflow: auto; +} + +#memex-node h1, article #memex-node h2, article #memex-node h3 { + margin: 0px 0px 1ch 0px; + font-size: 12pt; + font-weight: normal; + margin-left: -1.6ch; + margin-right: -0.6ch; + background-color: #2f4858; + color: #fff; + padding: 0.75ch 1ch 0.5ch 1.5ch; + box-shadow: #000a 4px 4px; +} + +article a.doc:before { + width: 1ch; + height: 1ch; + content: url('/ico/file.png'); + margin-right: .5ch; +} +article a.dir:before { + width: 1ch; + height: 1ch; + content: url('/ico/dir.png'); + margin-right: .5ch; +} +article a.img:before { + width: 1ch; + height: 1ch; + content: url('/ico/pic16.png'); + margin-right: .5ch; +} +#sidebar > * { + padding: 1ch; +} +#sidebar h1 { + margin-top: 0; + font-size: 12pt; + font-weight: normal; + margin-left: -1.6ch; + margin-right: -0.6ch; + background-color: #2f4858; + color: #fff; + padding: 0.5ch 1ch 0.5ch 1ch; + box-shadow: #000a 4px 4px; +} + +article ul, article dl { + margin: 0px; +} +#sidebar a { + color: #000; +} + +#memex-node img { + width: 100%; + margin-left: auto; + margin-right: auto; +} + +.toc { + margin-top: 1ch; +} +.toc a { + display: block; +} + +footer { + padding: 2ch; + margin: 16ch 0px 0px 0px; + background-color: #acae89; + height: 20ch; + font-size: 10pt; +} +@media only screen and (max-device-width: 800px) { + header nav a { + padding: 1ch !important; + } + + .topbar { + flex-direction: column; + } + article { + flex-direction: column; + } + #memex-node { + flex-basis: unset !important; + } +} +@media (prefers-color-scheme: dark) { + a { + color: #acf; + } + header { + background-color: #000; + } + article #memex-node { + border: unset; + } + header nav a { + color: #eee; + } + nav.topbar { + background: #222; + box-shadow: #0008 0 0 5px; + } + nav.topbar a { + color: #eee; + } + nav.topbar h1 { + background-color: unset; + border-right: 2px solid #444; + border-left: none; + border-top: none; + border-bottom: none; + } + footer { + background-color: #000; + color: #fff; + } + body { + background-color: #444; + } + #sidebar { + color: #fff; + } + #sidebar a { + color: #ccc; + } + #memex-node { + background-color: #222 !important; + color: #aaa; + box-shadow: #0008 0 0 5px; + } +} \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/static/podcast/style.css b/marginalia_nu/src/main/resources/static/podcast/style.css new file mode 100644 index 00000000..1939da58 --- /dev/null +++ b/marginalia_nu/src/main/resources/static/podcast/style.css @@ -0,0 +1,3 @@ +.headline:visited { + color: #5f3945 !important; +} \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/static/smhi/favicon.ico b/marginalia_nu/src/main/resources/static/smhi/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a1136a7f6ef20e65517dcc7f46705e46e7fff3a2 GIT binary patch literal 1211 zcmV;s1VsCZP)EX>4Tx04R}tkv&MmKpe$iQ>7x6f>sc5$WWauh>ALD6^c+H)C#RSm|Xe=O&XFG z7e~Rh;NZt%)xpJCR|i)?5c~jfb8}L3krMxx6k5c1aNLh~_a1le0HI!Dn$f_we!cF3PjK&;2=i)U3q-pGZ8*46{PKK|Hlt zF*xrNhgm^ZiO-2gO}ZfQBi9v|-#F(T7IZL1yduQB#x+>PWeLG zWtH<5XRTCa&3p0}2DAFgGS_JiA&x~XL4pVcRTNP|1yNdcQY<8CKjz^dbo>&z6mk{8 z$gzMjG{}x0{11M2Yvm@!-K1a)=zOv5k6|FN3p8rB{e5iPjT6BC3|#3gf4L6Ke3D*k zX^|r!v<+Nbw=`uBxZD8-o($QP9m!8q$mM|dGy0|s(02=TuerT7_i_3Fq^PUJ4RCM> zjN~bM-Q(R|?Y;ebrrF;Qgrah;V>MW400006VoOIv00000008+zyMF)x010qNS#tmY zE+YT{E+YYWr9XB6000McNlirueSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00NUqL_t(o!|j*NZqrZ@g}<2_+i_Z?l)ehE=%OM8u|h)p zJ_iWVP1U{ut<=|G(JHV41c=AzGf<(Xh5Y#X4vV->LR?gmLN`6qY@tJduujTlp z$7MpfWL!0H=?&DXF9!g;_tfKb?5a$xPr~p%Bg-<9B)u30mYPjV8&O#ci?yPXimKc4 z^0Mvh>|E>!jyqj?U0+mQl&`3aqoX6t%(k|+Xf%>}MxX`FhB!F<{*8l!11t=wAE@HY zF!4mWV#Rxi2I9z(#Mi9*vnw^{@VU(|!e+CHSzvF!&E9^ShItZl>LxffoDLgpweH}Y zV`F23-Q8XD-s7D6H!gUy_pU4ri|W7gN^x8vtlzHbFf=E&-P- z$PWM#um2{B#e^VL2{`imYU}Rk!Kz?>)*P6IGXWW@|970S^7Zq_QCrDeK~%*+gyE+~ za2C4V{>6^KB>&5#82UbnYV+m5C=UY0pGPnb?=uh~%LZec&yfQGMJhH^Q$q21UTxhi zbr8q##b}@;PO>xdVdRW)5ehL>x@JZVWUAEU_LU&p`Ya6UL^dGK z6|X{`=Ojsj_e0<3p+^W|9v6g?Naq}BnsU-P!Cc^aFCqy!Y7Vag1e_DRACzi$nqXuQ zbS#Q>HU`pq{rt8QoBEVC8jaaU(Chc*luA`cmibDyhKCNpDy*)m + + + {{service}} - log in + + + + + + +

      + +
      +
      +
      +

      {{service}}: You must log in

      +

      + This is not a public-access system. If you do not already have + the password, it is not yours to know. +

      +

      I am never the less legally obliged to inform you that if you + were to log in, this would place a cookie on your computer. This + cookie would then be used to keep track of your status of having + logged in, and nothing else. +

      +
      + + + +
      +
      +
      + + diff --git a/marginalia_nu/src/main/resources/templates/dating/dating-view.hdb b/marginalia_nu/src/main/resources/templates/dating/dating-view.hdb new file mode 100644 index 00000000..b6a5a997 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/dating/dating-view.hdb @@ -0,0 +1,150 @@ + + + + + Website Explorer - {{url}} + + + + + +
      +

      {{url}}

      + + + Screenshot of {{url}} + + + {{#if back}}⬅️{{/if}} + ➡️ + 🔀 + 🤩 +
      + + diff --git a/marginalia_nu/src/main/resources/templates/edge/browse-result.hdb b/marginalia_nu/src/main/resources/templates/edge/browse-result.hdb new file mode 100644 index 00000000..d2be3661 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/edge/browse-result.hdb @@ -0,0 +1,12 @@ +
      +

      {{url.domain}}

      + + + + + + +
      \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/edge/browse-results.hdb b/marginalia_nu/src/main/resources/templates/edge/browse-results.hdb new file mode 100644 index 00000000..a6a8b0f8 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/edge/browse-results.hdb @@ -0,0 +1,49 @@ + + + + + Marginalia Search - {{query}} + + + + + + + + +{{>edge/parts/search-header}} + +
      + {{>edge/parts/search-form}} + +
      + +{{#if focusDomain}} +
      +

      Similar Domains

      + +

      + Showing domains similar to {{focusDomain}}. +

      +
      +{{/if}} + +{{#each results}}{{>edge/browse-result}}{{/each}} + +{{#unless focusDomain}} +
      +

      Random Domains

      + +

      + This list of domains is random. Refresh to get + new domains, or click Similar Domains to + take the helm. +

      +
      +{{/unless}} + +
      +
      + +{{>edge/parts/search-footer}} + diff --git a/marginalia_nu/src/main/resources/templates/edge/conversion-results-gmi.hdb b/marginalia_nu/src/main/resources/templates/edge/conversion-results-gmi.hdb new file mode 100644 index 00000000..89c01d5e --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/edge/conversion-results-gmi.hdb @@ -0,0 +1,12 @@ +# Search Engine + +=> /search Search +=> /search-about.gmi About + +{{query}} = {{result}} + +## Warning + +These results use floating point calculations, and may not be accurate +for very large or very small numbers. Do not use for orbital calculations, +thesis projects, or other sensitive work. \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/edge/conversion-results.hdb b/marginalia_nu/src/main/resources/templates/edge/conversion-results.hdb new file mode 100644 index 00000000..50e5840b --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/edge/conversion-results.hdb @@ -0,0 +1,37 @@ + + + + + Marginalia Search - {{query}} + + + + + + + + +{{>edge/parts/search-header}} + +
      + {{>edge/parts/search-form}} + +
      +
      +

      {{query}}

      +

      {{result}}

      +
      +
      +

      Warning

      +

      + These results use floating point calculations, and may not be accurate + for very large or very small numbers. Do not use for orbital calculations, + thesis projects, or other sensitive work. +

      +
      +
      + +
      + +{{>edge/parts/search-footer}} + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/edge/dictionary-results-gmi.hdb b/marginalia_nu/src/main/resources/templates/edge/dictionary-results-gmi.hdb new file mode 100644 index 00000000..7de8f20a --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/edge/dictionary-results-gmi.hdb @@ -0,0 +1,17 @@ +# Search Engine + +=> /search Search +=> /search-about.gmi About + +## Results for "{{{query}}}" + +{{#each entries}} +({{type}}) - {{definition}} +{{/each}} + +## Legal + +These definitions are from wiktionary, available under GFDL and CC BY-SA 3.0, except for fair use exceptions. + +=> https://en.wiktionary.org/ +=> https://dumps.wikimedia.org/legal.html \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/edge/dictionary-results.hdb b/marginalia_nu/src/main/resources/templates/edge/dictionary-results.hdb new file mode 100644 index 00000000..b7888418 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/edge/dictionary-results.hdb @@ -0,0 +1,48 @@ + + + + + Marginalia Search - {{query}} + + + + + + + + +{{>edge/parts/search-header}} + +
      + {{>edge/parts/search-form}} + +
      + {{#unless entries}} +
      +

      No Results

      +
      No definitions were found for that word
      +
      + {{/unless}} + + {{#each entries}} +
      +

      {{type}} - {{word}}

      +
      {{definition}}
      +
      + {{/each}} + + {{#if entries}} +
      +

      Legal

      +

      + This data is derived from wiktionary, + available under GFDL and CC BY-SA 3.0. More Information. +

      +
      + {{/if}} +
      + +
      + +{{>edge/parts/search-footer}} + diff --git a/marginalia_nu/src/main/resources/templates/edge/error-page.hdb b/marginalia_nu/src/main/resources/templates/edge/error-page.hdb new file mode 100644 index 00000000..f56edf19 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/edge/error-page.hdb @@ -0,0 +1,20 @@ + + + Error + + + + +
      +

      Error

      +

      Oops! It appears the index server is {{indexState}}.

      +

      The server was probably restarted to bring online some changes. Restarting the index typically takes + a few minutes, during which searches can't be served.

      + +

      In the event of a longer outage, the @marginalianu feed + on Twitter may have details, otherwise you can always send me an email at kontakt@marginalia.nu.

      + +

      This page will attempt to refresh automatically every few seconds.

      +
      + + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/edge/parts/search-footer.hdb b/marginalia_nu/src/main/resources/templates/edge/parts/search-footer.hdb new file mode 100644 index 00000000..3e1a8637 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/edge/parts/search-footer.hdb @@ -0,0 +1,9 @@ +
      + This website complies with the GDPR by not collecting any personal + information, and with the EU Cookie Directive by not using + cookies. More Information. +

      + Reach me at kontakt@marginalia.nu. +

      + + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/edge/parts/search-form.hdb b/marginalia_nu/src/main/resources/templates/edge/parts/search-form.hdb new file mode 100644 index 00000000..0f0a7a9f --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/edge/parts/search-form.hdb @@ -0,0 +1,26 @@ +
      + +
      \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/edge/parts/search-header.hdb b/marginalia_nu/src/main/resources/templates/edge/parts/search-header.hdb new file mode 100644 index 00000000..7bbbe580 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/edge/parts/search-header.hdb @@ -0,0 +1,8 @@ + +
      + +
      diff --git a/marginalia_nu/src/main/resources/templates/edge/search-result-gmi.hdb b/marginalia_nu/src/main/resources/templates/edge/search-result-gmi.hdb new file mode 100644 index 00000000..fe20c054 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/edge/search-result-gmi.hdb @@ -0,0 +1,4 @@ + +### {{{title}}} +=> {{geminiLink}} + {{{description}}} diff --git a/marginalia_nu/src/main/resources/templates/edge/search-result-metadata.hdb b/marginalia_nu/src/main/resources/templates/edge/search-result-metadata.hdb new file mode 100644 index 00000000..9127d2e6 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/edge/search-result-metadata.hdb @@ -0,0 +1,9 @@ +{{#if scripts}}🏭️{{/if}} +{{#if tracking}}🕵️️{{/if}} +{{#if media}}🎞️{{/if}} +{{#if affiliate}}💳️{{/if}} +{{#if cookies}}👁️️{{/if}} +{{format}} +{{#unless focusDomain}} +{{{rankingSymbol}}} +{{/unless}} \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/edge/search-result.hdb b/marginalia_nu/src/main/resources/templates/edge/search-result.hdb new file mode 100644 index 00000000..9a2b163a --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/edge/search-result.hdb @@ -0,0 +1,14 @@ + +
      + +

      {{title}}

      +

      {{description}}

      + +
      + Info + {{#unless focusDomain}}Search{{/unless}} + +
      {{>edge/search-result-metadata}}
      +
      +
      +
      diff --git a/marginalia_nu/src/main/resources/templates/edge/search-results-gmi.hdb b/marginalia_nu/src/main/resources/templates/edge/search-results-gmi.hdb new file mode 100644 index 00000000..32319c90 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/edge/search-results-gmi.hdb @@ -0,0 +1,19 @@ +# Search Engine + +=> /search Search +=> /search-about.gmi About + +{{#each problems}} +* {{{.}}}{{/each}} + +## Results for "{{{query}}}" + +{{#each results}} + +{{>edge/search-result-gmi}} + +{{/each}} + +-- + +=> / To index \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/edge/search-results.hdb b/marginalia_nu/src/main/resources/templates/edge/search-results.hdb new file mode 100644 index 00000000..82d7f707 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/edge/search-results.hdb @@ -0,0 +1,47 @@ + + + + + Marginalia Search - {{query}} + + + + + + + + + + + +{{>edge/parts/search-header}} + +
      +{{>edge/parts/search-form}} +
      +
      + {{#if maintenanceMessage}}

      Maintenance

      {{maintenanceMessage}}

      {{/if}} + {{#if evalResult}}

      Evaluation

      {{query}} = {{evalResult}}


      {{/if}} + {{#each wiki.entries}}

      Encyclopedia

      {{.}} Encyclopedia Page


      {{/each}} + + {{#if focusDomain}} +
      +

      {{focusDomain}}

      +

      + Showing results from {{focusDomain}} +

      + +
      + {{/if}} + + {{#each results}}{{>edge/search-result}}{{/each}} + + {{#unless evalResult}}{{#if problems}}

      Suggestions

        {{#each problems}}
      • {{{.}}}
      • {{/each}}
      {{/if}}{{/unless}} +
      +
      + +{{>edge/parts/search-footer}} + diff --git a/marginalia_nu/src/main/resources/templates/edge/site-info-gmi.hdb b/marginalia_nu/src/main/resources/templates/edge/site-info-gmi.hdb new file mode 100644 index 00000000..5696b251 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/edge/site-info-gmi.hdb @@ -0,0 +1,14 @@ +# Search Engine + +=> /search Search +=> /search-about.gmi About + +## Results for "{{{query}}}" + +Blacklisted: {{blacklisted}} +Pages Known: {{pagesKnown}} +Pages Indexed: {{pagesKnown}} +Inbound Links: {{inboundLinks}} +Outbound Links: {{outboundLinks}} +Nominal Quality: {{nominalQuality}}% +Crawl Ranking: {{ranking}}% \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/edge/site-info.hdb b/marginalia_nu/src/main/resources/templates/edge/site-info.hdb new file mode 100644 index 00000000..19b585b8 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/edge/site-info.hdb @@ -0,0 +1,58 @@ + + + + + Marginalia Search - {{query}} + + + + + + + + +{{>edge/parts/search-header}} + +
      + {{>edge/parts/search-form}} + +
      +
      +

      {{domain}}

      + Thumbnail image of {{domain}} +
      + +
      + +

      Indexing Information

      +

      + Blacklisted: {{blacklisted}}
      + Pages Known: {{pagesKnown}}
      + Pages Crawled: {{pagesFetched}}
      + Pages Indexed: {{pagesIndexed}}
      + Crawl State: {{state}}
      +

      +
      + +
      +

      Links

      +

      + Nominal Quality: {{nominalQuality}}%
      + Crawl Ranking: {{ranking}}%
      + Incoming Links: {{incomingLinks}}
      + Outbound Links: {{outboundLinks}}
      +

      + +
      + + {{#each results}}{{>edge/search-result}}{{/each}} +
      +
      + +{{>edge/parts/search-footer}} + + + diff --git a/marginalia_nu/src/main/resources/templates/encyclopedia/wiki-error.hdb b/marginalia_nu/src/main/resources/templates/encyclopedia/wiki-error.hdb new file mode 100644 index 00000000..37376677 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/encyclopedia/wiki-error.hdb @@ -0,0 +1,28 @@ + + + + + Error + + + + + +
      + +
      +
      +

      An error has occurred!

      +

      + Either the page you attempted to access does not exist, + or the automatic cleaning has process failed. +

      +

      + Please use this link as a back-up:
      + {{.}} +

      +
      + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/encyclopedia/wiki-search.hdb b/marginalia_nu/src/main/resources/templates/encyclopedia/wiki-search.hdb new file mode 100644 index 00000000..bc5d5a12 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/encyclopedia/wiki-search.hdb @@ -0,0 +1,35 @@ + + + + + Encyclopedia Search: {{query}} + + + + + +
      + +
      +
      +

      Search the Encyclopedia

      + +

      Search results

      + {{#if error}} +
      Failed to find exact article match
      + {{/if}} +
      + {{#each results}} +
      {{name}}
      + {{#if refName}}
      {{refName}}
      {{/if}} + {{/each}} +
      +
      + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/memex/memex-create-form.hdb b/marginalia_nu/src/main/resources/templates/memex/memex-create-form.hdb new file mode 100644 index 00000000..6fbd6cec --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/memex-create-form.hdb @@ -0,0 +1,23 @@ + + +{{>memex/partial/memex-head}} + +{{>memex/partial/memex-topbar}} +
      +
      +

      New : {{url}}

      +
      +
      + +
      + + +
      +
      +

      Existing posts

      +
      +
        {{#each docs}}
      • {{url}}
      • {{/each}}
      +
      +
      +{{> memex/partial/memex-footer}} + diff --git a/marginalia_nu/src/main/resources/templates/memex/memex-delete-form.hdb b/marginalia_nu/src/main/resources/templates/memex/memex-delete-form.hdb new file mode 100644 index 00000000..ab41abcf --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/memex-delete-form.hdb @@ -0,0 +1,23 @@ + + +{{>memex/partial/memex-head}} + +{{>memex/partial/memex-topbar}} +
      +

      Delete {{url}}

      +
      +
      + + +
      +
      + +
      +
      +{{#if doc}}{{{doc}}}{{/if}} +{{#if image}}{{/if}} +
      +{{>memex/partial/memex-backlinks}} +
      +{{> memex/partial/memex-footer}} + diff --git a/marginalia_nu/src/main/resources/templates/memex/memex-image.hdb b/marginalia_nu/src/main/resources/templates/memex/memex-image.hdb new file mode 100644 index 00000000..795d4f9c --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/memex-image.hdb @@ -0,0 +1,24 @@ + + +{{>memex/partial/memex-head}} + + +{{>memex/partial/memex-topbar url=path}} + +
      +
      +

      {{path}}

      + +
      + +
      +{{> memex/partial/memex-footer}} + diff --git a/marginalia_nu/src/main/resources/templates/memex/memex-index-feed.hdb b/marginalia_nu/src/main/resources/templates/memex/memex-index-feed.hdb new file mode 100644 index 00000000..352897bb --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/memex-index-feed.hdb @@ -0,0 +1,20 @@ + + + marginalia.nu{{url}} + {{title}} + + marginalia kontakt@marginalia.nu + {{domain}}/ + {{now}} +{{#each docs}} +{{#amgarp DRAFT}} + + {{title}} + + {{domain}}{{url}} + {{date}}T00:00:00Z + +{{/amgarp}} +{{/each}} + + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/memex/memex-index.hdb b/marginalia_nu/src/main/resources/templates/memex/memex-index.hdb new file mode 100644 index 00000000..59243c33 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/memex-index.hdb @@ -0,0 +1,34 @@ + + +{{>memex/partial/memex-head}} + + +{{>memex/partial/memex-topbar}} + +
      + +
      +{{#if indexData}}{{{indexData}}}{{/if}} +{{#unless indexData}}

      {{url}}

      {{/unless}} + +{{>memex/partial/memex-task-listing}} +{{>memex/partial/memex-documents-inline}} +
      + + +
      +{{> memex/partial/memex-footer}} + diff --git a/marginalia_nu/src/main/resources/templates/memex/memex-rename-form.hdb b/marginalia_nu/src/main/resources/templates/memex/memex-rename-form.hdb new file mode 100644 index 00000000..db969027 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/memex-rename-form.hdb @@ -0,0 +1,23 @@ + + +{{>memex/partial/memex-head}} + +{{>memex/partial/memex-topbar}} +
      +

      Rename {{url}}

      +
      +
      + + +
      +
      + +
      +
      +{{#if doc}}{{{doc}}}{{/if}} +{{#if image}}{{/if}} +
      +{{>memex/partial/memex-backlinks}} +
      +{{> memex/partial/memex-footer}} + diff --git a/marginalia_nu/src/main/resources/templates/memex/memex-tombstone.hdb b/marginalia_nu/src/main/resources/templates/memex/memex-tombstone.hdb new file mode 100644 index 00000000..905b1bec --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/memex-tombstone.hdb @@ -0,0 +1,17 @@ + + +{{>memex/partial/memex-head}} + +{{>memex/partial/memex-topbar}} +
      +
      +

      {{url}} is gone

      +

      +{{#if message}}{{{message}}}{{/if}} +{{#if redirect}}See {{redirect}}{{/if}} +

      +
      +{{>memex/partial/memex-backlinks}} +
      +{{> memex/partial/memex-footer}} + diff --git a/marginalia_nu/src/main/resources/templates/memex/memex-update-form.hdb b/marginalia_nu/src/main/resources/templates/memex/memex-update-form.hdb new file mode 100644 index 00000000..6b7a55e5 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/memex-update-form.hdb @@ -0,0 +1,20 @@ + + +{{>memex/partial/memex-head}} + +{{>memex/partial/memex-topbar}} +
      +
      +
      + +
      + + +
      +
      + +
      +{{> memex/partial/memex-footer}} + diff --git a/marginalia_nu/src/main/resources/templates/memex/memex-upload-form.hdb b/marginalia_nu/src/main/resources/templates/memex/memex-upload-form.hdb new file mode 100644 index 00000000..e503a2db --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/memex-upload-form.hdb @@ -0,0 +1,26 @@ + + +{{>memex/partial/memex-head}} + +{{>memex/partial/memex-topbar}} +
      +
      +

      Upload : {{url}}

      +
      +
      + +
      +
      +
      +
      + +
      +
      +

      Existing posts

      +
      +
        {{#each docs}}
      • {{url}}
      • {{/each}} +
          {{#each images}}
        • {{url}}
        • {{/each}} +
      +
      +{{> memex/partial/memex-footer}} + diff --git a/marginalia_nu/src/main/resources/templates/memex/memex-view.hdb b/marginalia_nu/src/main/resources/templates/memex/memex-view.hdb new file mode 100644 index 00000000..47eb1480 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/memex-view.hdb @@ -0,0 +1,36 @@ + + +{{>memex/partial/memex-head}} + + +{{>memex/partial/memex-topbar url=baseDoc.url}} + +
      +
      +{{{doc}}} + +{{#pragma this "TOPIC"}} +{{>memex/partial/memex-backlinks-inline}} +{{/pragma}} +
      + +
      +{{> memex/partial/memex-footer}} + diff --git a/marginalia_nu/src/main/resources/templates/memex/partial/memex-backlinks-inline.hdb b/marginalia_nu/src/main/resources/templates/memex/partial/memex-backlinks-inline.hdb new file mode 100644 index 00000000..de40e40d --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/partial/memex-backlinks-inline.hdb @@ -0,0 +1,6 @@ +{{#if backlinks}} +
      {{#each backlinks}} +
      {{url}}
      +
      {{description}}
      +{{/each}}
      +{{/if}} \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/memex/partial/memex-backlinks.hdb b/marginalia_nu/src/main/resources/templates/memex/partial/memex-backlinks.hdb new file mode 100644 index 00000000..a15d7710 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/partial/memex-backlinks.hdb @@ -0,0 +1,9 @@ +{{#if backlinks}} + +{{/if}} \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/memex/partial/memex-directories.hdb b/marginalia_nu/src/main/resources/templates/memex/partial/memex-directories.hdb new file mode 100644 index 00000000..164707c5 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/partial/memex-directories.hdb @@ -0,0 +1,9 @@ +
      +

      Directories

      +{{#if parent}} + ..
      +{{/if}} +{{#each directories}} + {{filename}}
      +{{/each}} +
      \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/memex/partial/memex-documents-inline.hdb b/marginalia_nu/src/main/resources/templates/memex/partial/memex-documents-inline.hdb new file mode 100644 index 00000000..59598460 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/partial/memex-documents-inline.hdb @@ -0,0 +1,10 @@ +{{#pragma this "LISTING"}} +{{#if docs}}

      Documents


      {{/if}} +
      +{{#each docs}} +{{#unless index}} + {{url.filename}}
      +{{/unless}} +{{/each}} +
      +{{/pragma}} \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/memex/partial/memex-documents.hdb b/marginalia_nu/src/main/resources/templates/memex/partial/memex-documents.hdb new file mode 100644 index 00000000..242004ac --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/partial/memex-documents.hdb @@ -0,0 +1,10 @@ +{{#amgarp this "LISTING"}} +
      +{{#if docs}}

      Documents

      {{/if}} +{{#each docs}} +{{#unless index}} + {{url.filename}}
      +{{/unless}} +{{/each}} +
      +{{/amgarp}} \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/memex/partial/memex-footer.hdb b/marginalia_nu/src/main/resources/templates/memex/partial/memex-footer.hdb new file mode 100644 index 00000000..32edef62 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/partial/memex-footer.hdb @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/memex/partial/memex-head.hdb b/marginalia_nu/src/main/resources/templates/memex/partial/memex-head.hdb new file mode 100644 index 00000000..af30c6f5 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/partial/memex-head.hdb @@ -0,0 +1,9 @@ + + + MEMEX - {{title}} + + + {{#pragma this "FEED"}} + + {{/pragma}} + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/memex/partial/memex-images.hdb b/marginalia_nu/src/main/resources/templates/memex/partial/memex-images.hdb new file mode 100644 index 00000000..6498d04e --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/partial/memex-images.hdb @@ -0,0 +1,6 @@ +
      +{{#if images}}

      Images

      {{/if}} +{{#each images}} + {{path.filename}}
      +{{/each}} +
      \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/memex/partial/memex-task-listing.hdb b/marginalia_nu/src/main/resources/templates/memex/partial/memex-task-listing.hdb new file mode 100644 index 00000000..6f9b8060 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/partial/memex-task-listing.hdb @@ -0,0 +1,10 @@ +{{#if tasks}} +
      +

      Open Tasks

      +
      +{{#each tasks}} +
      {{task}}
      +{{/each}} +
      +
      +{{/if}} diff --git a/marginalia_nu/src/main/resources/templates/memex/partial/memex-topbar.hdb b/marginalia_nu/src/main/resources/templates/memex/partial/memex-topbar.hdb new file mode 100644 index 00000000..940ec5bb --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/memex/partial/memex-topbar.hdb @@ -0,0 +1,13 @@ +
      + +
      + \ No newline at end of file diff --git a/marginalia_nu/src/main/resources/templates/podcast/episode.hdb b/marginalia_nu/src/main/resources/templates/podcast/episode.hdb new file mode 100644 index 00000000..dc1e4306 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/podcast/episode.hdb @@ -0,0 +1,35 @@ + + + + {{podcastName}}: {{title}} + + + + + + +
      + +
      + + + + diff --git a/marginalia_nu/src/main/resources/templates/podcast/listing.hdb b/marginalia_nu/src/main/resources/templates/podcast/listing.hdb new file mode 100644 index 00000000..0c076762 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/podcast/listing.hdb @@ -0,0 +1,33 @@ + + + + Podcasts: Nya avsnitt + + + + + + +
      + +
      +
      +

      Podcasts

      +
      + {{#each podcasts}} +
      + {{title}} +
      +
      +

      {{{description}}}

      +
      + {{/each}} +
      +
      + + + diff --git a/marginalia_nu/src/main/resources/templates/podcast/new.hdb b/marginalia_nu/src/main/resources/templates/podcast/new.hdb new file mode 100644 index 00000000..62c41782 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/podcast/new.hdb @@ -0,0 +1,34 @@ + + + + Podcasts: Nya avsnitt + + + + + + +
      + +
      +
      +

      Nya avsnitt

      +
      + {{#each episodes}} +
      + {{title}} +
      +
      +

      {{podcastName}}
      {{dateUploaded}}

      + +
      + {{/each}} +
      +
      + + + diff --git a/marginalia_nu/src/main/resources/templates/podcast/podcast.hdb b/marginalia_nu/src/main/resources/templates/podcast/podcast.hdb new file mode 100644 index 00000000..8af807ba --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/podcast/podcast.hdb @@ -0,0 +1,38 @@ + + + + Podcasts: {{title}} + + + + + + +
      + +
      +
      +

      {{metadata.title}}

      +

      + {{{metadata.description}}} +

      +

      {{metadata.extLink}}

      +

      Avsnitt

      +
      + {{#each episodes}} +
      + {{title}} +
      +
      +

      {{dateUploaded}}

      +
      + {{/each}} +
      +
      + + + diff --git a/marginalia_nu/src/main/resources/templates/smhi/index.hdb b/marginalia_nu/src/main/resources/templates/smhi/index.hdb new file mode 100644 index 00000000..556d8e7e --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/smhi/index.hdb @@ -0,0 +1,44 @@ + + + + {{title}} + + + + + + + + +
      + +
      + + + + diff --git a/marginalia_nu/src/main/resources/templates/smhi/prognos.hdb b/marginalia_nu/src/main/resources/templates/smhi/prognos.hdb new file mode 100644 index 00000000..274a6fce --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/smhi/prognos.hdb @@ -0,0 +1,73 @@ + + + + Väderprognos för {{plats.namn}} + + + + + + + + + + +
      + +
      +
      +

      {{plats.namn}}

      + + {{#each dygn}} + + + + + + + + + + + {{#each data}} + + + + + + + + {{/each}} + + {{/each}} +
      {{date}}{{{veckodag}}}
      TidTempVindNeder.Moln
      {{time}}{{temp}}{{vind}} ({{byvind}}){{nederbord}} {{nederbordTyp}}{{moln}}
      + + +

      Förklaring

      +

      Molntäcke (Moln.) visas på en skala 0-8, där höga värden indikerar + tjockt molntäcke, och låga värden indikerar blåare skyar.

      + +

      Nederbörd (Neder.) indikeras med förkortningar: + + + + + + + +
      SSnö
      SBSnöblandat regn
      RRegn
      DDimma
      UKRUnderkylt regn
      UKDUnderkyld dimma
      +

      + +

      Källa SMHI

      +

      + All prognosdata hämtas från SMHI:s öppna API:er, under licensen + Creative Commons Erkännande 2.5. + Bäst före {{bastFore}}. +

      +
      + + + diff --git a/marginalia_nu/src/main/resources/templates/status/server-status.hdb b/marginalia_nu/src/main/resources/templates/status/server-status.hdb new file mode 100644 index 00000000..e8217101 --- /dev/null +++ b/marginalia_nu/src/main/resources/templates/status/server-status.hdb @@ -0,0 +1,25 @@ + + + + Server Status + + + + + +
      + +
      +
      +

      Server Status

      + + {{#each status}} +

      + {{server}} - {{status}} +

      + {{/each}} + +
      + + + diff --git a/marginalia_nu/src/main/resources/units.csv b/marginalia_nu/src/main/resources/units.csv new file mode 100644 index 00000000..9dd990c6 --- /dev/null +++ b/marginalia_nu/src/main/resources/units.csv @@ -0,0 +1,62 @@ +30856775800000000,DISTANCE,pc,parsec,parsecs +9460500000000000,DISTANCE,ly,light years,light year +149597870700,DISTANCE,au,astronomical unit +1000,DISTANCE,km,kilometers,kilometer +1,DISTANCE,m,meters,meter +0.1,DISTANCE,dm,decimeters,decimeter +0.01,DISTANCE,cm,centimeters,centimeter +0.001,DISTANCE,mm,millimeters,millimeter +0.9144,DISTANCE,yd,yards,yard +0.0254,DISTANCE,in,inches,inch +0.3048,DISTANCE,ft,feet,foot +0.3048,DISTANCE,ft,feet,foot +1609.344,DISTANCE,miles,mile +1852,DISTANCE,nautical miles,nautical mile +201.168,DISTANCE,furlong,furlongs +1,WEIGHT,kg,kilograms,kilogram +0.001,WEIGHT,g,grams,gram +1000,WEIGHT,metric tons,ton,tons,tonne,tonnes +907.185,WEIGHT,short tons,short ton,imperial ton,imperial tons, +0.45359237,WEIGHT,lb,lbs,pounds +0.0283495231,WEIGHT,oz,ounces,ounce +1,AREA,m^2,square meters,square meter +0.01,AREA,dm^2,square decimeters,square decimeter +0.0001,AREA,cm^2,square centimeters,square centimeter +0.000001,AREA,mm^2,square millimeters,square millimeter +1000000,AREA,km^2,square kilometers,square kilometer +4046.9,AREA,ac,acre,acres +2589988.1103360,AREA,sq mi,mi^2,square miles,square mile +258.99881103360,AREA,hectares,hectare +0.09290304,AREA,ft^2,square foot,square feet +0.83612736,AREA,yd^2,square yard,square yards +0.00064516,AREA,in^2,square inch,square inches +1,VOLUME,m^3,cubic meter,cubic meters +1000000000,VOLUME,km^3,cubic kilometer,cubic kilometers +0.001,VOLUME,L,l,dm^3,liter,liters,cubic decimeter,cubic decimeter +0.0001,VOLUME,dl,deciliter,deciliters +0.0001,VOLUME,cl,centiliter,centiliters +0.00001,VOLUME,ml,milliliter,milliliters +0.000001,VOLUME,cm^3,cc,cubic centimeter,cubic centimeters +0.000000001,VOLUME,mm^3,cubic millimeter,cubic millimeters +0.000236588237,VOLUME,us cup,cup,cups +0.0000295735296,VOLUME,fl.oz.,fl oz,fluid ounces,fluid ounce +0.028316846592,VOLUME,ft^3,cubic foot,cubic feet +0.000016387064,VOLUME,in^3,cubic inch,cubic inches +0.764554857984,VOLUME,yd^3,cubic yard,cubic yards +0.000473176473,VOLUME,US pint,pint,pints +0.00378541178,VOLUME,gallon,gallons +1,TEMPERATURE,C,c,celsius,centigrade +0,TEMPERATURE,F,f,fahrenheit,fahrenheit +0,TEMPERATURE,K,k,kelvin,kelvins +1,TIME,S,s,second,seconds +0.001,TIME,ms,millisecond +60,TIME,min,minutes +3600,TIME,hour,hours +864000,TIME,day,days +604800,TIME,week,weeks +31557600.0,TIME,year,years +31557600.0,TIME,year,years +315576000.0,TIME,decade,decades +3155760000.0,TIME,century,centuries +1,ANGLE,degree,degrees +57.2957795,ANGLE,radians,radian diff --git a/marginalia_nu/src/test/java/EmptyTest.java b/marginalia_nu/src/test/java/EmptyTest.java new file mode 100644 index 00000000..e789f2cf --- /dev/null +++ b/marginalia_nu/src/test/java/EmptyTest.java @@ -0,0 +1,8 @@ +import org.junit.jupiter.api.Test; + +public class EmptyTest { + @Test + public void test() { + + } +} diff --git a/marginalia_nu/src/test/java/nu/marginalia/gemini/gmi/GemtextDatabaseTest.java b/marginalia_nu/src/test/java/nu/marginalia/gemini/gmi/GemtextDatabaseTest.java new file mode 100644 index 00000000..8a020257 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/gemini/gmi/GemtextDatabaseTest.java @@ -0,0 +1,30 @@ +package nu.marginalia.gemini.gmi; + +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +class GemtextDatabaseTest { + + @Test + public void test() { + var db = new GemtextDatabase(new MemexNodeUrl("/test.gmi"), new String[] { + "=> / foo", + "=> /x bar", + "=> /y baz", + "=> /z" + }); + verifyResult("foo", db.getLinkData(new MemexNodeUrl("/"))); + verifyResult("bar", db.getLinkData(new MemexNodeUrl("/x"))); + verifyResult("baz", db.getLinkData(new MemexNodeUrl("/y"))); + verifyResult("", db.getLinkData(new MemexNodeUrl("/z"))); + Assertions.assertFalse(db.getLinkData(new MemexNodeUrl("/w")).isPresent()); + } + + void verifyResult(String expected, Optional actual) { + Assertions.assertTrue(actual.isPresent(), () -> "No value found, expected " + expected); + Assertions.assertEquals(expected, actual.get()); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/gemini/gmi/GemtextDocumentTest.java b/marginalia_nu/src/test/java/nu/marginalia/gemini/gmi/GemtextDocumentTest.java new file mode 100644 index 00000000..5dd8f252 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/gemini/gmi/GemtextDocumentTest.java @@ -0,0 +1,88 @@ +package nu.marginalia.gemini.gmi; + +import nu.marginalia.gemini.gmi.line.GemtextLink; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import nu.marginalia.wmsa.memex.model.MemexUrl; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.*; + +class GemtextDocumentTest { + + @Test + void testEmpty() { + var document = GemtextDocument.of(new MemexNodeUrl("/test.gmi"), ""); + assertEquals("/test.gmi", document.getTitle()); + assertTrue(document.getLinks().isEmpty()); + } + + @Test + void testParseTombstone() { + var lines = new String[] { + "# Tombstone", +"", + "This special file contains information about removed resources.", +"", +"# Removed links", +"=> /dead.gmi It was never here, I swear", + "=> /dead2.png Removed through an act of God", + "=> /worklog.gmi Old and unused file.", + "=> /todo.gmi Empty file", + "=> /search-about.gmi Confusingly gemini-specific", + "=> /05-test.gmi Cursed testing file"}; + + var document = GemtextDatabase.of(new MemexNodeUrl("/test.gmi"), lines); + Arrays.stream(document.getLines()).forEach(System.out::println); + document.keys().forEach(k -> System.out.println(k + "-" + document.getLinkData(new MemexNodeUrl(k)).orElse(""))); + +} + @Test + void testVanilla() { + var document = GemtextDocument.of(new MemexNodeUrl("/test.gmi"), + "# Test Document", + "=> /foo.gmi\tMy foos", + "=>/bar.gmi\tMy bars", + "=>/baz.gmi", + "=>/foobar.gmi ", + "=>/volvo240.png hey cool car right", + "=>", + "=> ", + " => ", + "## Goodbye", + "... and good luck"); + assertEquals("Test Document", document.getTitle()); + Arrays.stream(document.getLines()).forEach(System.out::println); + assertEquals(5, document.getLinks().size()); + document.getLinks().forEach(System.out::println); + + assertArrayEquals(new String[] { + "/foo.gmi", "/bar.gmi", "/baz.gmi", "/foobar.gmi", "/volvo240.png" + }, + document.getLinks().stream().map(GemtextLink::getUrl).map(MemexUrl::getUrl).toArray()); + + assertArrayEquals(new String[] {"My foos", "My bars", null, null, "hey cool car right"}, + document.getLinks().stream().map(GemtextLink::getTitle).toArray()); + } + + + + @Test + void testTasks() { + var document = GemtextDocument.of(new MemexNodeUrl("/test.gmi"), + "# Test Document", + "- Go shopping", + "-- Milk", + "-- Eggs", + "-- Bacon", + "--- If they have organic, buy two", + "- Go dancing", + "Stuff", + "- Go dancing again"); + assertEquals("Test Document", document.getTitle()); + Arrays.stream(document.getLines()).forEach(System.out::println); + document.getOpenTopTasks().values().forEach(System.out::println); + } + +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/gemini/gmi/parser/GemtextTaskParserTest.java b/marginalia_nu/src/test/java/nu/marginalia/gemini/gmi/parser/GemtextTaskParserTest.java new file mode 100644 index 00000000..62a62082 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/gemini/gmi/parser/GemtextTaskParserTest.java @@ -0,0 +1,18 @@ +package nu.marginalia.gemini.gmi.parser; + +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import nu.marginalia.wmsa.memex.model.MemexNodeTaskId; +import org.junit.jupiter.api.Test; + +class GemtextTaskParserTest { + + @Test + void parse() { + System.out.println(GemtextTaskParser.parse("-task", new MemexNodeHeadingId(0), new MemexNodeTaskId(0))); + System.out.println(GemtextTaskParser.parse("- task", new MemexNodeHeadingId(0), new MemexNodeTaskId(0))); + System.out.println(GemtextTaskParser.parse("--task", new MemexNodeHeadingId(0), new MemexNodeTaskId(0))); + System.out.println(GemtextTaskParser.parse("-task(/)", new MemexNodeHeadingId(0), new MemexNodeTaskId(0))); + System.out.println(GemtextTaskParser.parse("-task(-)", new MemexNodeHeadingId(0), new MemexNodeTaskId(0))); + System.out.println(GemtextTaskParser.parse("-task(?)(x)", new MemexNodeHeadingId(0), new MemexNodeTaskId(0))); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/util/SeekDictionaryTest.java b/marginalia_nu/src/test/java/nu/marginalia/util/SeekDictionaryTest.java new file mode 100644 index 00000000..1987da6f --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/util/SeekDictionaryTest.java @@ -0,0 +1,31 @@ +package nu.marginalia.util; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class SeekDictionaryTest { + + @Test + public void testSeek() { + var dict = SeekDictionary.of((int[] x) -> x.length); + + for (int i = 0; i < 10000;) { + int j = (int)(1 + 9 * Math.random()); + int[] block = new int[j]; + for (int k = 0; k < j; k++) { + block[k] = i+k; + } + dict.add(block); + i+=j; + } + + o: for (int i = 0; i < 10000; i++) { + int[] vals = dict.bankForOffset(i); + for (var v : vals) { + if (v == i) continue o; + } + Assertions.fail("Could not find " + i); + } + } + +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/util/TestUtil.java b/marginalia_nu/src/test/java/nu/marginalia/util/TestUtil.java new file mode 100644 index 00000000..48e5202a --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/util/TestUtil.java @@ -0,0 +1,58 @@ +package nu.marginalia.util; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.DriverManager; + +public class TestUtil { + private static final int TEST_PORT_BASE = 6000; + private static final int TEST_PORT_RANGE = 2000; + + public static int getPort() { + return TEST_PORT_BASE + (int)(TEST_PORT_RANGE * Math.random()); + } + private final static Logger logger = LoggerFactory.getLogger(TestUtil.class); + + @SneakyThrows + public static HikariDataSource getConnection() { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl("jdbc:mysql://localhost:3306/WMSA_test"); + config.setUsername("wmsa"); + config.setPassword("wmsa"); + config.setMaximumPoolSize(16); + config.addDataSourceProperty("cachePrepStmts", "true"); + config.addDataSourceProperty("prepStmtCacheSize", "250"); + config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + + return new HikariDataSource(config); + } + @SneakyThrows + public static void evalScript(HikariDataSource hds, String scriptFile) { + + try (var conn = hds.getConnection()) { + + logger.info("Running script {}", scriptFile); + try (var scriptStream = ClassLoader.getSystemResourceAsStream(scriptFile); + var stmt = conn.createStatement()) { + for (String s : new String(scriptStream.readAllBytes()).split(";")) { + if (!s.isBlank()) { + try { + Assertions.assertTrue(stmt.executeUpdate(s) >= 0); + } catch (Exception ex) { + logger.error("Failed to execute\n{}" + s, ex); + } + + } + } + } + } + } + + +} diff --git a/marginalia_nu/src/test/java/nu/marginalia/util/btree/BTreeWriterTest.java b/marginalia_nu/src/test/java/nu/marginalia/util/btree/BTreeWriterTest.java new file mode 100644 index 00000000..694cf09a --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/util/btree/BTreeWriterTest.java @@ -0,0 +1,329 @@ +package nu.marginalia.util.btree; + +import nu.marginalia.util.btree.model.BTreeContext; +import nu.marginalia.util.btree.model.BTreeHeader; +import nu.marginalia.util.multimap.MultimapFileLong; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; +import java.util.StringJoiner; + +import static org.junit.jupiter.api.Assertions.*; + +class BTreeWriterTest { + + BTreeContext ctx = new BTreeContext(4, 2, 0xFFFF_FFFF_FFFF_FFFFL, 3); + BTreeWriter writer = new BTreeWriter(null, ctx); + + Logger logger = LoggerFactory.getLogger(getClass()); + @Test + void testSmallDataBlock() { + var header = writer.makeHeader(1024, ctx.BLOCK_SIZE_WORDS()/2); + assertEquals(1024 + BTreeHeader.BTreeHeaderSizeLongs, header.dataOffsetLongs()); + assertEquals(header.dataOffsetLongs(), header.indexOffsetLongs()); + } + + @Test + void testLayerCount() { + int wsq = ctx.BLOCK_SIZE_WORDS()*ctx.BLOCK_SIZE_WORDS(); + int wcub = ctx.BLOCK_SIZE_WORDS()*ctx.BLOCK_SIZE_WORDS()*ctx.BLOCK_SIZE_WORDS(); + + assertEquals(2, writer.makeHeader(1024, wsq-1).layers()); + assertEquals(2, writer.makeHeader(1024, wsq).layers()); + assertEquals(3, writer.makeHeader(1024, wsq+1).layers()); + + assertEquals(3, writer.makeHeader(1024, wcub-1).layers()); + assertEquals(3, writer.makeHeader(1024, wcub).layers()); + assertEquals(4, writer.makeHeader(1024, wcub+1).layers()); + } + + @Test + void testLayerOffset() { + int wcub = ctx.BLOCK_SIZE_WORDS()*ctx.BLOCK_SIZE_WORDS()*ctx.BLOCK_SIZE_WORDS(); + System.out.println(writer.makeHeader(1025, wcub).relativeLayerOffset(ctx, 0)); + System.out.println(writer.makeHeader(1025, wcub).relativeLayerOffset(ctx, 1)); + System.out.println(writer.makeHeader(1025, wcub).relativeLayerOffset(ctx, 2)); + + for (int i = 0; i < 1024; i++) { + var header = writer.makeHeader(0, i); + + + printTreeLayout(i, header, ctx); + + if (header.layers() >= 1) { + assertEquals(1, ctx.layerSize(i, header.layers() - 1) / ctx.BLOCK_SIZE_WORDS()); + } + } + } + + private void printTreeLayout(int numEntries, BTreeHeader header, BTreeContext ctx) { + StringJoiner sj = new StringJoiner(","); + for (int l = 0; l < header.layers(); l++) { + sj.add(""+ctx.layerSize(numEntries, l)/ctx.BLOCK_SIZE_WORDS()); + } + System.out.println(numEntries + ":" + sj); + } + + @Test + public void testWriteEntrySize2() throws IOException { + + var tempFile = Files.createTempFile(Path.of("/tmp"), "tst", "dat"); + Set toPut = new HashSet<>(); + + for (int i = 0; i < 500; i++) { + while (!toPut.add((int)(Integer.MAX_VALUE * Math.random()))); + } + + int[] data = toPut.stream().mapToInt(Integer::valueOf).sorted().toArray(); + + try { + RandomAccessFile raf = new RandomAccessFile(tempFile.toFile(), "rw"); + MultimapFileLong mmf = new MultimapFileLong(raf, FileChannel.MapMode.READ_WRITE, 10000, 1000, true); + + { + var writer = new BTreeWriter(mmf, ctx); + writer.write(0, toPut.size(), (offset) -> { + for (int i = 0; i < data.length; i++) { + mmf.put(offset + 2L*i, data[i]); + mmf.put(offset + 2L*i + 1, i); + } + }); + mmf.force(); + } + + { + var reader = new BTreeReader(mmf, ctx); + var header = reader.getHeader(0); + for (int i = 0; i < data.length; i++) { + long offset = reader.offsetForEntry(header, data[i]); + assertTrue(offset >= 0, "Negative offset for " + i + " -> " + offset); + assertEquals(i, mmf.get(offset+1)); + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + Files.delete(tempFile); + } + } + + @Test + public void testWriteEntrySize2Small() throws IOException { + + var tempFile = Files.createTempFile(Path.of("/tmp"), "tst", "dat"); + Set toPut = new HashSet<>(); + + for (int i = 0; i < 5; i++) { + while (!toPut.add((int)(Integer.MAX_VALUE * Math.random()))); + } + + int[] data = toPut.stream().mapToInt(Integer::valueOf).sorted().toArray(); + + try { + RandomAccessFile raf = new RandomAccessFile(tempFile.toFile(), "rw"); + MultimapFileLong mmf = new MultimapFileLong(raf, FileChannel.MapMode.READ_WRITE, 10000, 1000, true); + + { + var writer = new BTreeWriter(mmf, ctx); + writer.write( 0, toPut.size(), (offset) -> { + for (int i = 0; i < data.length; i++) { + mmf.put(offset + 2L*i, data[i]); + mmf.put(offset + 2L*i + 1, i); + } + }); + mmf.force(); + } + + { + var reader = new BTreeReader(mmf, ctx); + var header = reader.getHeader(0); + for (int i = 0; i < data.length; i++) { + long offset = reader.offsetForEntry(header, data[i]); + assertTrue(offset >= 0, "Negative offset for " + i + " -> " + offset); + assertEquals(i, mmf.get(offset+1)); + } + + for (int i = 0; i < 500; i++) { + long val = (long)(Long.MAX_VALUE * Math.random()); + while (toPut.contains(val)) val = (long)(Long.MAX_VALUE * Math.random()); + assertEquals(-1, reader.offsetForEntry(header, val)); + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + Files.delete(tempFile); + } + } + + + @Test + public void testWriteEqualityNotMasked() throws IOException { + for (int bs = 2; bs <= 4; bs++) { + var tempFile = Files.createTempFile(Path.of("/tmp"), "tst", "dat"); + Set toPut = new HashSet<>(); + + var ctx = new BTreeContext(5, 1, ~0, bs); + + for (int i = 0; i < 500; i++) { + while (!toPut.add((long) (Long.MAX_VALUE * Math.random()))) ; + } + + long[] data = toPut.stream().mapToLong(Long::valueOf).sorted().toArray(); + + try (MultimapFileLong mmf = MultimapFileLong.forOutput(tempFile, 1000)) { + { + var writer = new BTreeWriter(mmf, ctx); + writer.write(0, toPut.size(), (offset) -> { + for (int i = 0; i < data.length; i++) { + mmf.put(offset + i, data[i]); + } + }); + mmf.force(); + } + + { + var reader = new BTreeReader(mmf, ctx); + var header = reader.getHeader(0); + + printTreeLayout(toPut.size(), header, ctx); + + for (int i = 0; i < data.length; i++) { + long offset = reader.offsetForEntry(header, data[i]); + assertTrue(offset >= 0, "Negative offset for " + i + " -> " + offset); + assertEquals(data[i], mmf.get(offset)); + } + + for (int i = 0; i < 500; i++) { + long val = (long) (Long.MAX_VALUE * Math.random()); + while (toPut.contains(val)) val = (long) (Long.MAX_VALUE * Math.random()); + assertEquals(-1, reader.offsetForEntry(header, val)); + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + Files.delete(tempFile); + } + } + } + + @Test + public void testWriteEqualityMasked() throws IOException { + + for (int bs = 2; bs <= 4; bs++) { + var tempFile = Files.createTempFile(Path.of("/tmp"), "tst", "dat"); + Set toPut = new HashSet<>(); + + long mask = 0xFFFF_FFFF_0000_0000L; + var ctx = new BTreeContext(5, 1, mask, bs); + + for (int i = 0; i < 500; i++) { + while (!toPut.add((long) (Long.MAX_VALUE * Math.random()))) ; + } + + long[] data = toPut.stream().mapToLong(Long::valueOf).sorted().toArray(); + + try (MultimapFileLong mmf = MultimapFileLong.forOutput(tempFile, 1000)) { + { + var writer = new BTreeWriter(mmf, ctx); + writer.write(0, toPut.size(), (offset) -> { + for (int i = 0; i < data.length; i++) { + mmf.put(offset + i, data[i]); + } + }); + mmf.force(); + } + + { + var reader = new BTreeReader(mmf, ctx); + var header = reader.getHeader(0); + + printTreeLayout(toPut.size(), header, ctx); + + for (int i = 0; i < data.length; i++) { + long offset = reader.offsetForEntry(header, data[i] & mask); + assertTrue(offset >= 0, "Negative offset for " + i + " -> " + offset); + assertEquals(data[i], mmf.get(offset)); + } + + for (int i = 0; i < 500; i++) { + long val = (long) (Long.MAX_VALUE * Math.random()); + while (toPut.contains(val)) val = (long) (Long.MAX_VALUE * Math.random()); + assertEquals(-1, reader.offsetForEntry(header, val & mask)); + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + Files.delete(tempFile); + } + } + } + + @Test + public void testWriteTwoEqualityMasked() throws IOException { + + for (int bs = 2; bs <= 4; bs++) { + var tempFile = Files.createTempFile(Path.of("/tmp"), "tst", "dat"); + Set toPut = new HashSet<>(); + + long mask = 0xFFFF_FFFF_0000_0000L; + var ctx = new BTreeContext(5, 2, mask, bs); + + for (int i = 0; i < 500; i++) { + while (!toPut.add((long) (Long.MAX_VALUE * Math.random()))) ; + } + + long[] data = toPut.stream().mapToLong(Long::valueOf).sorted().toArray(); + + try (MultimapFileLong mmf = MultimapFileLong.forOutput(tempFile, 1000)) { + { + var writer = new BTreeWriter(mmf, ctx); + writer.write(0, toPut.size(), (offset) -> { + for (int i = 0; i < data.length; i++) { + mmf.put(offset + i*2L, data[i]); + mmf.put(offset + i*2L+1, i); + } + }); + mmf.force(); + } + + { + var reader = new BTreeReader(mmf, ctx); + var header = reader.getHeader(0); + + printTreeLayout(toPut.size(), header, ctx); + + for (int i = 0; i < data.length; i++) { + long offset = reader.offsetForEntry(header, data[i] & mask); + assertTrue(offset >= 0, "Negative offset for " + i + " -> " + offset); + assertEquals(data[i], mmf.get(offset)); + assertEquals(i, mmf.get(offset+1)); + } + + for (int i = 0; i < 500; i++) { + long val = (long) (Long.MAX_VALUE * Math.random()); + while (toPut.contains(val)) val = (long) (Long.MAX_VALUE * Math.random()); + assertEquals(-1, reader.offsetForEntry(header, val & mask)); + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + Files.delete(tempFile); + } + } + } + + + +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/util/graphics/dithering/FloydSteinbergDitherTest.java b/marginalia_nu/src/test/java/nu/marginalia/util/graphics/dithering/FloydSteinbergDitherTest.java new file mode 100644 index 00000000..b9510517 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/util/graphics/dithering/FloydSteinbergDitherTest.java @@ -0,0 +1,32 @@ +package nu.marginalia.util.graphics.dithering; + +import org.junit.jupiter.api.Test; + +import javax.imageio.ImageIO; +import java.io.File; +import java.io.IOException; + +class FloydSteinbergDitherTest { + + @Test + public void test() throws IOException { + convert("/home/vlofgren/Work/dither/volvo.jpg", "/home/vlofgren/Work/dither/volvo-raster.png"); + convert("/home/vlofgren/Work/dither/dog.jpg", "/home/vlofgren/Work/dither/dog-raster.png"); + convert("/home/vlofgren/Work/dither/robocop.jpg", "/home/vlofgren/Work/dither/robocop-raster.png"); + convert("/home/vlofgren/Work/dither/socrates.jpeg", "/home/vlofgren/Work/dither/socrates-raster.png"); + + +// convert("C:\\Users\\vlofg\\Documents\\volvo.jpg", +// "C:\\Users\\vlofg\\Documents\\volvo-raster.png"); +// convert("C:\\Users\\vlofg\\Documents\\socrates.jpg", +// "C:\\Users\\vlofg\\Documents\\socrates-raster.png"); +// convert("C:\\Users\\vlofg\\Documents\\goya_nude_maja.jpg", +// "C:\\Users\\vlofg\\Documents\\goya_nude_maja-raster.png"); + } + + void convert(String in, String out) throws IOException { + var result = new FloydSteinbergDither(Palettes.MARGINALIA_PALETTE, 640, 480).convert(ImageIO.read(new File(in))); + + ImageIO.write(result, "png", new File(out)); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/util/hash/LongPairHashMapTest.java b/marginalia_nu/src/test/java/nu/marginalia/util/hash/LongPairHashMapTest.java new file mode 100644 index 00000000..326c9b15 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/util/hash/LongPairHashMapTest.java @@ -0,0 +1,58 @@ +package nu.marginalia.util.hash; + +import nu.marginalia.util.multimap.MultimapFileLong; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; + +class LongPairHashMapTest { + + @Test + public void test() throws IOException { + + var tempFile = Files.createTempFile(Path.of("/tmp"), "tst", "dat"); + Set toPut = new HashSet<>(); + + for (int i = 0; i < 500; i++) { + while (!toPut.add((int)(Integer.MAX_VALUE * Math.random()))); + } + + try { + RandomAccessFile raf = new RandomAccessFile(tempFile.toFile(), "rw"); + MultimapFileLong mmf = new MultimapFileLong(raf, FileChannel.MapMode.READ_WRITE, 10000, 1000, true); + var lphm = new LongPairHashMap(mmf, 1024); + toPut.forEach(i -> { + lphm.put(new LongPairHashMap.CellData(i, i)); + }); + mmf.force(); + lphm.close(); + + RandomAccessFile raf2 = new RandomAccessFile(tempFile.toFile(), "rw"); + MultimapFileLong mmf2 = new MultimapFileLong(raf2, FileChannel.MapMode.READ_WRITE, 10000, 1000, true); + var lphm2 = new LongPairHashMap(mmf2); + toPut.forEach(i -> { + Assertions.assertTrue(lphm2.get(i).isSet()); + Assertions.assertEquals(i, (int) lphm2.get(i).getKey()); + Assertions.assertEquals(i, (int) lphm2.get(i).getOffset()); + }); + + for (int i = 0; i < 10_000_000; i++) { + if (!toPut.contains(i)) { + Assertions.assertFalse(lphm2.get(i).isSet()); + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + Files.delete(tempFile); + } + } + +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/util/test/TestUtil.java b/marginalia_nu/src/test/java/nu/marginalia/util/test/TestUtil.java new file mode 100644 index 00000000..2c5407ba --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/util/test/TestUtil.java @@ -0,0 +1,31 @@ +package nu.marginalia.util.test; + +import org.junit.jupiter.api.Assertions; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; + +public class TestUtil { + private static boolean isTempDir(Path dir) { + return dir.startsWith("/tmp") || dir.toString().contains("Temp"); + } + public static void clearTempDir(Path dir) { + if (!isTempDir(dir)) { + throw new IllegalArgumentException("Refusing to recursively delete directory with that name"); + } + if (Files.isDirectory(dir)) { + for (File f : dir.toFile().listFiles()) { + File[] files = f.listFiles(); + if (files != null) { + Arrays.stream(files).map(File::toPath).forEach(TestUtil::clearTempDir); + } + System.out.println("Deleting " + f); + f.delete(); + } + } + System.out.println("Deleting " + dir); + dir.toFile().delete(); + } +} diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/configuration/server/ServiceTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/configuration/server/ServiceTest.java new file mode 100644 index 00000000..fe709da9 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/configuration/server/ServiceTest.java @@ -0,0 +1,82 @@ +package nu.marginalia.wmsa.configuration.server; + +import com.zaxxer.hikari.HikariDataSource; +import lombok.SneakyThrows; +import nu.marginalia.util.TestUtil; +import nu.marginalia.wmsa.client.exception.RemoteException; +import nu.marginalia.wmsa.edge.assistant.EdgeAssistantService; +import nu.marginalia.wmsa.edge.assistant.client.AssistantClient; +import nu.marginalia.wmsa.edge.assistant.dict.DictionaryService; +import nu.marginalia.wmsa.edge.assistant.dict.SpellChecker; +import nu.marginalia.wmsa.edge.assistant.eval.MathParser; +import nu.marginalia.wmsa.edge.assistant.eval.Units; +import nu.marginalia.wmsa.edge.assistant.screenshot.ScreenshotService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import spark.Spark; + +import static nu.marginalia.util.TestUtil.getConnection; +import static org.junit.Assert.assertEquals; + +class ServiceTest { + static EdgeAssistantService service; + static AssistantClient client; + + private static HikariDataSource dataSource; + + static int testPort = TestUtil.getPort(); + + @SneakyThrows + public static HikariDataSource provideConnection() { + return getConnection(); + } + + + @BeforeAll + public static void setUpClass() { + Spark.port(testPort); + System.setProperty("service-name", "test"); + + dataSource = provideConnection(); + dataSource.setKeepaliveTime(100); + dataSource.setIdleTimeout(100); + + client = new AssistantClient(); + client.setServiceRoute("127.0.0.1", testPort); + + service = new EdgeAssistantService("127.0.0.1", + testPort, + new Initialization(), null, + new DictionaryService(dataSource, new SpellChecker()), + new MathParser(), + new Units(new MathParser()), + null, + null, + new ScreenshotService(null), null); + + Spark.awaitInitialization(); + } + + @Test + public void testDenyXPublic() { + try { + client.ping(Context.internal().treatAsPublic()).blockingSubscribe(); + Assertions.fail("Expected exception"); + } + catch (RemoteException ex) { + // + } + } + @Test + public void testAllowInternalNoXPublic() { + client.ping(Context.internal()).blockingSubscribe(); + } + + @Test + public void testAllowOnPublic() { + Assertions.assertEquals("EdgeAssistantService", client.who(Context.internal()).blockingFirst()); + Assertions.assertEquals("EdgeAssistantService", client.who(Context.internal().treatAsPublic()).blockingFirst()); + } + +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/data_store/AssistantTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/data_store/AssistantTest.java new file mode 100644 index 00000000..0e0d4509 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/data_store/AssistantTest.java @@ -0,0 +1,152 @@ +package nu.marginalia.wmsa.data_store; + +import com.zaxxer.hikari.HikariDataSource; +import lombok.SneakyThrows; +import nu.marginalia.util.TestUtil; +import nu.marginalia.wmsa.client.exception.RemoteException; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.edge.assistant.dict.DictionaryService; +import nu.marginalia.wmsa.edge.assistant.dict.SpellChecker; +import nu.marginalia.wmsa.edge.assistant.eval.MathParser; +import nu.marginalia.wmsa.edge.assistant.eval.Units; +import nu.marginalia.wmsa.edge.assistant.EdgeAssistantService; +import nu.marginalia.wmsa.edge.assistant.client.AssistantClient; +import nu.marginalia.wmsa.edge.assistant.screenshot.ScreenshotService; +import nu.marginalia.wmsa.edge.search.UnitConversion; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import spark.Spark; + +import static nu.marginalia.util.TestUtil.getConnection; +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ResourceLock(value = "mariadb", mode = ResourceAccessMode.READ_WRITE) +@Execution(ExecutionMode.SAME_THREAD) +@Tag("db") +class AssistantTest { + static EdgeAssistantService service; + static AssistantClient client; + + private static HikariDataSource dataSource; + + static int testPort = TestUtil.getPort(); + + @SneakyThrows + public static HikariDataSource provideConnection() { + return getConnection(); + } + + + @BeforeAll + public static void setUpClass() { + Spark.port(testPort); + System.setProperty("service-name", "test"); + + dataSource = provideConnection(); + dataSource.setKeepaliveTime(100); + dataSource.setIdleTimeout(100); + + client = new AssistantClient(); + client.setServiceRoute("127.0.0.1", testPort); + + service = new EdgeAssistantService("127.0.0.1", + testPort, + new Initialization(), null, + new DictionaryService(dataSource, new SpellChecker()), + new MathParser(), + new Units(new MathParser()), + null, null, + new ScreenshotService(null), null); + + Spark.awaitInitialization(); + } + + @BeforeEach + public void clearDb() { + } + + @SneakyThrows + @AfterAll + public static void tearDownAll() { + dataSource.close(); + Spark.awaitStop(); + } + + @Test + public void testEncyclopedia() { + var result = client.encyclopediaLookup(Context.internal(), "plato").blockingFirst(); + System.out.println(result); + assertTrue(result.entries.size() >= 1); + } + @Test + public void testSpellCheck() { + var result = client.spellCheck(Context.internal(), "plato").blockingFirst(); + System.out.println(result); + } + @Test + public void testDictionary() { + var result = client.dictionaryLookup(Context.internal(), "adiabatic").blockingFirst(); + System.out.println(result); + assertTrue(result.entries.size() > 1); + } + + @Test + public void testDictionaryNoQuery() { + var result = client.dictionaryLookup(Context.internal(), "vlofgren").blockingFirst(); + System.out.println(result); + assertTrue(result.entries.isEmpty()); + } + + @Test + public void testEncyclopediaNoQuery() { + var result = client.dictionaryLookup(Context.internal(), "vlofgren").blockingFirst(); + System.out.println(result); + assertTrue(result.entries.isEmpty()); + } + + @Test + public void testConvertUnitsWithParser() { + var conversion = new UnitConversion(client); + assertEquals("0.3 m", conversion.tryConversion(Context.internal(), "30 cm in m").get()); + assertEquals("500 m", conversion.tryConversion(Context.internal(), "0.5 km in m").get()); + assertEquals("500 m", conversion.tryConversion(Context.internal(), "0.1+0.4 km in m").get()); + assertTrue(conversion.tryConversion(Context.internal(), "0.5 km in F").isEmpty()); + assertTrue(conversion.tryConversion(Context.internal(), "plato").isEmpty()); + } + + @Test + public void testConvertUnits() { + assertEquals("5 m", client.unitConversion(Context.internal(), "500", "cm", "meters").blockingFirst()); + } + + @Test + public void testEvalmath() { + assertEquals("300", client.evalMath(Context.internal(), "3*10^2").blockingFirst()); + } + + @Test + public void testEvalWithParser() { + var conversion = new UnitConversion(client); + assertEquals("305", conversion.tryEval(Context.internal(), "300+5").get()); + assertEquals("1.772", conversion.tryEval(Context.internal(), "sqrt(pi)").get()); + + } + + + @Test + public void testConvertUnitsWeirdError() { + try { + client.unitConversion(Context.internal(), "500", "kg", "meters").blockingFirst(); + fail("Wanted exception"); + } + catch (RemoteException ex) { + + } + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/data_store/DataStoreServiceTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/data_store/DataStoreServiceTest.java new file mode 100644 index 00000000..4f5e7b87 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/data_store/DataStoreServiceTest.java @@ -0,0 +1,173 @@ +package nu.marginalia.wmsa.data_store; + +import com.zaxxer.hikari.HikariDataSource; +import io.reactivex.rxjava3.functions.Consumer; +import lombok.SneakyThrows; +import nu.marginalia.util.TestUtil; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.data_store.client.DataStoreClient; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDaoImpl; +import nu.marginalia.wmsa.edge.model.*; +import org.eclipse.jetty.util.UrlEncoded; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import spark.Spark; + +import java.net.URISyntaxException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import static nu.marginalia.util.TestUtil.evalScript; +import static nu.marginalia.util.TestUtil.getConnection; +import static org.junit.jupiter.api.Assertions.*; + +@ResourceLock(value = "mariadb", mode = ResourceAccessMode.READ_WRITE) +@Execution(ExecutionMode.SAME_THREAD) +@Tag("db") +class DataStoreServiceTest { + static DataStoreService service; + static DataStoreClient client; + + private static HikariDataSource dataSource; + private static EdgeDataStoreService edgeService; + + static int testPort = TestUtil.getPort(); + private static EdgeDataStoreDaoImpl edgeDataStore; + + @SneakyThrows + public static HikariDataSource provideConnection() { + return getConnection(); + } + + + @BeforeAll + public static void setUpClass() { + Spark.port(testPort); + System.setProperty("service-name", "test"); + + dataSource = provideConnection(); + dataSource.setKeepaliveTime(100); + dataSource.setIdleTimeout(100); + + client = new DataStoreClient(); + client.setServiceRoute("127.0.0.1", testPort); + + edgeDataStore = new EdgeDataStoreDaoImpl(dataSource); + edgeService = new EdgeDataStoreService(edgeDataStore); + service = new DataStoreService("127.0.0.1", + testPort, + new FileRepository(), + dataSource, + edgeService, + new Initialization(), null + ); + + Spark.awaitInitialization(); + } + + @SneakyThrows + @BeforeEach + public void clearDb() { + edgeDataStore.clearCaches(); + + evalScript(dataSource, "sql/data-store-init.sql"); + evalScript(dataSource, "sql/edge-crawler-cache.sql"); + + try (var connection = dataSource.getConnection()) { + + try (var stmt = connection.createStatement()) { + Assertions.assertTrue(stmt.executeUpdate("DELETE FROM EC_URL") >= 0); + Assertions.assertTrue(stmt.executeUpdate("DELETE FROM EC_DOMAIN_LINK") >= 0); + Assertions.assertTrue(stmt.executeUpdate("DELETE FROM EC_DOMAIN") >= 0); + } + connection.commit(); + } + } + + @SneakyThrows + @AfterAll + public static void tearDownAll() { + dataSource.close(); + Spark.awaitStop(); + } + + @Test + public void test() { + client.offerJson(Context.internal(), String.class, "Hello World", "test", "aaa").blockingSubscribe(); + assertEquals("Hello World", + client.getJson(Context.internal(), String.class, "test", "aaa").blockingFirst()); + } + + @Test + public void testUnderscore() { + client.offerJson(Context.internal(), String.class, "Hello World", "test", "aaa_bbb").blockingSubscribe();; + assertEquals("Hello World", + client.getJson(Context.internal(), String.class, "test", "aaa_bbb").blockingFirst()); + } + + @Test + public void testList() { + client.offerJson(Context.internal(), String.class, "Hello", "test", "aaa").blockingSubscribe();; + client.offerJson(Context.internal(), String.class, "World", "test", "bbb").blockingSubscribe();; + client.offerJson(Context.internal(), String.class, "Dude", "dummy", "ccc").blockingSubscribe(); + + List allElements = new ArrayList<>(); + client.getJsonIndicies(Context.internal(), String.class, "test") + .flatMapIterable(i->i) + .concatMap(id -> client.getJson(Context.internal(), String.class, "test", id)) + .blockingForEach(allElements::add); + + assertEquals(2, allElements.size()); + assertTrue(allElements.contains("Hello")); + assertTrue(allElements.contains("World")); + } + + + @Test + public void testEdgePutUrl() throws URISyntaxException { + client.putUrl(Context.internal(), -2, new EdgeUrl("https://marginalia.nu/")) + .blockingSubscribe(); + } + + @SneakyThrows + void query(String query, Consumer resultConsumer) { + try (var conn = dataSource.getConnection(); + var stmt = conn.createStatement() + ) { + resultConsumer.accept(stmt.executeQuery(query)); + + } catch (SQLException throwables) { + Assertions.fail(throwables); + } + } + + @SneakyThrows + void update(String sql) { + try (var conn = dataSource.getConnection(); + var stmt = conn.createStatement() + ) { + Assertions.assertTrue(stmt.executeUpdate(sql) >= 0); + conn.commit(); + } catch (SQLException throwables) { + Assertions.fail(throwables); + + } + } + + @Test + public void test2() throws URISyntaxException { + var request = new EdgeUrl("https://marginalia.nu/"); + var domain = UrlEncoded.encodeString(request.domain.toString()); + var path = UrlEncoded.encodeString(request.path); + + System.out.println(domain); + System.out.println(path); + } + +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/EdgeDirectorServiceTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/EdgeDirectorServiceTest.java new file mode 100644 index 00000000..f16dcc1f --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/EdgeDirectorServiceTest.java @@ -0,0 +1,263 @@ +package nu.marginalia.wmsa.edge; + +import com.zaxxer.hikari.HikariDataSource; +import io.reactivex.rxjava3.functions.Consumer; +import lombok.SneakyThrows; +import nu.marginalia.util.TestUtil; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlFeature; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDaoImpl; +import nu.marginalia.wmsa.edge.data.dao.task.*; +import nu.marginalia.wmsa.edge.director.EdgeDirectorService; +import nu.marginalia.wmsa.edge.director.client.EdgeDirectorClient; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlState; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlVisit; +import org.eclipse.jetty.util.UrlEncoded; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import spark.Spark; + +import java.net.URISyntaxException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.stream.Stream; + +import static nu.marginalia.util.TestUtil.evalScript; +import static nu.marginalia.util.TestUtil.getConnection; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ResourceLock(value = "mariadb", mode = ResourceAccessMode.READ_WRITE) +@Execution(ExecutionMode.SAME_THREAD) +@Tag("db") +class EdgeDirectorServiceTest { + static EdgeDirectorService service; + static EdgeDirectorClient client; + + private static HikariDataSource dataSource; + + static int testPort = TestUtil.getPort(); + private static EdgeDataStoreTaskDaoImpl taskDao; + private static EdgeDataStoreDaoImpl dataDao; + + private static Initialization init; + + @SneakyThrows + public static HikariDataSource provideConnection() { + return getConnection(); + } + + + @BeforeAll + public static void setUpClass() { + Spark.port(testPort); + System.setProperty("service-name", "test"); + + dataSource = provideConnection(); + dataSource.setKeepaliveTime(100); + dataSource.setIdleTimeout(100); + + client = new EdgeDirectorClient(); + client.setServiceRoute("127.0.0.1", testPort); + + dataDao = new EdgeDataStoreDaoImpl(dataSource); + var ongoingJobs = new EdgeDataStoreTaskOngoingJobs(); + init = new Initialization(); + taskDao = new EdgeDataStoreTaskDaoImpl(dataSource, new EdgeDomainBlacklistImpl(dataSource), + new EdgeDataStoreTaskTuner(dataSource), ongoingJobs, new EdgeFinishTasksQueue(dataSource, ongoingJobs), init); + service = new EdgeDirectorService("127.0.0.1", + testPort, + init, + taskDao, null + ); + + Spark.awaitInitialization(); + } + + @SneakyThrows + @BeforeEach + public void clearDb() { + taskDao.clearCaches(); + + evalScript(dataSource, "sql/data-store-init.sql"); + evalScript(dataSource, "sql/edge-crawler-cache.sql"); + + try (var connection = dataSource.getConnection()) { + + try (var stmt = connection.createStatement()) { + Assertions.assertTrue(stmt.executeUpdate("DELETE FROM EC_URL") >= 0); + Assertions.assertTrue(stmt.executeUpdate("DELETE FROM EC_DOMAIN_LINK") >= 0); + Assertions.assertTrue(stmt.executeUpdate("DELETE FROM EC_DOMAIN") >= 0); + } + connection.commit(); + } + init.setReady(); + } + + @SneakyThrows + @AfterAll + public static void tearDownAll() { + dataSource.close(); + Spark.awaitStop(); + } + + @SneakyThrows + void query(String query, Consumer resultConsumer) { + try (var conn = dataSource.getConnection(); + var stmt = conn.createStatement() + ) { + resultConsumer.accept(stmt.executeQuery(query)); + + } catch (SQLException throwables) { + Assertions.fail(throwables); + } + } + + @SneakyThrows + void update(String sql) { + try (var conn = dataSource.getConnection(); + var stmt = conn.createStatement() + ) { + Assertions.assertTrue(stmt.executeUpdate(sql) >= 0); + conn.commit(); + } catch (SQLException throwables) { + Assertions.fail(throwables); + + } + } + + + @Test + public void testEdgeGetIndexTask() throws URISyntaxException, InterruptedException { + dataDao.putUrl(-2, + new EdgeUrl("https://marginalia.nu/"), + new EdgeUrl("https://marginalia.nu/a"), + new EdgeUrl("https://marginalia.nu/b"), + new EdgeUrl("https://marginalia.nu/c")); + + dataDao.putUrl(-1.5, + new EdgeUrl("https://www.marginalia.nu/"), + new EdgeUrl("https://www.marginalia.nu/a"), + new EdgeUrl("https://www.marginalia.nu/b"), + new EdgeUrl("https://www.marginalia.nu/c")); + + dataDao.putUrl(-2.5, + new EdgeUrl("https://memex.marginalia.nu/"), + new EdgeUrl("https://memex.marginalia.nu/a"), + new EdgeUrl("https://memex.marginalia.nu/b"), + new EdgeUrl("https://memex.marginalia.nu/c")); + + Thread.sleep(1000); + + for (int i = 0; i < 4; i++) { + var rsp = client.getDiscoverTask(Context.internal()).blockingFirst(); + System.out.println(rsp); + if (rsp.domain != null) { + client.finishTask(Context.internal(), rsp.domain, -2, EdgeDomainIndexingState.ACTIVE).blockingSubscribe(); + Thread.sleep(1000); + } + } + + for (int i = 0; i < 4; i++) { + var rsp = client.getIndexTask(Context.internal(), 2, 10).blockingFirst(); + System.out.println(rsp); + } + } + + + @Test + public void testEdgeGetDiscoverTask() throws URISyntaxException { + + update("UPDATE EC_DOMAIN SET INDEXED=0"); + dataDao.putUrl(-2, + new EdgeUrl("https://marginalia.nu/"), + new EdgeUrl("https://marginalia.nu/a"), + new EdgeUrl("https://marginalia.nu/b"), + new EdgeUrl("https://marginalia.nu/c")); + + + query("SELECT URL,VISITED FROM EC_URL WHERE DOMAIN_ID=1", (rsp) -> { + while (rsp.next()) { + System.out.println(rsp.getString(1) + " - " + rsp.getString(2)); + } + }); + dataDao.putUrlVisited(new EdgeUrlVisit(new EdgeUrl("https://marginalia.nu/c"), + 0xF34, -1.1, "title", "desc", + "ip", "test", HtmlFeature.AFFILIATE_LINK.bit,123, 456, + EdgeUrlState.OK)); + + query("SELECT URL,VISITED FROM EC_URL WHERE DOMAIN_ID=1", (rsp) -> { + while (rsp.next()) { + System.out.println(rsp.getString(1) + " - " + rsp.getString(2)); + } + }); + + dataDao.putUrl(-2., + new EdgeUrl("https://www.marginalia.nu/"), + new EdgeUrl("https://www.marginalia.nu/y")); + + query("SELECT URL_PART, INDEXED FROM EC_DOMAIN", (rsp) -> { + while (rsp.next()) { + System.out.println(rsp.getString(1) + " - " + rsp.getString(2)); + } + }); + + { + var task = client.getDiscoverTask(Context.internal()).blockingFirst(); + System.out.println( + task + ); + assertEquals(3, task.urls.size()); + task.urls.forEach(System.out::println); + } + + { + var task = client.getDiscoverTask(Context.internal()).blockingFirst(); + assertEquals(2, task.urls.size()); + task.urls.forEach(System.out::println); + } + + { + var task = client.getDiscoverTask(Context.internal()).blockingFirst(); + assertEquals(0, task.urls.size()); + task.urls.forEach(System.out::println); + } + } + + + @Test + public void testFinalizeTask() throws SQLException, URISyntaxException { + Stream.of(new EdgeUrl("https://marginalia.nu/"), + new EdgeUrl("https://marginalia.nu/q"), + new EdgeUrl("https://marginalia.nu/r")) + .forEach(data -> dataDao.putUrl(-2, data)); + + update("UPDATE EC_DOMAIN SET INDEXED=1"); + + { + var task = client.getIndexTask(Context.internal(), 1, 10).blockingFirst(); + assertEquals(3, task.urls.size()); + task.urls.forEach(System.out::println); + } + client.finishTask(Context.internal(), new EdgeDomain("https://marginalia.nu"), -5, EdgeDomainIndexingState.ACTIVE) + .blockingSubscribe(); + } + + + @Test + public void test2() throws URISyntaxException { + var request = new EdgeUrl("https://marginalia.nu/"); + var domain = UrlEncoded.encodeString(request.domain.toString()); + var path = UrlEncoded.encodeString(request.path); + + System.out.println(domain); + System.out.println(path); + } + +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/archive/ArchiveTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/archive/ArchiveTest.java new file mode 100644 index 00000000..701e0cdb --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/archive/ArchiveTest.java @@ -0,0 +1,72 @@ +package nu.marginalia.wmsa.edge.archive; + +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.edge.archive.archiver.Archiver; +import nu.marginalia.wmsa.edge.archive.client.ArchiveClient; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import spark.Spark; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static nu.marginalia.util.TestUtil.getPort; +import static nu.marginalia.util.test.TestUtil.clearTempDir; + +@Execution(ExecutionMode.SAME_THREAD) +public class ArchiveTest { + static EdgeArchiveService service; + + static int testPort = getPort(); + private static Path tempPath; + private static Path tempPath2; + private static ArchiveClient archiveClient; + private static Archiver archiver; + + @BeforeAll + public static void setUpClass() throws IOException, InterruptedException { + Spark.port(testPort); + System.setProperty("service-name", "edge-archive"); + archiveClient = new ArchiveClient(); + archiveClient.setServiceRoute("127.0.0.1", testPort); + + tempPath = Files.createTempDirectory("archiveTest"); + tempPath2 = Files.createTempDirectory("wikiTest"); + + archiver = new Archiver(tempPath, 10); + service = new EdgeArchiveService("127.0.0.1", testPort, + tempPath, + archiver, + new Initialization(), null); + + Spark.awaitInitialization(); + } + + @AfterAll + public static void tearDown() throws Exception { + archiver.close(); + archiveClient.close(); + clearTempDir(tempPath); + clearTempDir(tempPath2); + } + + @SneakyThrows + @Test + public void testWiki() { + var url = "Plato_(Disambiguation)"; + + Assertions.assertFalse(archiveClient.hasWiki(Context.internal(), url).blockingFirst()); + + archiveClient.submitWiki(Context.internal(), url, "

      Hello

      ").blockingFirst(); + Assertions.assertTrue(archiveClient.hasWiki(Context.internal(), url).blockingFirst()); + Assertions.assertEquals("

      Hello

      ", archiveClient.getWiki(Context.internal(), url).blockingFirst()); + } + +} diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/archive/archiver/ArchiverTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/archive/archiver/ArchiverTest.java new file mode 100644 index 00000000..58dde1f8 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/archive/archiver/ArchiverTest.java @@ -0,0 +1,18 @@ +package nu.marginalia.wmsa.edge.archive.archiver; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import java.nio.file.Path; + +public class ArchiverTest { + + @Test + public void testArchiver() throws Exception { + Archiver archiver = new Archiver(Path.of("/tmp/"), 3); + archiver.writeData(new ArchivedFile("file1", "Hey".getBytes())); + archiver.writeData(new ArchivedFile("file2", "Hey".getBytes())); + archiver.writeData(new ArchivedFile("file3", "Hey".getBytes())); + archiver.writeData(new ArchivedFile("file4", "Hey".getBytes())); + archiver.close(); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/assistant/dict/WikiCleanerTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/assistant/dict/WikiCleanerTest.java new file mode 100644 index 00000000..ccea45ab --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/assistant/dict/WikiCleanerTest.java @@ -0,0 +1,47 @@ +package nu.marginalia.wmsa.edge.assistant.dict; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.openzim.ZIMTypes.ZIMFile; +import org.openzim.ZIMTypes.ZIMReader; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +class WikiCleanerTest { + + @Test + void cleanWikiJunk() throws IOException { + String str = new WikiCleaner().cleanWikiJunk("https://en.wikipedia.org/wiki/Scamander", new String(Files.readAllBytes(Path.of("/home/vlofgren/Work/wiki-cleaner/Scamander.wiki.html")))); + String str2 = new WikiCleaner().cleanWikiJunk("https://en.wikipedia.org/wiki/Plato", new String(Files.readAllBytes(Path.of("/home/vlofgren/Work/wiki-cleaner/Plato.wiki.html")))); + String str3 = new WikiCleaner().cleanWikiJunk("https://en.wikipedia.org/wiki/C++", new String(Files.readAllBytes(Path.of("/home/vlofgren/Work/wiki-cleaner/Cpp.wiki.html")))); + String str4 = new WikiCleaner().cleanWikiJunk("https://en.wikipedia.org/wiki/Memex", new String(Files.readAllBytes(Path.of("/home/vlofgren/Work/wiki-cleaner/Memex.wiki.html")))); + Files.writeString(Path.of("/home/vlofgren/Work/wiki-cleaner/Scamander.out.html"), str); + Files.writeString(Path.of("/home/vlofgren/Work/wiki-cleaner/Plato.out.html"), str2); + Files.writeString(Path.of("/home/vlofgren/Work/wiki-cleaner/Cpp.out.html"), str3); + Files.writeString(Path.of("/home/vlofgren/Work/wiki-cleaner/Memex.out.html"), str4); + } + + @Test @Disabled + public void readZim() throws IOException { + var zr = new ZIMReader(new ZIMFile("/home/vlofgren/Work/wikipedia_en_all_nopic_2021-01.zim")); +// try (var pw = new PrintWriter(new File("/home/vlofgren/Work/article-clusters.tsv"))) { +// zr.enumerateArticles(pw); +// } + zr.forEachArticles((url, art) -> { + if (art != null) { + System.out.println(url); + } +// if (art != null && art.length() > 5) { +// System.out.println(url + " -> " + art.substring(0, 5)); +// } + }, (p) -> true); + + /*try (var baos = zr.getArticleData("Giraffe", 'A')) { + String str = baos.toString(); + Files.writeString(Path.of("/home/vlofgren/Work/wiki-cleaner/Giraffe.wiki.html"), str); + Files.writeString(Path.of("/home/vlofgren/Work/wiki-cleaner/Giraffe.out.html"), new WikiCleaner().cleanWikiJunk("https://en.wikipedia.org/wiki/Giraffe", str)); + }*/ + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/assistant/eval/MathParserTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/assistant/eval/MathParserTest.java new file mode 100644 index 00000000..1935ad6b --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/assistant/eval/MathParserTest.java @@ -0,0 +1,42 @@ +package nu.marginalia.wmsa.edge.assistant.eval; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.ParseException; + +class MathParserTest { + + Logger logger = LoggerFactory.getLogger(getClass()); + + @Test + void parse() throws ParseException { + var parser = new MathParser(); + logger.info(parser.evalFormatted("3+5")); + logger.info(parser.evalFormatted("1+(300+log(5))")); + logger.info(parser.evalFormatted("sqrt(1+300)")); + logger.info(parser.evalFormatted("sqrt(pi)")); + logger.info(parser.evalFormatted("3+5-5")); + logger.info(parser.evalFormatted("3+-5+5")); + logger.info(parser.evalFormatted("3+-5+log 5")); + logger.info(parser.evalFormatted("log -5")); + } + + @Test + void tokenize() throws ParseException { + var parser = new MathParser(); + logger.info("{}", parser.tokenize("3.5")); + + logger.info("{}", parser.tokenize("(3.5 + 2)*3")); + } + + @Test + void parenthesize() throws ParseException { + var parser = new MathParser(); + logger.info("{}", parser.parenthesize(parser.tokenize("3.5"))); + logger.info("{}", parser.tokenize("(3.5)")); + logger.info("{}", parser.parenthesize(parser.tokenize("(3.5)"))); + logger.info("{}", parser.parenthesize(parser.tokenize("(3.5 * (2+5))"))); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/assistant/eval/UnitsTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/assistant/eval/UnitsTest.java new file mode 100644 index 00000000..93d1efd2 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/assistant/eval/UnitsTest.java @@ -0,0 +1,44 @@ +package nu.marginalia.wmsa.edge.assistant.eval; + +import org.junit.jupiter.api.Test; + +class UnitsTest { + + @Test + void convert() { + var units = new Units(new MathParser()); + units.convert("3.33", "cm", "m").ifPresent(System.out::println); + } + + @Test + void convert2() { + var units = new Units(new MathParser()); + units.convert("10", "km", "ft").ifPresent(System.out::println); + } + + @Test + void convert3() { + var units = new Units(new MathParser()); + units.convert("10", "oz", "tons").ifPresent(System.out::println); + } + + @Test + void convert4() { + var units = new Units(new MathParser()); + units.convert("10", "pc", "in").ifPresent(System.out::println); + } + + @Test + void convert5() { + var units = new Units(new MathParser()); + units.convert("50", "K", "K").ifPresent(System.out::println); + units.convert("50", "F", "K").ifPresent(System.out::println); + units.convert("50", "C", "K").ifPresent(System.out::println); + units.convert("50", "K", "F").ifPresent(System.out::println); + units.convert("50", "F", "F").ifPresent(System.out::println); + units.convert("50", "C", "F").ifPresent(System.out::println); + units.convert("50", "K", "C").ifPresent(System.out::println); + units.convert("50", "F", "C").ifPresent(System.out::println); + units.convert("50", "C", "C").ifPresent(System.out::println); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/assistant/suggest/SuggestionsTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/assistant/suggest/SuggestionsTest.java new file mode 100644 index 00000000..94f68001 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/assistant/suggest/SuggestionsTest.java @@ -0,0 +1,47 @@ +package nu.marginalia.wmsa.edge.assistant.suggest; + +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.assistant.dict.SpellChecker; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.util.List; + +class SuggestionsTest { + private static Suggestions suggestions; + + @BeforeAll + public static void setUp() { + LanguageModels lm = new LanguageModels( + Path.of("/home/vlofgren/Work/ngrams/ngrams-generous-emstr.bin"), + Path.of("/home/vlofgren/Work/ngrams/tfreq-new-algo3.bin"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-sentence-1.0-1.9.3.bin"), + Path.of("/home/vlofgren/Work/ngrams/English.RDR"), + Path.of("/home/vlofgren/Work/ngrams/English.DICT"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-tokens-1.0-1.9.3.bin") + ); + suggestions = new Suggestions(Path.of("/home/vlofgren/Work/sql-titles-clean"), + new SpellChecker(), new NGramDict(lm)); + } + + @Test + void getSuggestions() { + System.out.println(tryGetSuggestions("neop")); + System.out.println(tryGetSuggestions("neopla")); + System.out.println(tryGetSuggestions("middle p")); + System.out.println(tryGetSuggestions("new public mana")); + System.out.println(tryGetSuggestions("euse")); + } + + List tryGetSuggestions(String s) { + long start = System.currentTimeMillis(); + try { + return suggestions.getSuggestions(10, s); + } + finally { + System.out.println(System.currentTimeMillis() - start); + } + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/CrawlJobsSpecificationSetTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/CrawlJobsSpecificationSetTest.java new file mode 100644 index 00000000..2e147b42 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/CrawlJobsSpecificationSetTest.java @@ -0,0 +1,59 @@ +package nu.marginalia.wmsa.edge.crawler; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +class CrawlJobsSpecificationSetTest { + @Test + public void readSet() throws IOException { + Path tempFile = Files.createTempFile("tmp", "test"); + tempFile.toFile().deleteOnExit(); + Files.writeString(tempFile, "0\n10\n15"); + var specsSet = new CrawlJobsSpecificationSet(tempFile); + assertEquals(3, specsSet.size()); + assertEquals(0, specsSet.get(0).pass); + assertEquals(10, specsSet.get(1).pass); + assertEquals(15, specsSet.get(2).pass); + } + + @Test + public void readSetTrailingJunk() throws IOException { + Path tempFile = Files.createTempFile("tmp", "test"); + tempFile.toFile().deleteOnExit(); + Files.writeString(tempFile, "0\n10\n15\n"); + var specsSet = new CrawlJobsSpecificationSet(tempFile); + assertEquals(3, specsSet.size()); + assertEquals(0, specsSet.get(0).pass); + assertEquals(10, specsSet.get(1).pass); + assertEquals(15, specsSet.get(2).pass); + } + + @Test + public void readSetEmptyLines() throws IOException { + Path tempFile = Files.createTempFile("tmp", "test"); + tempFile.toFile().deleteOnExit(); + Files.writeString(tempFile, "\n0\n10\n\n15\n\n"); + var specsSet = new CrawlJobsSpecificationSet(tempFile); + assertEquals(3, specsSet.size()); + assertEquals(0, specsSet.get(0).pass); + assertEquals(10, specsSet.get(1).pass); + assertEquals(15, specsSet.get(2).pass); + } + + @Test + public void readSetEmptyLinesComments() throws IOException { + Path tempFile = Files.createTempFile("tmp", "test"); + tempFile.toFile().deleteOnExit(); + Files.writeString(tempFile, "#Hello\n0\n # World\n10\n\n15\n\n"); + var specsSet = new CrawlJobsSpecificationSet(tempFile); + assertEquals(3, specsSet.size()); + assertEquals(0, specsSet.get(0).pass); + assertEquals(10, specsSet.get(1).pass); + assertEquals(15, specsSet.get(2).pass); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerRobotsTxtTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerRobotsTxtTest.java new file mode 100644 index 00000000..8946231f --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerRobotsTxtTest.java @@ -0,0 +1,33 @@ +package nu.marginalia.wmsa.edge.crawler.domain; + +import crawlercommons.robots.SimpleRobotRules; +import crawlercommons.robots.SimpleRobotRulesParser; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + +class DomainCrawlerRobotsTxtTest { + @Test + public void testOverride() { + String contentsStr = "User-agent: *\n" + + "Disallow: /\n" + + "\n" + + "User-agent: Googlebot\n" + + "User-agent: YandexBot\n" + + "User-agent: Twitterbot\n" + + "User-agent: special_archiver\n" + + "User-agent: archive.org_bot\n" + + "User-agent: search.marginalia.nu\n" + + "Disallow:\n"; + + byte[] contents = contentsStr.getBytes(); + SimpleRobotRules rules = new SimpleRobotRulesParser().parseContent("https://www.brutman.com/robots.txt", + contents, + "text/plain", + "search.marginalia.nu"); + + assertTrue(rules.isAllowed("http://www.brutman.com/test")); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerTest.java new file mode 100644 index 00000000..931d2cf7 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerTest.java @@ -0,0 +1,287 @@ +package nu.marginalia.wmsa.edge.crawler.domain; + +import com.zaxxer.hikari.HikariDataSource; +import io.reactivex.rxjava3.exceptions.UndeliverableException; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import lombok.SneakyThrows; +import nu.marginalia.util.TestUtil; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.data_store.DataStoreService; +import nu.marginalia.wmsa.data_store.EdgeDataStoreService; +import nu.marginalia.wmsa.data_store.FileRepository; +import nu.marginalia.wmsa.data_store.client.DataStoreClient; +import nu.marginalia.wmsa.edge.archive.EdgeArchiveService; +import nu.marginalia.wmsa.edge.archive.client.ArchiveClient; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.LanguageFilter; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.DocumentKeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlProcessor; +import nu.marginalia.wmsa.edge.crawler.domain.processor.PlainTextProcessor; +import nu.marginalia.wmsa.edge.crawler.fetcher.HttpFetcher; +import nu.marginalia.wmsa.edge.crawler.fetcher.HttpRedirectResolver; +import nu.marginalia.wmsa.edge.crawler.worker.GeoIpBlocklist; +import nu.marginalia.wmsa.edge.crawler.worker.IpBlockList; +import nu.marginalia.wmsa.edge.crawler.worker.Worker; +import nu.marginalia.wmsa.edge.crawler.worker.WorkerFactory; +import nu.marginalia.wmsa.edge.crawler.worker.data.CrawlJobsSpecification; +import nu.marginalia.wmsa.edge.crawler.worker.facade.TaskProvider; +import nu.marginalia.wmsa.edge.crawler.worker.facade.UploadFacadeDirectImpl; +import nu.marginalia.wmsa.edge.crawler.worker.results.WorkerResults; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDaoImpl; +import nu.marginalia.wmsa.edge.director.client.EdgeDirectorClient; +import nu.marginalia.wmsa.edge.index.client.EdgeIndexClient; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.crawl.EdgeIndexTask; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Spark; + +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.LinkedBlockingQueue; + +import static nu.marginalia.util.TestUtil.evalScript; +import static nu.marginalia.util.TestUtil.getConnection; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@Tag("nobuild") +@ResourceLock(value = "mariadb", mode = ResourceAccessMode.READ_WRITE) +@Execution(ExecutionMode.SAME_THREAD) +@Tag("db") +class DomainCrawlerTest { + private static final Logger logger = LoggerFactory.getLogger(DomainCrawlerTest.class); + private static HttpFetcher fetcher; + private static LanguageFilter languageFilter; + + static DataStoreService service; + static DataStoreClient dataStoreClient; + static EdgeDirectorClient edgeDirectorClient; + + private static HikariDataSource dataSource; + private static EdgeDataStoreService edgeService; + + static int testPort = TestUtil.getPort(); + private static EdgeDataStoreDaoImpl edgeDataStore; + private static WorkerFactory workerFactory; + private static ArchiveClient archiveClient; + + private List crawlJobsSpecifications + = List.of( + new CrawlJobsSpecification(0), + new CrawlJobsSpecification(0), + new CrawlJobsSpecification(0), + new CrawlJobsSpecification(0), + new CrawlJobsSpecification(0), + new CrawlJobsSpecification(0), + new CrawlJobsSpecification(0), + new CrawlJobsSpecification(0), + new CrawlJobsSpecification(1), + new CrawlJobsSpecification(1), + new CrawlJobsSpecification(10), + new CrawlJobsSpecification(10), + new CrawlJobsSpecification(10), + new CrawlJobsSpecification(10), + new CrawlJobsSpecification(50), + new CrawlJobsSpecification(50) + ); + + static LinkedList indexTasks = new LinkedList<>(); + static LinkedList discoverTasks = new LinkedList<>(); + + @SneakyThrows + public static HikariDataSource provideConnection() { + var conn = getConnection(); + + evalScript(conn, "sql/data-store-init.sql"); + evalScript(conn, "sql/edge-crawler-cache.sql"); + + return conn; + } + + + @SneakyThrows + @BeforeAll + public static void setUpClass() { + Spark.port(testPort); + System.setProperty("service-name", "test"); + + dataSource = provideConnection(); + dataSource.setKeepaliveTime(100); + dataSource.setIdleTimeout(100); + dataStoreClient = new DataStoreClient(); + dataStoreClient.setServiceRoute("localhost", testPort); + edgeDirectorClient = new EdgeDirectorClient(); + edgeDirectorClient.setServiceRoute("localhost", testPort); + archiveClient = new ArchiveClient(); + archiveClient.setServiceRoute("localhost", testPort); + + edgeDataStore = new EdgeDataStoreDaoImpl(dataSource); + edgeService = new EdgeDataStoreService(edgeDataStore); + + + service = new DataStoreService("127.0.0.1", + testPort, + new FileRepository(), + dataSource, + edgeService, + new Initialization(), null + ); + + new EdgeArchiveService("127.0.0.1", + testPort, Files.createTempDirectory("domainCrawlerTest"), null, Initialization.already(), null); + + String userAgent = "nu.marginalia.wmsa.edge-crawler"; + fetcher = new HttpFetcher(userAgent); + + languageFilter = new LanguageFilter(); + + var lm = new LanguageModels( + Path.of("/var/lib/wmsa/model/ngrams-generous-emstr.bin"), + Path.of("/var/lib/wmsa/model/tfreq-generous-emstr.bin"), + Path.of("/var/lib/wmsa/model/opennlp-sentence.bin"), + Path.of("/var/lib/wmsa/model/English.RDR"), + Path.of("/var/lib/wmsa/model/English.DICT"), + Path.of("/var/lib/wmsa/model/opennlp-tok.bin") + ); + + var ke = new DocumentKeywordExtractor(new NGramDict(lm)); + var se = new SentenceExtractor(lm); + + DomainCrawlerFactory domainCrawlerFactory = + new DomainCrawlerFactory(fetcher, + new HtmlProcessor(ke,new SentenceExtractor(lm)), + new PlainTextProcessor(ke, se), archiveClient, new DomainCrawlerRobotsTxt(fetcher, userAgent), languageFilter, new IpBlockList(new GeoIpBlocklist())); + + workerFactory = new WorkerFactory(domainCrawlerFactory, + new TaskProvider() { + + @Override + public EdgeIndexTask getIndexTask(int pass) { + try { + return indexTasks.pop(); + } + catch (NoSuchElementException ex) { + return new EdgeIndexTask(null, 0, 0, 1.); + } + } + + @Override + public EdgeIndexTask getDiscoverTask() { + try { + return discoverTasks.pop(); + } + catch (NoSuchElementException ex) { + return new EdgeIndexTask(null, 0, 0, 1.); + } + } + }, + new HttpRedirectResolver(userAgent), + new UploadFacadeDirectImpl(edgeDataStore, + Mockito.mock(EdgeIndexClient.class), + edgeDirectorClient), + /* new UploadFacadeDirectImpl(edgeDataStore, new SearchIndexWriterDummyImpl()) */ + new IpBlockList(new GeoIpBlocklist())); + + RxJavaPlugins.setErrorHandler(ex -> { + if (ex instanceof UndeliverableException) { + ex = ex.getCause(); + } + logger.error("Error {} {}", ex.getClass(), ex.getMessage()); + }); + + Spark.get("/edge/task/blocked", (req,rsp) -> "false"); + Spark.awaitInitialization(); + } + + @SneakyThrows + @AfterAll + public static void tearDownAll() { + dataSource.close(); + Spark.awaitStop(); + } + + + @BeforeEach + @SneakyThrows + public void setUp() { + + edgeDataStore.clearCaches(); + + evalScript(dataSource, "sql/data-store-init.sql"); + evalScript(dataSource, "sql/edge-crawler-cache.sql"); + } + + @AfterEach + @SneakyThrows + public void tearDown() { + dataSource.close(); + } + + @Test + @Disabled + void localCrawl() throws URISyntaxException { + + dataStoreClient.putUrl(Context.internal(), 0, + new EdgeUrl("https://www.marginalia.nu/")) + .blockingSubscribe(); + dataStoreClient.putUrl(Context.internal(), 0, new EdgeUrl("http://www.cs.uni.edu/~mccormic/humor.html")).blockingSubscribe(); + dataStoreClient.putUrl(Context.internal(), 0, new EdgeUrl("https://www.leonardcohenfiles.com/")).blockingSubscribe(); + dataStoreClient.putUrl(Context.internal(), 0, new EdgeUrl("http://atsf.railfan.net/")).blockingSubscribe(); + dataStoreClient.putUrl(Context.internal(), 0, new EdgeUrl("http://sprott.physics.wisc.edu/")).blockingSubscribe(); + + final List> queues = new ArrayList<>(crawlJobsSpecifications.size()); + + for (int i = 0; i < crawlJobsSpecifications.size(); i++) { + queues.add(new LinkedBlockingQueue<>(1)); + } + + for (int i = 0; i < crawlJobsSpecifications.size()*16; i++) { + var spec = crawlJobsSpecifications.get(i/16); + var queue = queues.get(i/16); + + Worker worker; + if (spec.pass == 0) { + worker = workerFactory.buildDiscoverWorker(queue); + } + else { + worker = workerFactory.buildIndexWorker(queue, spec.pass); + } + + new Thread(worker, "Fetcher-"+i).start(); + } + + var uploader = workerFactory.buildUploader(queues); + uploader.run(); + } + + @SneakyThrows + @Test + void testCrawl() { + EdgeIndexTask task = new EdgeIndexTask(new EdgeDomain("www.marginalia.nu"), 0, 0, 1.); + task.urls.add(new EdgeUrl("https://www.marginalia.nu/")); + discoverTasks.add(task); + + LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); + + var uploader = workerFactory.buildUploader(List.of(queue)); + workerFactory.buildDiscoverWorker(queue).runCycle(); + assertFalse(queue.isEmpty()); + + queue.poll().upload(uploader); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerTest2.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerTest2.java new file mode 100644 index 00000000..da026154 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/DomainCrawlerTest2.java @@ -0,0 +1,68 @@ +package nu.marginalia.wmsa.edge.crawler.domain; + +import com.opencsv.exceptions.CsvValidationException; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.archive.client.ArchiveClient; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.LanguageFilter; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.DocumentKeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlProcessor; +import nu.marginalia.wmsa.edge.crawler.domain.processor.PlainTextProcessor; +import nu.marginalia.wmsa.edge.crawler.fetcher.HttpFetcher; +import nu.marginalia.wmsa.edge.crawler.worker.GeoIpBlocklist; +import nu.marginalia.wmsa.edge.crawler.worker.IpBlockList; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.crawl.EdgeIndexTask; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.mockito.Mockito; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.stream.Collectors; + +@Tag("nobuild") +@Tag("db") +@ResourceLock(value = "mariadb", mode = ResourceAccessMode.READ_WRITE) +@Execution(ExecutionMode.SAME_THREAD) +class DomainCrawlerTest2 { + + @SneakyThrows + @Test + public void test() throws CsvValidationException, IOException { + var fetcher = new HttpFetcher("search.marginalia.nu"); + var ingress = new EdgeIndexTask(new EdgeDomain("memex.marginalia.nu"), 0, 10, 1.); + ingress.urls.add(new EdgeUrl("https://memex.marginalia.nu/")); + + LanguageModels lm = new LanguageModels( + Path.of("/home/vlofgren/Work/ngrams/ngrams-generous-emstr.bin"), + Path.of("/home/vlofgren/Work/ngrams/tfreq-generous-emstr.bin"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-sentence-1.0-1.9.3.bin"), + Path.of("/home/vlofgren/Work/ngrams/English.RDR"), + Path.of("/home/vlofgren/Work/ngrams/English.DICT"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-tok.bin") + ); + var dict = new NGramDict(lm); + HtmlProcessor processor = new HtmlProcessor(new DocumentKeywordExtractor(dict),new SentenceExtractor(lm)); + + DomainCrawler dc = new DomainCrawler(fetcher, + Mockito.mock(PlainTextProcessor.class), + processor, + Mockito.mock(ArchiveClient.class), + new DomainCrawlerRobotsTxt(fetcher, "search.marginalia.nu") + , new LanguageFilter(), ingress , new IpBlockList(new GeoIpBlocklist())); + var res = dc.crawlToExhaustion(500, ()->true); + var wordsByCount = res.pageContents.values().stream().map(pc -> pc.words.get(IndexBlock.Top)).flatMap(top -> top.getWords().stream()).collect(Collectors.toMap(w -> w, w->1, Integer::sum)); + wordsByCount.entrySet().stream().filter(e -> dict.getTermFreq(e.getKey()) > 10_000).filter(e -> e.getValue()>2).sorted(Comparator.comparing(e -> e.getValue() / Math.max(1, dict.getTermFreq(e.getKey())))).forEach(System.out::println); + + } + +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/LanguageFilterTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/LanguageFilterTest.java new file mode 100644 index 00000000..d8429417 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/LanguageFilterTest.java @@ -0,0 +1,25 @@ +package nu.marginalia.wmsa.edge.crawler.domain; + +import nu.marginalia.wmsa.edge.crawler.domain.language.LanguageFilter; +import org.jsoup.Jsoup; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class LanguageFilterTest { + + @Test + void isPageInteresting() { + var languageFilter = new LanguageFilter(); + assertTrue(languageFilter.isPageInterestingByHtmlTag(Jsoup.parse("")).orElse(true)); + assertTrue(languageFilter.isPageInterestingByHtmlTag(Jsoup.parse("")).orElse(false)); + assertFalse(languageFilter.isPageInterestingByHtmlTag(Jsoup.parse("")).orElse(false)); + } + + @Test + public void isStringChinsese() { + var languageFilter = new LanguageFilter(); + assertTrue(languageFilter.isBlockedUnicodeRange("溶岩ドームの手前に広がる斜面(木が生えているところ)は普賢岳の山体です.今回の噴火にともない,このあたりの山体がマグマに押されて変形し,北(写真では左)にむかって100mほどせりだしました\n")); + } + +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/LinkParserTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/LinkParserTest.java new file mode 100644 index 00000000..347b4195 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/LinkParserTest.java @@ -0,0 +1,53 @@ +package nu.marginalia.wmsa.edge.crawler.domain; + +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.jsoup.Jsoup; +import org.junit.jupiter.api.Test; + +import java.net.URISyntaxException; + +import static org.junit.jupiter.api.Assertions.*; + +class LinkParserTest { + + private String parseLink(String href, String base) throws URISyntaxException { + var url = new EdgeUrl("http://www.marginalia.nu/" + base); + var domain = url.domain; + var parser = new LinkParser(); + var stuff = Jsoup.parseBodyFragment("test"); + var lnk = parser.parseLink( + url, + stuff.getElementsByTag("a").get(0)); + + if (lnk.isEmpty()) { + return null; + } + + return lnk.get().toString(); + } + + @Test + void testRenormalization() throws URISyntaxException { + assertEquals("http://www.marginalia.nu/test", parseLink("http://www.marginalia.nu/../test", "/")); + } + + @Test + void testRenormalization2() { + assertTrue("http:".matches("^[a-zA-Z]+:")); + assertFalse("/foo".matches("^[a-zA-Z]+:")); + } + + + @Test + void testAnchor() throws URISyntaxException { + assertNull(parseLink("#test", "/")); + } + @Test + void testRelative() throws URISyntaxException { + assertEquals("http://www.marginalia.nu/test", parseLink("../test", "/")); + assertEquals("http://www.marginalia.nu/test", parseLink("test", "/")); + assertEquals("http://www.marginalia.nu/foo/test", parseLink("test", "/foo/index.html")); + assertEquals("http://www.marginalia.nu/test", parseLink("../test", "/foo/index.html")); + assertEquals("http://www.marginalia.nu/test", parseLink("/test", "/foo/index.html")); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/RssCrawlerTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/RssCrawlerTest.java new file mode 100644 index 00000000..87f4bbf0 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/RssCrawlerTest.java @@ -0,0 +1,66 @@ +package nu.marginalia.wmsa.edge.crawler.domain; + +import jdk.security.jarsigner.JarSigner; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; + +class RssCrawlerTest { + + LinkParser lp = new LinkParser(); + + @Test @Disabled + public void test() throws URISyntaxException, IOException { + getLinks(new EdgeUrl("https://eli.li/feed.rss"), new String(Files.readAllBytes(Path.of("/home/vlofgren/Work/feed.rss")))); + } + + private Set getLinks(EdgeUrl base, String str) { + + var doc = Jsoup.parse(str.replaceAll("link", "lnk")); + + Set urls = new LinkedHashSet<>(); + + doc.select("entry > lnk[rel=alternate]").forEach(element -> { + var href = element.attr("href"); + if (href != null && !href.isBlank()) { + lp.parseLink(base, href) + .filter(u -> Objects.equals(u.domain.domain, base.domain.domain)) + .ifPresent(urls::add); + } + }); + + doc.getElementsByTag("lnk").forEach(element -> { + var href = element.text(); + if (href != null && !href.isBlank()) { + lp.parseLink(base, href) + .filter(u -> Objects.equals(u.domain.domain, base.domain.domain)) + .ifPresent(urls::add); + } + }); + + doc.select("item > guid[isPermalink=true]").forEach(element -> { + var href = element.text(); + if (href != null && !href.isBlank()) { + lp.parseLink(base, href) + .filter(u -> Objects.equals(u.domain.domain, base.domain.domain)) + .ifPresent(urls::add); + } + }); + + return urls; + } + + + + +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/UrlsCacheTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/UrlsCacheTest.java new file mode 100644 index 00000000..92e5c8db --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/UrlsCacheTest.java @@ -0,0 +1,58 @@ +package nu.marginalia.wmsa.edge.crawler.domain; + +import nu.marginalia.wmsa.edge.model.WideHashable; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class UrlsCacheTest { + + static class TestBox implements WideHashable { + public final long value; + + TestBox(long value) { + this.value = value; + } + + @Override + public long wideHash() { + return value; + } + } + + @Test + void testCacheEviction() { + var cache = new UrlsCache(5); + cache.add(new TestBox(0)); + hasValues(cache, 0L); + cache.add(new TestBox(1)); + hasValues(cache, 0L, 1L); + cache.add(new TestBox(2)); + hasValues(cache, 0L, 1L, 2L); + cache.add(new TestBox(3)); + hasValues(cache, 0L, 1L, 2L, 3L); + cache.add(new TestBox(4)); + hasValues(cache, 0L, 1L, 2L, 3L, 4L); + cache.add(new TestBox(5)); + hasValues(cache, 1L, 2L, 3L, 4L, 5L); + hasntValues(cache, 0L); + cache.add(new TestBox(6)); + hasValues(cache, 2L, 3L, 4L, 5L, 6L); + hasntValues(cache, 0L, 1L); + cache.add(new TestBox(7)); + hasValues(cache, 3L, 4L, 5L, 6L); + hasntValues(cache, 0L, 1L, 2L); + } + + public void hasValues(UrlsCache box, long... values) { + for (long v : values) { + assertTrue(box.contains(new TestBox(v)), () -> "Testing if cache contains " + v); + } + } + public void hasntValues(UrlsCache box, long... values) { + for (long v : values) { + assertFalse(box.contains(new TestBox(v)), () -> "Testing if cache misses " + v); + } + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/SentenceExtractorTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/SentenceExtractorTest.java new file mode 100644 index 00000000..435e203d --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/language/processing/SentenceExtractorTest.java @@ -0,0 +1,255 @@ +package nu.marginalia.wmsa.edge.crawler.domain.language.processing; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.WordRep; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.WordSpan; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.tag.WordSeparator; +import nu.marginalia.wmsa.edge.index.service.util.ranking.BuggyReversePageRank; +import nu.marginalia.wmsa.edge.index.service.util.ranking.BuggyStandardPageRank; +import nu.marginalia.wmsa.edge.integration.wikipedia.WikipediaReader; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.jsoup.Jsoup; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.regex.Pattern; + +class SentenceExtractorTest { + SentenceExtractor newSe; + SentenceExtractor legacySe; + LanguageModels lm = new LanguageModels( + Path.of("/home/vlofgren/Work/ngrams/ngrams-generous-emstr.bin"), + Path.of("/home/vlofgren/Work/ngrams/tfreq-new-algo4.bin"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-sentence-1.0-1.9.3.bin"), + Path.of("/home/vlofgren/Work/ngrams/English.RDR"), + Path.of("/home/vlofgren/Work/ngrams/English.DICT"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-tokens-1.0-1.9.3.bin") + ); + @BeforeEach + public void setUp() { + + newSe = new SentenceExtractor(lm); + legacySe = new SentenceExtractor(lm); + legacySe.setLegacyMode(true); + } + + + @Test @Disabled + public void getTheData() throws IOException { + var connStr = "jdbc:mariadb://localhost:3306/WMSA_test?rewriteBatchedStatements=true"; + + HikariConfig config = new HikariConfig(); + + config.setJdbcUrl(connStr); + config.setUsername("wmsa"); + config.setPassword("wmsa"); + config.addDataSourceProperty("cachePrepStmts", "true"); + config.addDataSourceProperty("prepStmtCacheSize", "250"); + config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + config.setMaximumPoolSize(100); + config.setMinimumIdle(10); + + var conn = new HikariDataSource(config); + + var rpr = new BuggyReversePageRank(conn, "virginia.xroads.edu"); + var spr = new BuggyStandardPageRank(conn, "virginia.xroads.edu"); + + var rankVector = spr.pageRankVector(); + var norm = rankVector.norm(); + + int resultCount = rpr.size()/10; + var domains = spr.pageRank(i -> rankVector.get(i) / norm, resultCount).toArray(); + int i = 0; + + try (var bw = Files.newBufferedWriter(Path.of("/tmp/domains.txt")); + var stmt = conn.getConnection().prepareStatement("SELECT URL_PROTO, URL_DOMAIN, URL_PORT, URL_PATH FROM EC_URL_VIEW WHERE DOMAIN_ID=? AND TITLE IS NOT NULL ORDER BY ID ASC LIMIT 10 ")) { + for (int domainId : domains) { + bw.write(String.format("%f\n", i++/(double) resultCount)); + stmt.setInt(1, domainId); + var rsp = stmt.executeQuery(); + while (rsp.next()) { + var url = new EdgeUrl(rsp.getString(1), new EdgeDomain(rsp.getString(2)), + rsp.getInt(3), rsp.getString(4)); + bw.write(url.toString()); + bw.write("\n"); + } + bw.write(".\n"); + } + + } + catch (Exception e) { + + } + } + + @SneakyThrows + @Test + void testExtractSubject() { + var data = Path.of("/home/vlofgren/Code/tmp-data/"); + + System.out.println("Running"); + + var dict = new NGramDict(lm); + + SentenceExtractor se = new SentenceExtractor(lm); + KeywordExtractor keywordExtractor = new KeywordExtractor(); + + for (var file : Objects.requireNonNull(data.toFile().listFiles())) { + System.out.println(file); + var dld = se.extractSentences(Jsoup.parse(Files.readString(file.toPath()))); + Map counts = new HashMap<>(); + for (var sentence : dld.sentences) { + for (WordSpan kw : keywordExtractor.getNames(sentence)) { + if (kw.end + 2 >= sentence.length()) { + continue; + } + if (sentence.separators[kw.end] == WordSeparator.COMMA + || sentence.separators[kw.end + 1] == WordSeparator.COMMA) + break; + + if (("VBZ".equals(sentence.posTags[kw.end]) || "VBP".equals(sentence.posTags[kw.end])) + && ("DT".equals(sentence.posTags[kw.end + 1]) || "RB".equals(sentence.posTags[kw.end]) || sentence.posTags[kw.end].startsWith("VB")) + ) { + counts.merge(new WordRep(sentence, new WordSpan(kw.start, kw.end)).word, -1, Integer::sum); + } + } + } + + int best = counts.values().stream().mapToInt(Integer::valueOf).min().orElse(0); + + counts.entrySet().stream().sorted(Map.Entry.comparingByValue()) + .filter(e -> e.getValue()<-2 && e.getValue() { + + var newResult = newSe.extractSentences(Jsoup.parse(post.body)); + + var newRes = documentKeywordExtractor.extractKeywords(newResult); + System.out.println(newRes); + }); + reader.join(); + } + @Test + void extractSentences() throws IOException { + var data = Path.of("/home/vlofgren/Code/tmp-data/"); + + System.out.println("Running"); + + var dict = new NGramDict(lm); + + DocumentKeywordExtractor documentKeywordExtractor = new DocumentKeywordExtractor(dict); + +// documentKeywordExtractorLegacy.setLegacy(true); + +// for (;;) { + long st = System.currentTimeMillis(); + for (var file : Objects.requireNonNull(data.toFile().listFiles())) { + + + var newResult = newSe.extractSentences(Jsoup.parse(Files.readString(file.toPath()))); + + var newRes = documentKeywordExtractor.extractKeywords(newResult); + + +// var legacyRes = documentKeywordExtractorLegacy.extractKeywords(newResult); +// +// EdgePageWordSet difference = new EdgePageWordSet(); +// for (IndexBlock block : IndexBlock.values()) { + +// var newWords = new HashSet<>(newRes.get(block).words); +// var oldWords = new HashSet<>(legacyRes.get(block).words); +// newWords.removeAll(oldWords); + +// if (!newWords.isEmpty()) { +// difference.append(block, newWords); +// } +// } +// System.out.println(difference); + System.out.println(newRes); +// System.out.println("---"); + } + System.out.println(System.currentTimeMillis() - st); +// } + + } + + @SneakyThrows + @Test + @Disabled + public void testSE() { + var result = newSe.extractSentences(Jsoup.parse(new URL("https://memex.marginalia.nu/log/26-personalized-pagerank.gmi"), 10000)); + + var dict = new NGramDict(lm); + System.out.println(new DocumentKeywordExtractor(dict).extractKeywords(result)); + + +// +// var pke = new PositionKeywordExtractor(dict, new KeywordExtractor()); +// pke.count(result).stream().map(wr -> wr.word).distinct().forEach(System.out::println); +// for (var sent : result.sentences) { +// System.out.println(sent); +// } + + } + + @Test + public void separatorExtraction() { + seprateExtractor("Cookies, cream and shoes"); + seprateExtractor("Cookies"); + seprateExtractor(""); + + } + + Pattern p = Pattern.compile("([, ]+)"); + public void seprateExtractor(String sentence) { + var matcher = p.matcher(sentence); + + Arrays.stream(p.split(sentence)).forEach(System.out::println); + List words = new ArrayList<>(); + List separators = new ArrayList<>(); + + int start = 0; + int wordStart = 0; + while (wordStart <= sentence.length()) { + if (!matcher.find(wordStart)) { + words.add(sentence.substring(wordStart)); + separators.add("S"); + break; + } + + if (wordStart != matcher.start()) { + words.add(sentence.substring(wordStart, matcher.start())); + separators.add(sentence.substring(matcher.start(), matcher.end()).isBlank() ? "S" : "C"); + } + wordStart = matcher.end(); + } + + System.out.println(words); + System.out.println(separators); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlProcessorTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlProcessorTest.java new file mode 100644 index 00000000..b90585c0 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlProcessorTest.java @@ -0,0 +1,119 @@ +package nu.marginalia.wmsa.edge.crawler.domain.processor; + +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.DocumentKeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.wmsa.edge.model.crawl.EdgeRawPageContents; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.jsoup.Jsoup; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.List; + +@Disabled +class HtmlProcessorTest { + Logger logger = LoggerFactory.getLogger(getClass()); + + LanguageModels lm = new LanguageModels( + Path.of("/home/vlofgren/Work/ngrams/ngrams-generous-emstr.bin"), + Path.of("/home/vlofgren/Work/ngrams/tfreq-generous-emstr.bin"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-sentence.bin"), + Path.of("/var/lib/wmsa/model/English.RDR"), + Path.of("/var/lib/wmsa/model/English.DICT"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-tok.bin") + ); + HtmlProcessor processor = new HtmlProcessor(new DocumentKeywordExtractor(new NGramDict(lm)),new SentenceExtractor(lm)); + + @Test + @Disabled + void processHtmlPage0() throws IOException, URISyntaxException { + List urls = List.of("https://www.marginalia.nu/", + "https://www.marginalia.nu/00-skrifter/", + "https://www.marginalia.nu/2021-04-smart/", + "https://www.marginalia.nu/2020-06-att-l%C3%A4ra/", + "https://www.marginalia.nu/2020-04-grader-av-liv/", + "https://www.marginalia.nu/2020-03-battre-internet/", + "https://www.marginalia.nu/2020-05-dr%C3%B6m/", + "https://www.marginalia.nu/2020-02-faktaresistens/", + "https://www.marginalia.nu/2020-01-dialog-forfattaren-i-verket/", + "https://search.marginalia.nu/about.html", + "https://www.putty.org/", + "https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html", + "https://legacy.3drealms.com/duke3d/", + "http://classics.mit.edu/Plato/stateman.html", + "http://www.southaustralianhistory.com.au/bruce.htm", + "http://www.castlecraft.com/main.htm", + "http://www.discoveryvallarta.com/gaybars.html", + "https://twitterrific.com/ios" + ); + + + + for (String url : urls) { + + + + var doc = Jsoup.parse(new URL(url), 15000); + var res = processor.processHtmlPage(new EdgeRawPageContents(new EdgeUrl("http://www.example.com/"), new EdgeUrl("http://www.example.com/"), doc.html(), null, "", true, LocalDateTime.now().toString()), + doc); + + + System.out.println("Q:" + res.metadata.quality()); + System.out.println(100*Math.exp(res.metadata.quality())); + System.out.println(res.metadata.rawLength + ", " + res.metadata.textBodyLength); + + System.out.println(res.metadata.totalWords + ", " + res.metadata.textDistinctWords / res.metadata.totalWords); + for (var words : res.words.values()) { + logger.info("{}: {}", words.block, words.getWords()); + } + } + } + + @Test @Disabled + void processHtmlPage() throws IOException, URISyntaxException { + var doc = Jsoup.parse(new URL("https://aysia.blondeninna.com/"), 5000); + var res = processor.processHtmlPage(new EdgeRawPageContents(new EdgeUrl("http://www.example.com/"), new EdgeUrl("http://www.example.com/"), doc.data(), null, "", true, LocalDateTime.now().toString()), + doc); + System.out.println(res); + System.out.println("--"); + System.out.println(res.metadata.title); + System.out.println("--"); + System.out.println(res.metadata.description); + System.out.println(res.metadata.textDistinctWords); + + System.out.println(res.metadata.smutCoefficient); + } + + @Test @Disabled + void processHtmlPage3() throws IOException, URISyntaxException { + var doc = Jsoup.parse(new URL("http://thelagniappechateau.com/wwwboard/720p/starcraft-2-pc-iso-download/"), 5000); + var res = processor.processHtmlPage(new EdgeRawPageContents(new EdgeUrl("http://www.example.com/"), new EdgeUrl("http://www.example.com/"), doc.data(), null, "", true, LocalDateTime.now().toString()), + doc); + System.out.println(res); + System.out.println("--"); + System.out.println(res.metadata.title); + System.out.println("--"); + System.out.println(res.metadata.description); + System.out.println(res.metadata.textDistinctWords); + + System.out.println(res.metadata.smutCoefficient); + } + + @Test @Disabled + void processHtmlPage2() throws IOException { + + var doc = Jsoup.parse(new String(Files.readAllBytes(Path.of("/home/vlofgren/monadnock.html")))); + doc.getElementsByTag("a").forEach(System.out::println); + + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlTagCleanerTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlTagCleanerTest.java new file mode 100644 index 00000000..0ad93a91 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/domain/processor/HtmlTagCleanerTest.java @@ -0,0 +1,27 @@ +package nu.marginalia.wmsa.edge.crawler.domain.processor; + +import org.jsoup.Jsoup; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class HtmlTagCleanerTest { + + HtmlTagCleaner tagCleaner = new HtmlTagCleaner(); + + public String cleanTag(String text) { + var doc = Jsoup.parse(text); + tagCleaner.clean(doc); + return doc.text(); + } + + @Test + public void testBriefCodeTag() { + assertEquals("hello", cleanTag("hello")); + assertEquals("System out println", cleanTag("System.out.println")); + assertEquals("hello", cleanTag("hello()")); + assertEquals("hello", cleanTag("<hello>")); + assertEquals("hello", cleanTag("hello(p,q)")); + assertEquals("hello", cleanTag("hello(p,q);")); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/fetcher/HttpFetcherTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/fetcher/HttpFetcherTest.java new file mode 100644 index 00000000..0fb68535 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/fetcher/HttpFetcherTest.java @@ -0,0 +1,79 @@ +package nu.marginalia.wmsa.edge.crawler.fetcher; + +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.net.URISyntaxException; + +import static org.junit.Assert.assertTrue; + +class HttpFetcherTest { + + @SneakyThrows + @Test + void testUrlPattern() { + var fetcher = new HttpFetcher("nu.marginalia.edge-crawler"); + + Assertions.assertFalse(fetcher.isUrlLikeBinary(new EdgeUrl("https://marginalia.nu/log.txt"))); + Assertions.assertTrue(fetcher.isUrlLikeBinary(new EdgeUrl("https://marginalia.nu/log.bin"))); + Assertions.assertTrue(fetcher.isUrlLikeBinary(new EdgeUrl("https://marginalia.nu/log.tar.gz"))); + Assertions.assertFalse(fetcher.isUrlLikeBinary(new EdgeUrl("https://marginalia.nu/log.htm"))); + Assertions.assertFalse(fetcher.isUrlLikeBinary(new EdgeUrl("https://marginalia.nu/log.html"))); + Assertions.assertFalse(fetcher.isUrlLikeBinary(new EdgeUrl("https://marginalia.nu/log"))); + Assertions.assertFalse(fetcher.isUrlLikeBinary(new EdgeUrl("https://marginalia.nu/log.php?id=1"))); + } + + @Test + void fetchUTF8() throws URISyntaxException { + var fetcher = new HttpFetcher("nu.marginalia.edge-crawler"); + var str = fetcher.fetchContent(new EdgeUrl("https://www.marginalia.nu")); + System.out.println(str.contentType); + System.out.println(str.fetchTimestamp); + System.out.println(str.data.substring(0, 1000)); + } + + @Test + void fetchText() throws URISyntaxException { + var fetcher = new HttpFetcher("nu.marginalia.edge-crawler"); + var str = fetcher.fetchContent(new EdgeUrl("https://www.marginalia.nu/robots.txt")); + System.out.println(str); + } + + @Test + void resolveRedirect() throws URISyntaxException { + var fetcher = new HttpRedirectResolver("nu.marginalia.edge-crawler"); + var str = fetcher.probe(new EdgeUrl("https://www.marginalia.nu/robots.txt")); + System.out.println(str); + } + + @Test + void resolveRedirectRitEdu() throws URISyntaxException { + var fetcher = new HttpRedirectResolver("nu.marginalia.edge-crawler"); + var str = fetcher.probe(new EdgeUrl("http://www.rit.edu/cla/philosophy/Suits.html")).blockingFirst(); + System.out.println(str); + } + + @Test + void resolveRedirect2() throws URISyntaxException { + var fetcher = new HttpRedirectResolver("nu.marginalia.edge-crawler"); + var str = fetcher.probe(new EdgeUrl("https://www.marginalia.nu/robots.txt")).blockingFirst(); + System.out.println(str); + } + + @Test + void resolveRedirect3() throws URISyntaxException { + var fetcher = new HttpRedirectResolver("nu.marginalia.edge-crawler"); + var str = fetcher.probe(new EdgeUrl("https://www.marginalia.nu/robots.txt")); + System.out.println(str); + } + + + @Test + void resolveRedirect4() throws URISyntaxException { + var fetcher = new HttpRedirectResolver("nu.marginalia.edge-crawler"); + var str = fetcher.probe(new EdgeUrl("https://www.marginalia.nu/robots.txt")); + System.out.println(str); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/worker/IpBlockListTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/worker/IpBlockListTest.java new file mode 100644 index 00000000..1b874101 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/worker/IpBlockListTest.java @@ -0,0 +1,35 @@ +package nu.marginalia.wmsa.edge.crawler.worker; + +import com.opencsv.exceptions.CsvValidationException; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.InetAddress; + +class IpBlockListTest { + + @Test + void getCountry() throws IOException, CsvValidationException { + var blocklist = new GeoIpBlocklist(); + + String country = blocklist.getCountry(InetAddress.getByName("federali.st")); + country = blocklist.getCountry(InetAddress.getByName("hugo.md")); + System.out.println(country); + + country = blocklist.getCountry(InetAddress.getByName("hugo.md")); + System.out.println(country); + } + + @Test + void isAllowed() throws CsvValidationException, IOException { + var blocklist = new IpBlockList(new GeoIpBlocklist()); + +// Assertions.assertFalse(blocklist.isAllowed(new EdgeDomain("localhost"))); +// Assertions.assertFalse(blocklist.isAllowed(new EdgeDomain("www.cloudflare.com"))); +// Assertions.assertTrue(blocklist.isAllowed(new EdgeDomain("https://marginalia.nu"))); + Assertions.assertTrue(blocklist.isAllowed(new EdgeDomain("federali.st"))); + Assertions.assertTrue(blocklist.isAllowed(new EdgeDomain("hugo.md"))); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/worker/UrlBlocklistTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/worker/UrlBlocklistTest.java new file mode 100644 index 00000000..7433d200 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawler/worker/UrlBlocklistTest.java @@ -0,0 +1,22 @@ +package nu.marginalia.wmsa.edge.crawler.worker; + +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.junit.jupiter.api.Test; + +import java.net.URISyntaxException; + +import static org.junit.jupiter.api.Assertions.*; + +class UrlBlocklistTest { + + @Test + void isUrlBlocked() throws URISyntaxException { + UrlBlocklist blocklist = new UrlBlocklist(); + assertTrue(blocklist.isUrlBlocked(new EdgeUrl("https://memex.marginalia.nu/ghc/ghc/blob/1b1067d14b656bbbfa7c47f156ec2700c9751549/compiler/main/UpdateCafInfos.hs"))); + assertTrue(blocklist.isUrlBlocked(new EdgeUrl("https://memex.marginalia.nu//gn/+/d62642c920e6a0d1756316d225a90fd6faa9e21e"))); + assertTrue(blocklist.isUrlBlocked(new EdgeUrl("http://yelenasimone.com/pdf/download-a-course-in-algebra.html"))); + assertFalse(blocklist.isUrlBlocked(new EdgeUrl("http://yelenasimone.com/nope/x-a-course-in-algebra.html"))); + assertTrue(blocklist.isUrlBlocked(new EdgeUrl("http://yelenasimone.com/_module/slide/pqPan/library/american-sour-beer-innovative-techniques-for-mixed-fermentations/"))); + assertTrue(blocklist.isUrlBlocked(new EdgeUrl("http://w-m-p.de/images/book/download-firstborn-starcraft-dark-templar-book-1.php"))); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawling/CrawlPlanLoaderTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawling/CrawlPlanLoaderTest.java new file mode 100644 index 00000000..40b77484 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawling/CrawlPlanLoaderTest.java @@ -0,0 +1,50 @@ +package nu.marginalia.wmsa.edge.crawling; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +class CrawlPlanLoaderTest { + + Path tempFile; + + @BeforeEach + public void setUp() throws IOException { + tempFile = Files.createTempFile(getClass().getSimpleName(), ".yaml"); + } + @AfterEach + public void tearDown() throws IOException { + Files.delete(tempFile); + } + + @Test + void load() throws IOException { + Files.writeString(tempFile, """ + jobSpec: "job.spec" + crawl: + dir: "/foo" + logName: "foo.log" + process: + dir: "/bar" + logName: "bar.log" + """); + var loader = new CrawlPlanLoader(); + var ret = loader.load(tempFile); + + assertEquals(Path.of("job.spec"), ret.getJobSpec()); + + assertEquals(Path.of("/foo"), ret.crawl.getDir()); + assertEquals(Path.of("/foo/foo.log"), ret.crawl.getLogFile()); + + assertEquals(Path.of("/bar"), ret.process.getDir()); + assertEquals(Path.of("/bar/bar.log"), ret.process.getLogFile()); + + System.out.println(ret); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawling/WorkLogTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawling/WorkLogTest.java new file mode 100644 index 00000000..bf5a6bdb --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/crawling/WorkLogTest.java @@ -0,0 +1,54 @@ +package nu.marginalia.wmsa.edge.crawling; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +class WorkLogTest { + Path outFile; + @BeforeEach + public void setUp() throws IOException { + outFile = Files.createTempFile(getClass().getSimpleName(), ".log"); + } + @AfterEach + public void tearDown() throws IOException { + Files.delete(outFile); + } + + @Test + public void testLog() throws IOException { + var log = new WorkLog(outFile); + log.setJobToFinished("A", "a.txt",1); + log.setJobToFinished("B", "b.txt",2); + log.setJobToFinished("C", "c.txt",3); + assertTrue(log.isJobFinished("A")); + assertTrue(log.isJobFinished("B")); + assertTrue(log.isJobFinished("C")); + assertFalse(log.isJobFinished("E")); + } + + @Test + public void testLogResume() throws Exception { + WorkLog log = new WorkLog(outFile); + log.setJobToFinished("A", "a.txt",1); + log.setJobToFinished("B", "b.txt",2); + log.setJobToFinished("C", "c.txt",3); + log.close(); + log = new WorkLog(outFile); + log.setJobToFinished("E", "e.txt",4); + assertTrue(log.isJobFinished("A")); + assertTrue(log.isJobFinished("B")); + assertTrue(log.isJobFinished("C")); + assertTrue(log.isJobFinished("E")); + log.close(); + + Files.readAllLines(outFile).forEach(System.out::println); + } + +} diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/data/EdgeDataStoreDaoTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/data/EdgeDataStoreDaoTest.java new file mode 100644 index 00000000..b2dff60a --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/data/EdgeDataStoreDaoTest.java @@ -0,0 +1,213 @@ +package nu.marginalia.wmsa.edge.data; + +import com.zaxxer.hikari.HikariDataSource; +import io.reactivex.rxjava3.functions.Consumer; +import lombok.SneakyThrows; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDaoImpl; +import nu.marginalia.wmsa.edge.data.dao.task.*; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainIndexingState; +import nu.marginalia.wmsa.edge.model.crawl.EdgeDomainLink; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlState; +import nu.marginalia.wmsa.edge.model.crawl.EdgeUrlVisit; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; + +import java.net.URISyntaxException; +import java.sql.ResultSet; +import java.sql.SQLException; + +import static nu.marginalia.util.TestUtil.evalScript; +import static nu.marginalia.util.TestUtil.getConnection; +import static org.junit.jupiter.api.Assertions.*; + +@ResourceLock(value = "mariadb", mode = ResourceAccessMode.READ_WRITE) +@Execution(ExecutionMode.SAME_THREAD) +@Tag("db") +class EdgeDataStoreDaoTest { + HikariDataSource dataSource; + private EdgeDataStoreTaskDaoImpl taskDao; + + + @SneakyThrows + public static HikariDataSource provideConnection() { + var conn = getConnection(); + + evalScript(conn, "sql/edge-crawler-cache.sql"); + + return conn; + } + + + @SneakyThrows + void query(String query, Consumer resultConsumer) { + try (var conn = dataSource.getConnection(); + var stmt = conn.createStatement() + ) { + resultConsumer.accept(stmt.executeQuery(query)); + + } catch (Throwable throwables) { + Assertions.fail(throwables); + } + } + @SneakyThrows + void update(String sql) { + try (var conn = dataSource.getConnection(); + var stmt = conn.createStatement() + ) { + stmt.executeUpdate(sql); + conn.commit(); + + } catch (Throwable throwables) { + Assertions.fail(throwables); + + } + } + + @SneakyThrows + @AfterEach + public void tearDownDb() { + dataSource.close(); + } + + + @SneakyThrows + @BeforeEach + public void setUpDb() { + dataSource = provideConnection(); + try (var connection = dataSource.getConnection()) { + + try (var stmt = connection.createStatement()) { + stmt.execute("DELETE FROM EC_URL"); + stmt.execute("DELETE FROM EC_DOMAIN_LINK"); + stmt.execute("DELETE FROM EC_DOMAIN"); + stmt.execute("DELETE FROM EC_URL_DETAILS"); + } + connection.commit(); + } + var ongoingJobs = new EdgeDataStoreTaskOngoingJobs(); + Initialization init = new Initialization(); + taskDao = new EdgeDataStoreTaskDaoImpl(dataSource, + new EdgeDomainBlacklistImpl(dataSource), + new EdgeDataStoreTaskTuner(dataSource), + ongoingJobs, + new EdgeFinishTasksQueue(dataSource, ongoingJobs), + init); + } + + @SneakyThrows + @Test + public void test() { + try (var connection = dataSource.getConnection()) { + var ds = new EdgeDataStoreDaoImpl(dataSource); + assertFalse(ds.isBlacklisted(new EdgeDomain("https://www.marginalia.nu"))); + } + } + + @Test + void putLink() throws SQLException { + try (var connection = dataSource.getConnection()) { + var ds = new EdgeDataStoreDaoImpl(dataSource); + ds.putLink( + false, new EdgeDomainLink(new EdgeDomain("https://www.marginalia.nu"), + new EdgeDomain("https://www.marginalia.nu") + )); + var res = connection.createStatement().executeQuery("SELECT * FROM EC_DOMAIN_LINK"); + res.next(); + assertEquals(res.getString(1), res.getString(2)); + } + } + + @SneakyThrows + @Test + void putUrl() { + var ds = new EdgeDataStoreDaoImpl(dataSource); + ds.putUrl(-2, new EdgeUrl("https://www.marginalia.nu/")); + ds.putUrl(-2, new EdgeUrl("https://www.marginalia.nu/robots.txt")); + ds.putUrl(-2, new EdgeUrl("https://www.marginalia.nu/sitemap.xml")); + ds.putUrl(-2, new EdgeUrl("https://marginalia.nu/")); + ds.putUrl(-2, new EdgeUrl("https://marginalia.nu/robots.txt")); + ds.putUrl(-2, new EdgeUrl("https://marginalia.nu/sitemap.xml")); + + taskDao.getIndexTask(0, 100).urls.forEach(System.out::println); + taskDao.finishIndexTask(new EdgeDomain("https://www.marginalia.nu/"), 0.5, EdgeDomainIndexingState.ACTIVE); + System.out.println("-"); + taskDao.getIndexTask(0, 100).urls.forEach(System.out::println); + } + + + @SneakyThrows + @Test + void putUrlVisit() { + var ds = new EdgeDataStoreDaoImpl(dataSource); + + var url = new EdgeUrl("https://www.marginalia.nu/"); + ds.putUrl(-2, url); + ds.putUrlVisited(new EdgeUrlVisit(url, 255, -2., "Bob's Website", "A homepage", "", "test", 0,0, 0, EdgeUrlState.OK)); + var deets = ds.getUrlDetails(ds.getUrlId(url)); + assertEquals(-2., deets.urlQuality); + assertEquals("Bob's Website", deets.title); + assertEquals("A homepage", deets.description); + System.out.println(deets); + } + + @Test + void getDomainId() throws URISyntaxException { + var ds = new EdgeDataStoreDaoImpl(dataSource); + var domain = new EdgeDomain("www.marginalia.nu"); + var url = new EdgeUrl("https://www.marginalia.nu/"); + + ds.putUrl(-2, url); + var id = ds.getDomainId(domain); + assertEquals(domain, ds.getDomain(id)); + } + + @Test + public void setDomainAlias() throws URISyntaxException { + var ds = new EdgeDataStoreDaoImpl(dataSource); + + ds.putUrl(1.0, new EdgeUrl("https://marginalia.nu/")); + + ds.putDomainAlias(new EdgeDomain("marginalia.nu"), new EdgeDomain("www.marginalia.nu")); + + query("SELECT COUNT(*) FROM EC_DOMAIN", res -> { + assertTrue(res.next()); + assertEquals(2, res.getInt(1)); + }); + + query("SELECT COUNT(DISTINCT(QUALITY)) FROM EC_DOMAIN", res -> { + assertTrue(res.next()); + assertEquals(1, res.getInt(1)); + }); + + query("SELECT URL_PART, DOMAIN_ALIAS FROM EC_DOMAIN", res -> { + + while (res.next()) { + System.out.println(res.getString(1) + ":" + res.getString(2)); + switch (res.getString(1)) { + case "https://marginalia.nu": + assertNotNull(res.getString(2)); + break; + case "https://www.marginalia.nu": + assertNull(res.getString(2)); + break; + } + } + }); + } + + @Test + void getUrlId() throws URISyntaxException { + var ds = new EdgeDataStoreDaoImpl(dataSource); + var url = new EdgeUrl("https://www.marginalia.nu/"); + + ds.putUrl(-2, url); + var id = ds.getUrlId(url); + assertEquals(url, ds.getUrl(id)); + } + +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/DictionaryWriterTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/DictionaryWriterTest.java new file mode 100644 index 00000000..cf497193 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/DictionaryWriterTest.java @@ -0,0 +1,213 @@ +package nu.marginalia.wmsa.edge.index.service; + +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.index.service.dictionary.DictionaryReader; +import nu.marginalia.wmsa.edge.index.service.dictionary.DictionaryWriter; +import nu.marginalia.wmsa.edge.index.service.index.SearchIndexConverter; +import nu.marginalia.wmsa.edge.index.service.query.SearchIndexPartitioner; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +class DictionaryWriterTest { + /* + @Test @Disabled + + public void analyze2() throws IOException { + System.out.println("Loading dictionary"); + var dr = new DictionaryReader(null, new File("/home/vlofgren/dictionary.dat")); + System.out.println("Loading indices"); + var reader = new SearchIndexReader(new SearchIndex("test", Path.of("/tmp"), + new File("/tmp/urls-0"), + new File("/tmp/words-0")), + new SearchIndex("test", Path.of("/tmp"), + new File("/tmp/urls-24"), + new File("/tmp/words-24"))); + System.out.println("Gogo"); + long hitsTotal = 0L; + try (var wr = new PrintWriter(new FileOutputStream("/home/vlofgren/words-count"))) { + hitsTotal = dr.stream().mapToLong(w -> { + long hits = reader.numHits(dr.get(w)); + wr.printf("%08d %s\n", hits, w); + return hits; + }).sum(); + } + System.out.println(hitsTotal); + } + */ + @Test @Disabled + public void convert() { + new SearchIndexConverter(IndexBlock.Title, 0, Path.of("/tmp"), + new File("/home/vlofgren/page-index-0.dat"), + new File("/tmp/words-0"), + new File("/tmp/urls-0"), + new SearchIndexPartitioner(null), + val -> false); + } + @SneakyThrows + @Test + @Disabled + void test() { + try (var dict = new DictionaryWriter(Path.of("/home/vlofgren/Code/data/dictionary.dat").toFile(), 1L<<16, false)) { + wait(); + } + } + + + @SneakyThrows + @Test + void getFold() { + var path = Files.createTempFile("dict", ".tmp"); + try (var dict = new DictionaryWriter(path.toFile(), 1L<<16, false)) { + dict.get("hic"); + dict.get("hac"); + dict.commitToDisk(); + dict.get("quae"); + dict.get("quis"); + dict.get("quem1"); + dict.get("quem2"); + dict.get("quem3"); + dict.get("quem4"); + dict.get("quem5"); + dict.get("quem6"); + dict.get("quem7"); + dict.get("quem8"); + dict.get("quem9"); + dict.get("quem10"); + dict.get("cuis"); + dict.get("haec_hic"); + dict.get("hoc_hac_cuis"); + dict.commitToDisk(); + assertNotEquals(0, dict.get("hac")); + assertEquals(0, dict.get("hic")); + } + + try (var dict = new DictionaryWriter(path.toFile(), 1L<<16, false)) { + assertNotEquals(0, dict.get("hoc")); + assertEquals(0, dict.get("hic")); + } + + path.toFile().delete(); + } + + @SneakyThrows + @Test + void get() { + var path = Files.createTempFile("dict", ".tmp"); + try (var dict = new DictionaryWriter(path.toFile(), 1L<<16, false)) { + dict.get("hic"); + dict.get("hac"); + dict.get("haec"); + dict.get("hoc"); + dict.commitToDisk(); + dict.get("quae"); + dict.get("quis"); + dict.get("quem"); + dict.get("cuis"); + dict.commitToDisk(); + assertNotEquals(0, dict.get("hac")); + assertEquals(0, dict.get("hic")); + } + + try (var dict = new DictionaryWriter(path.toFile(), 1L<<16, false)) { + assertNotEquals(0, dict.get("hoc")); + assertEquals(0, dict.get("hic")); + } + + path.toFile().delete(); + } + + @SneakyThrows + @Test + void getDoubleWrite() { + var path = Files.createTempFile("dict", ".tmp"); + + try (var dict = new DictionaryWriter(path.toFile(), 1L<<16, false)) { + dict.commitToDisk(); + } + + try (var dict = new DictionaryWriter(path.toFile(), 1L<<16, false)) { + dict.get("hic"); + dict.get("hac"); + dict.get("haec"); + dict.get("hoc"); + dict.get("quae"); + dict.get("quis"); + dict.get("quem"); + dict.get("cuis"); + dict.commitToDisk(); + assertNotEquals(0, dict.get("hac")); + assertEquals(0, dict.get("hic")); + } + + var dict = new DictionaryReader(new DictionaryWriter(path.toFile(), 1L<<16, false)); + + assertNotEquals(0, dict.get("hoc")); + assertEquals(0, dict.get("hic")); + + path.toFile().delete(); + } + + @SneakyThrows + @Test + void getDoubleWrite2() { + var path = Files.createTempFile("dict", ".tmp"); + + try (var dict = new DictionaryWriter(path.toFile(), 1L<<16, false)) { + dict.get("hic"); + dict.get("hac"); + dict.get("haec"); + dict.get("hoc"); + dict.get("quae"); + dict.get("quis"); + dict.get("quem"); + dict.get("cuis"); + dict.commitToDisk(); + assertNotEquals(0, dict.get("hac")); + assertEquals(0, dict.get("hic")); + } + + try (var dict = new DictionaryWriter(path.toFile(), 1L<<16, false)) { + dict.get("fe"); + dict.get("fi"); + dict.get("fo"); + dict.get("fum"); + dict.commitToDisk(); + assertNotEquals(0, dict.get("hac")); + assertEquals(0, dict.get("hic")); + } + + try (var dict = new DictionaryWriter(path.toFile(), 1L<<16, false)) { + dict.get("bip"); + dict.get("bap"); + dict.commitToDisk(); + } + + + var dict = new DictionaryReader(new DictionaryWriter(path.toFile(), 1L<<16, false)); + + assertEquals(0, dict.get("hic")); + assertEquals(1, dict.get("hac")); + assertEquals(2, dict.get("haec")); + assertEquals(3, dict.get("hoc")); + assertEquals(4, dict.get("quae")); + assertEquals(5, dict.get("quis")); + assertEquals(6, dict.get("quem")); + assertEquals(7, dict.get("cuis")); + assertEquals(8, dict.get("fe")); + assertEquals(9, dict.get("fi")); + assertEquals(10, dict.get("fo")); + assertEquals(11, dict.get("fum")); + assertEquals(12, dict.get("bip")); + assertEquals(13, dict.get("bap")); + path.toFile().delete(); + } + +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/EdgeIndexClientTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/EdgeIndexClientTest.java new file mode 100644 index 00000000..bd3b194c --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/EdgeIndexClientTest.java @@ -0,0 +1,187 @@ +package nu.marginalia.wmsa.edge.index.service; + +import com.zaxxer.hikari.HikariDataSource; +import lombok.SneakyThrows; +import nu.marginalia.util.TestUtil; +import nu.marginalia.wmsa.client.exception.RemoteException; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.edge.index.EdgeIndexService; +import nu.marginalia.wmsa.edge.index.IndexServicesFactory; +import nu.marginalia.wmsa.edge.index.client.EdgeIndexClient; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.index.service.query.SearchIndexPartitioner; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageWordSet; +import nu.marginalia.wmsa.edge.model.search.EdgeSearchSpecification; +import nu.marginalia.wmsa.edge.model.EdgeId; +import nu.marginalia.wmsa.edge.model.crawl.EdgePageWords; +import nu.marginalia.wmsa.edge.model.EdgeUrl; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import spark.Spark; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static nu.marginalia.util.TestUtil.getConnection; +import static nu.marginalia.wmsa.edge.index.EdgeIndexService.DYNAMIC_BUCKET_LENGTH; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ResourceLock(value = "mariadb", mode = ResourceAccessMode.READ_WRITE) +@Execution(ExecutionMode.SAME_THREAD) +@Tag("db") +public class EdgeIndexClientTest { + private static HikariDataSource dataSource; + private static EdgeIndexService service; + private static EdgeIndexClient client; + private static Path tempDir; + private static SearchIndexes indexes; + + @SneakyThrows + public static HikariDataSource provideConnection() { + return getConnection(); + } + + static int testPort = TestUtil.getPort(); + + @SneakyThrows + @BeforeAll + public static void setUpClass() { + Spark.port(testPort); + System.setProperty("service-name", "edge-index"); + + dataSource = provideConnection(); + dataSource.setKeepaliveTime(100); + dataSource.setIdleTimeout(100); + client = new EdgeIndexClient(); + client.setServiceRoute("127.0.0.1", testPort); + + tempDir = Files.createTempDirectory("EdgeIndexClientTest"); + + var servicesFactory = new IndexServicesFactory(tempDir,tempDir,tempDir,tempDir, + "writer-index", + "writer-dictionary", + "index-words-read", + "index-urls-read", + "index-words-write", + "index-urls-write", + 1L<<24, + id->false, + new SearchIndexPartitioner(null) + ); + + indexes = new SearchIndexes(servicesFactory, new SearchIndexPartitioner(null)); + service = new EdgeIndexService("127.0.0.1", + testPort, + new Initialization(), null, + indexes); + + Spark.awaitInitialization(); + } + + @Test + public void testMultiBucketHit() { + putWords(1, 1, -2, "fancy", "anagram", "dilbert", "whoah", "engram"); + putWords(2, 2, -5, "quibble", "angry", "whoah", "fancy"); + putWords(3, 3, -0.01, "strong", "manly", "muscles"); + indexes.repartition(); + indexes.preconvert(); + indexes.reindexAll(); + + var results = client.query(Context.internal(), EdgeSearchSpecification.justIncludes("fancy")).resultsList.get(IndexBlock.Title).get(0).results; + System.out.println(results); + List> flatResults = results.values().stream().flatMap(List::stream).map(rs -> rs.url).collect(Collectors.toList()); + + assertEquals(2, flatResults.size()); + assertTrue(flatResults.contains(new EdgeId(1))); + assertTrue(flatResults.contains(new EdgeId(2))); + } + + @Test + public void testLowHit() { + putWords(1, 4, 0, "elmoped"); + indexes.repartition(); + indexes.preconvert(); + indexes.reindexAll(); + var rsp = client.query(Context.internal(), EdgeSearchSpecification.justIncludes("elmoped")); + System.out.println(rsp); + assertEquals(4, rsp.resultsList.get(0).get(0).results.get(DYNAMIC_BUCKET_LENGTH).get(0).url.getId()); + } + + @Test + public void testHighHit() { + putWords(2, 5, -100, "trapphus"); + indexes.repartition(); + indexes.preconvert(); + indexes.reindexAll(); + var rsp = client.query(Context.internal(), EdgeSearchSpecification.justIncludes("trapphus")); + System.out.println(rsp); + assertEquals(5, rsp.resultsList.get(0).get(0).results.get(0).get(0).url.getId()); + } + + + @Test + public void testSearchDomain() { + putWords(8, 1, -2, "domain"); + putWords(8, 2, -5, "domain"); + putWords(10, 3, -0.01, "domain"); + putWords(11, 3, -0.01, "domain"); + putWords(12, 3, -0.01, "domain"); + indexes.repartition(); + indexes.preconvert(); + indexes.reindexAll(); + + var results = client.query(Context.internal(), EdgeSearchSpecification.justIncludes("fancy")).resultsList.get(IndexBlock.Title).get(0).results; + System.out.println(results); + List> flatResults = results.values().stream().flatMap(List::stream).map(rs -> rs.url).collect(Collectors.toList()); + + assertEquals(2, flatResults.size()); + assertTrue(flatResults.contains(new EdgeId(1))); + assertTrue(flatResults.contains(new EdgeId(2))); + } + + @Test + public void miss() { + indexes.repartition(); + indexes.preconvert(); + indexes.reindexAll(); + + try { + client.query(Context.internal(), EdgeSearchSpecification.justIncludes("skumtomte")); + Assertions.fail(); + } + catch (RemoteException ex) { + + } + } + + void putWords(int didx, int idx, double quality, String... words) { + EdgePageWords epw = new EdgePageWords(IndexBlock.Words); + epw.addAll(Arrays.asList(words)); + client.putWords(Context.internal(), new EdgeId<>(didx), new EdgeId<>(idx), quality, + new EdgePageWordSet(epw), 0).blockingSubscribe(); + } + + @AfterAll + public static void tearDownClass() { + for (File f : tempDir.toFile().listFiles()) { + if (f.isDirectory()) { + for (File f2 : f.listFiles()) { + f2.delete(); + } + } + f.delete(); + } + + tempDir.toFile().delete(); + } + +} diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/EdgeSearchTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/EdgeSearchTest.java new file mode 100644 index 00000000..216bc1f3 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/EdgeSearchTest.java @@ -0,0 +1,524 @@ +package nu.marginalia.wmsa.edge.index.service; + +import com.opencsv.exceptions.CsvValidationException; +import com.zaxxer.hikari.HikariDataSource; +import lombok.SneakyThrows; +import nu.marginalia.util.TestUtil; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.data_store.DataStoreService; +import nu.marginalia.wmsa.data_store.EdgeDataStoreService; +import nu.marginalia.wmsa.data_store.FileRepository; +import nu.marginalia.wmsa.data_store.client.DataStoreClient; +import nu.marginalia.wmsa.edge.archive.client.ArchiveClient; +import nu.marginalia.wmsa.edge.assistant.dict.DictionaryService; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.assistant.dict.SpellChecker; +import nu.marginalia.wmsa.edge.assistant.eval.MathParser; +import nu.marginalia.wmsa.edge.assistant.eval.Units; +import nu.marginalia.wmsa.edge.assistant.EdgeAssistantService; +import nu.marginalia.wmsa.edge.assistant.client.AssistantClient; +import nu.marginalia.wmsa.edge.assistant.screenshot.ScreenshotService; +import nu.marginalia.wmsa.edge.crawler.domain.DomainCrawler; +import nu.marginalia.wmsa.edge.crawler.domain.DomainCrawlerRobotsTxt; +import nu.marginalia.wmsa.edge.crawler.domain.LinkParser; +import nu.marginalia.wmsa.edge.crawler.domain.language.LanguageFilter; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.DocumentKeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlFeature; +import nu.marginalia.wmsa.edge.crawler.domain.processor.HtmlProcessor; +import nu.marginalia.wmsa.edge.crawler.fetcher.HttpFetcher; +import nu.marginalia.wmsa.edge.crawler.worker.GeoIpBlocklist; +import nu.marginalia.wmsa.edge.crawler.worker.IpBlockList; +import nu.marginalia.wmsa.edge.data.dao.*; +import nu.marginalia.wmsa.edge.data.dao.task.*; +import nu.marginalia.wmsa.edge.index.EdgeIndexService; +import nu.marginalia.wmsa.edge.index.IndexServicesFactory; +import nu.marginalia.wmsa.edge.index.client.EdgeIndexClient; +import nu.marginalia.wmsa.edge.index.service.query.SearchIndexPartitioner; +import nu.marginalia.util.ParallelPipe; +import nu.marginalia.wmsa.edge.integration.model.BasicDocumentData; +import nu.marginalia.wmsa.edge.integration.stackoverflow.StackOverflowPostProcessor; +import nu.marginalia.wmsa.edge.integration.stackoverflow.StackOverflowPostsReader; +import nu.marginalia.wmsa.edge.integration.stackoverflow.model.StackOverflowPost; +import nu.marginalia.wmsa.edge.integration.wikipedia.WikipediaProcessor; +import nu.marginalia.wmsa.edge.integration.wikipedia.WikipediaReader; +import nu.marginalia.wmsa.edge.integration.wikipedia.model.WikipediaArticle; +import nu.marginalia.wmsa.edge.model.*; +import nu.marginalia.wmsa.edge.model.crawl.*; +import nu.marginalia.wmsa.edge.search.EdgeSearchOperator; +import nu.marginalia.wmsa.edge.search.EdgeSearchService; +import nu.marginalia.wmsa.edge.search.UnitConversion; +import nu.marginalia.wmsa.edge.search.query.*; +import nu.marginalia.wmsa.edge.search.results.SearchResultDecorator; +import nu.marginalia.wmsa.edge.search.results.SearchResultValuator; +import nu.marginalia.wmsa.renderer.mustache.RendererFactory; +import org.jsoup.Jsoup; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Spark; + +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.LocalDateTime; +import java.util.*; + +import static nu.marginalia.util.TestUtil.evalScript; +import static nu.marginalia.util.TestUtil.getConnection; + +@ResourceLock(value = "mariadb", mode = ResourceAccessMode.READ_WRITE) +@Execution(ExecutionMode.SAME_THREAD) +@Tag("db") +public class EdgeSearchTest { + private static HikariDataSource dataSource; + private static EdgeIndexService indexService; + private static EdgeIndexClient indexClient; + private static Path tempDir; + private static EdgeDataStoreDao edgeStoreDao; + private static SentenceExtractor sentenceExtractor; + private static DocumentKeywordExtractor documentKeywordExtractor; + private static StackOverflowPostProcessor stackOverflowPostProcessor; + private static WikipediaProcessor wikipediaProcessor; + private static SearchIndexes indexes; + + Logger logger = LoggerFactory.getLogger(getClass()); + @SneakyThrows + public static HikariDataSource provideConnection() { + return getConnection(); + } + + static int testPort = TestUtil.getPort(); + + static Initialization init = new Initialization(); + private QueryParser parser; + private static NGramDict dict; + private static LanguageModels lm = new LanguageModels( + Path.of("/home/vlofgren/Work/ngrams/ngrams-generous-emstr.bin"), + Path.of("/home/vlofgren/Work/ngrams/tfreq-new-algo3.bin"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-sentence-1.0-1.9.3.bin"), + Path.of("/home/vlofgren/Work/ngrams/English.RDR"), + Path.of("/home/vlofgren/Work/ngrams/English.DICT"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-tokens-1.0-1.9.3.bin") + ); + + + @SneakyThrows + @BeforeAll + public static void setUpClass() { + Spark.port(testPort); + System.setProperty("service-name", "edge-index"); + System.setProperty("unit-test", "TRUE"); + Spark.staticFileLocation("/static/edge/"); + + dict = new NGramDict(lm); + + dataSource = provideConnection(); + dataSource.setKeepaliveTime(100); + dataSource.setIdleTimeout(100); + + indexClient = new EdgeIndexClient(); + indexClient.setServiceRoute("127.0.0.1", testPort); + + AssistantClient assistantClient = new AssistantClient(); + assistantClient.setServiceRoute("127.0.0.1", testPort); + + var dataStoreClient = new DataStoreClient(); + dataStoreClient.setServiceRoute("127.0.0.1", testPort); + tempDir = Files.createTempDirectory("EdgeIndexClientTest"); + + var servicesFactory = new IndexServicesFactory(tempDir,tempDir,tempDir,tempDir, + "writer-index", + "writer-dictionary", + "index-words-read", + "index-urls-read", + "index-words-write", + "index-urls-write", + 1L<<24, + id->false, + new SearchIndexPartitioner(null) + ); + + servicesFactory.getDictionaryWriter().noCommit = true; + + edgeStoreDao = new EdgeDataStoreDaoImpl(dataSource); + + sentenceExtractor = new SentenceExtractor(lm); + documentKeywordExtractor = new DocumentKeywordExtractor(new NGramDict(lm)); + + stackOverflowPostProcessor = new StackOverflowPostProcessor(sentenceExtractor, documentKeywordExtractor); + wikipediaProcessor = new WikipediaProcessor(sentenceExtractor, documentKeywordExtractor); + + var valuator = new SearchResultValuator(dict); + EdgeSearchService searchService = new EdgeSearchService("127.0.0.1", testPort, + edgeStoreDao, indexClient, + new RendererFactory(), new Initialization(), null, + dataStoreClient, assistantClient, new UnitConversion(assistantClient), + new EdgeSearchOperator(assistantClient, edgeStoreDao, indexClient, new QueryFactory(lm, dict, new EnglishDictionary(dict)), new SearchResultDecorator(edgeStoreDao, valuator), valuator), + new EdgeDomainBlacklistImpl(dataSource), new ScreenshotService(edgeStoreDao)); + + EdgeAssistantService assistantService = new EdgeAssistantService("127.0.0.1", testPort, Initialization.already(), null, + new DictionaryService(dataSource, new SpellChecker()), new MathParser(), + new Units(new MathParser()), null, null, + new ScreenshotService(edgeStoreDao), null); + + indexes = new SearchIndexes(servicesFactory, new SearchIndexPartitioner(null)); + + indexService = new EdgeIndexService("127.0.0.1", + testPort, + init, + null, + indexes); + + new DataStoreService("127.0.0.1", testPort, new FileRepository(), dataSource, new EdgeDataStoreService(new EdgeDataStoreDaoImpl(dataSource)), Initialization.already(), null); + + + Spark.awaitInitialization(); + } + + + @SneakyThrows + @BeforeEach + public void clearDb() { + evalScript(dataSource, "sql/data-store-init.sql"); + evalScript(dataSource, "sql/edge-crawler-cache.sql"); + evalScript(dataSource, "sql/reference-data.sql"); + + try (var connection = dataSource.getConnection()) { + + try (var stmt = connection.createStatement()) { + Assertions.assertTrue(stmt.executeUpdate("DELETE FROM EC_URL") >= 0); + Assertions.assertTrue(stmt.executeUpdate("DELETE FROM EC_DOMAIN_LINK") >= 0); + Assertions.assertTrue(stmt.executeUpdate("DELETE FROM EC_DOMAIN") >= 0); + } + } + } + + @Test @Disabled + public void getUrls() throws IOException { + var doc = Jsoup.parse(new URL("https://search.marginalia.nu/search?query=putty%20ssh%20download"), 1000); + + doc.select(".teknisk a").stream().map(e -> e.attr("href")).forEach( + href -> { + try { + var path = Path.of("/home/vlofgren/Code/tmp-data/").resolve("url-"+href.hashCode()); + if (!Files.exists(path)) { + var doc2 =Jsoup.parse(new URL(href), 9000); + Files.writeString(path, doc2.outerHtml(), StandardOpenOption.CREATE_NEW); + } + + } catch (IOException e) { + e.printStackTrace(); + } + } + ); + } + + + + HtmlProcessor processor = new HtmlProcessor(new DocumentKeywordExtractor(new NGramDict(lm)),new SentenceExtractor(lm)); + + @SneakyThrows + @Test + public void justLoadUrls() { + var data = Path.of("/home/vlofgren/Code/tmp-data/"); + for (var file : Objects.requireNonNull(data.toFile().listFiles())) { + testNgram(file.toPath()); + } +// cralUrl(new EdgeUrl("https://memex.marginalia.nu/"), 5); +// loadFile(Path.of("/home/vlofgren/Work/tmp.html")); + } + + + @SneakyThrows + @Test + // @Disabled + public void runStackOverflow() { + var data = Path.of("/home/vlofgren/Code/tmp-data/"); + + + var pipe = new ParallelPipe("pipe", 32, 5, 2) { + @Override + public BasicDocumentData onProcess(StackOverflowPost stackOverflowPost) { + return stackOverflowPostProcessor.process(stackOverflowPost); + } + + @Override + public void onReceive(BasicDocumentData stackOverflowIndexData) { + loadStackOverflowPost(stackOverflowIndexData); + } + }; + + var reader = new StackOverflowPostsReader("/mnt/storage/downloads.new/stackexchange/sites/philosophy/Posts.xml", + new EdgeDomain("philosophy.stackexchange.com"), pipe::accept); + reader.join(); + + init.setReady(); + indexService.initialize(); + + while (!indexes.repartition()); + while (!indexes.preconvert()); + while (!indexes.reindexAll()); + + System.err.println("http://localhost:"+testPort + "/public/search?query=putty%20ssh%20download%20site:localhost"); + Thread.currentThread().join(); + } + + @SneakyThrows + @Test + // @Disabled + public void runWikipedia() { + var data = Path.of("/home/vlofgren/Code/tmp-data/"); + + var reader = new WikipediaReader("/home/vlofgren/Work/wikipedia_en_100_nopic_2021-06.zim", new EdgeDomain("encyclopedia.marginalia.nu"), + this::loadWikipediaPost); + reader.join(); + + init.setReady(); + indexService.initialize(); + + while (!indexes.repartition()); + while (!indexes.preconvert()); + while (!indexes.reindexAll()); + + System.err.println("http://localhost:"+testPort + "/public/search?query=putty%20ssh%20download%20site:localhost"); + Thread.currentThread().join(); + } + + final LinkParser lp = new LinkParser(); + + + private void loadStackOverflowPost(BasicDocumentData indexData) { + + var url = indexData.getUrl(); + + edgeStoreDao.putUrl(-2, url); + edgeStoreDao.putUrlVisited(new EdgeUrlVisit(url, indexData.hashCode, -2., + indexData.getTitle(), + indexData.getDescription() + , "", + EdgeHtmlStandard.HTML5.toString(), + 1 << HtmlFeature.JS.bit, + 1000, 1000, EdgeUrlState.OK)); + edgeStoreDao.putLink(false, indexData.domainLinks); + + putWords(edgeStoreDao.getDomainId(url.domain).getId(), + edgeStoreDao.getUrlId(url).getId(), + -2, + indexData.words); + } + + private void loadWikipediaPost(WikipediaArticle post) { + + var indexData = wikipediaProcessor.process(post); + + var url = indexData.getUrl(); + + edgeStoreDao.putUrl(-2, url); + edgeStoreDao.putUrlVisited(new EdgeUrlVisit(url, post.body.hashCode(), -2., + indexData.getTitle(), + indexData.getDescription() + , "", + EdgeHtmlStandard.HTML5.toString(), + 1 << HtmlFeature.JS.bit, + 1000, 1000, EdgeUrlState.OK)); + edgeStoreDao.putLink(false, indexData.domainLinks); + + putWords(edgeStoreDao.getDomainId(url.domain).getId(), + edgeStoreDao.getUrlId(url).getId(), + -2, + indexData.words); + } + + @SneakyThrows + @Test + // @Disabled + public void run() { + var data = Path.of("/home/vlofgren/Code/tmp-data/"); + for (var file : Objects.requireNonNull(data.toFile().listFiles())) { + loadFile(file.toPath()); + } + +// cralUrl(new EdgeUrl("https://search.marginalia.nu/"), 5); +// cralUrl(new EdgeUrl("https://memex.marginalia.nu/"), 5); + +// loadUrl("https://reddit.marginalia.nu", "/reddit/login.html"); + + var conn = getConnection(); + ArrayList ids = new ArrayList<>(); + try (var c = conn.getConnection()) { + var stmt = c.prepareStatement("SELECT ID FROM EC_DOMAIN"); + + var rsp = stmt.executeQuery(); + while (rsp.next()) { + ids.add(rsp.getInt(1)); + } + + for (int i = 0; i < ids.size(); i++) { + try (var s2 = c.prepareStatement("INSERT INTO EC_DOMAIN_NEIGHBORS(DOMAIN_ID, NEIGHBOR_ID, ADJ_IDX) VALUES (?,?,?)")) { + for (int j = 0; j < 15; j++) { + s2.setInt(1, ids.get(i)); + s2.setInt(2, ids.get((int)(ids.size()*Math.random()))); + s2.setInt(3, j); + s2.addBatch(); + } + s2.executeBatch(); + } + } + + } + + init.setReady(); + indexService.initialize(); + + while (!indexes.repartition()); + while (!indexes.preconvert()); + while (!indexes.reindexAll()); + + System.err.println("http://localhost:"+testPort + "/public/search?query=putty%20ssh%20download%20site:localhost"); + Thread.currentThread().join(); + } + + @SneakyThrows + private void loadUrl(String uri) { + try { + var doc = Jsoup.parse(new URL(uri), 5000); + var res = processor.processHtmlPage(new EdgeRawPageContents(new EdgeUrl(new URI(uri)), new EdgeUrl(new URI(uri)), "", null, "", true, + LocalDateTime.now().toString()), + doc); + var url = new EdgeUrl(uri); + edgeStoreDao.putUrl(-2, url); + edgeStoreDao.putUrlVisited(new EdgeUrlVisit(url, 5, -2., + res.metadata.title, + res.metadata.description, "", + res.metadata.htmlStandard.toString(), + res.metadata.features, + 0, 0, EdgeUrlState.OK)); + + + logger.info("LW: {}", res.linkWords); + putWords(edgeStoreDao.getDomainId(url.domain).getId(), + edgeStoreDao.getUrlId(url).getId(), + -2, + res.words); + } + catch (SocketTimeoutException ex) { + ex.printStackTrace(); + } + } + + private void cralUrl(EdgeUrl url, int pass) throws CsvValidationException, IOException { + var fetcher = new HttpFetcher("search.marginalia.nu"); + var ingress = new EdgeIndexTask(url.domain, pass, 100, 1.); + ingress.urls.add(url); + DomainCrawler dc = new DomainCrawler(fetcher, null, processor, Mockito.mock(ArchiveClient.class), new DomainCrawlerRobotsTxt(fetcher, "search.marginalia.nu") + , new LanguageFilter(), ingress , new IpBlockList(new GeoIpBlocklist())); + System.err.println("Crawling " + url); + var cr = dc.crawl(); + System.err.println("Crawled " + url); + cr.pageContents.values().forEach(res -> { + logger.info("Put URL {} {}%", res.url, 100*Math.exp(res.metadata.quality())); + edgeStoreDao.putUrl(res.metadata.quality(), res.url); + edgeStoreDao.putUrlVisited(new EdgeUrlVisit(res.url, res.hash, res.metadata.quality(), + res.metadata.title, + res.metadata.description, "", + res.metadata.htmlStandard.toString(), + res.metadata.features, res.metadata.textDistinctWords, res.metadata.totalWords, EdgeUrlState.OK)); + + putWords(edgeStoreDao.getDomainId(res.url.domain).getId(), + edgeStoreDao.getUrlId(res.url).getId(), + -2, + res.words); + }); + } + + private void testNgram(Path path) throws IOException, URISyntaxException { + var doc = Jsoup.parse(Files.readString(path)); + doc.getElementsByTag("a").remove(); + String text = doc.text(); + + var res = processor.processHtmlPage(new EdgeRawPageContents(new EdgeUrl("http://www.example.com/"),new EdgeUrl("http://www.example.com/"), "", null, "", true, + LocalDateTime.now().toString()), + doc); + if (null == res) { + return; + } + System.out.println(doc.getElementsByTag("title").text()); + System.out.println(res.metadata.description); + System.out.println("---"); + +// System.out.println(Optional.ofNullable(doc.getElementsByTag("h1")).map(Elements::first).map(Element::text).orElse("")); +// +// System.out.println(res.words.get(IndexBlock.Topic_Names)); +// System.out.println(res.words.get(IndexBlock.Title_Names)); +// System.out.println(res.words.get(IndexBlock.Body_Names)); +// System.out.println(res.words.get(IndexBlock.TextRank)); +// System.out.println(res.words.get(IndexBlock.Keywords)); +// System.out.println(res.words.get(IndexBlock.Names)); +// System.out.println(res.words.get(IndexBlock.Title)); +// System.out.println(res.words.get(IndexBlock.Topic)); +// System.out.println(res.words.get(IndexBlock.Body)); +// +// var multi = new HashSet<>(); +// var trs = new HashSet<>(res.words.get(IndexBlock.TextRank).words); +// multi.addAll(Sets.intersection(trs,new HashSet<>(res.words.get(IndexBlock.Names).words))); +// multi.addAll(Sets.intersection(trs,new HashSet<>(res.words.get(IndexBlock.Title).words))); +// multi.addAll(Sets.intersection(trs,new HashSet<>(res.words.get(IndexBlock.Topic).words))); +// var uniq = new HashSet<>(Sets.difference(trs, multi)); +// res.words.get(IndexBlock.Body_Names).words.forEach(multi::remove); +// res.words.get(IndexBlock.Topic_Names).words.forEach(multi::remove); +// res.words.get(IndexBlock.Title_Names).words.forEach(multi::remove); +// System.out.println(multi); +// System.out.println(uniq); +// System.out.println("--\n--\n"); + + } + + private void loadFile(Path path) throws IOException, URISyntaxException { + var doc = Jsoup.parse(Files.readString(path)); + var url = new EdgeUrl("http://" + Math.abs(path.hashCode()) + ".example.com/"); + var res = processor.processHtmlPage(new EdgeRawPageContents(url, url, "", null, (int)(Math.random()*255)+"."+(int)(Math.random()*255)+"."+(int)(Math.random()*255), Math.random() > 0.5, LocalDateTime.now().toString()), + doc); + if (null == res) { + System.err.println("*** did not insert " + path); + return; + } + edgeStoreDao.putUrl(-2, url); + edgeStoreDao.putUrlVisited(new EdgeUrlVisit(url, 5, -2., + res.metadata.title, + res.metadata.description, res.ipAddress, + res.metadata.htmlStandard.toString(), + res.metadata.features, + 0, 0, EdgeUrlState.OK)); + + logger.info("LW: {}", res.linkWords); + putWords(edgeStoreDao.getDomainId(url.domain).getId(), + edgeStoreDao.getUrlId(url).getId(), + -2, + res.words + ); + + } + void putWords(int didx, int idx, double quality, EdgePageWordSet wordsSet) { + indexClient.putWords(Context.internal(), new EdgeId<>(didx), new EdgeId<>(idx), quality, + wordsSet, 0).blockingSubscribe(); + } + + @AfterAll + public static void tearDownClass() { + nu.marginalia.util.test.TestUtil.clearTempDir(tempDir); + } + +} diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/EdgeSearchTestLocal.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/EdgeSearchTestLocal.java new file mode 100644 index 00000000..c3b605a9 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/EdgeSearchTestLocal.java @@ -0,0 +1,143 @@ +package nu.marginalia.wmsa.edge.index.service; + +import com.zaxxer.hikari.HikariDataSource; +import lombok.SneakyThrows; +import nu.marginalia.util.TestUtil; +import nu.marginalia.wmsa.configuration.ServiceDescriptor; +import nu.marginalia.wmsa.configuration.module.DatabaseModule; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.data_store.DataStoreService; +import nu.marginalia.wmsa.data_store.EdgeDataStoreService; +import nu.marginalia.wmsa.data_store.FileRepository; +import nu.marginalia.wmsa.data_store.client.DataStoreClient; +import nu.marginalia.wmsa.edge.assistant.EdgeAssistantService; +import nu.marginalia.wmsa.edge.assistant.client.AssistantClient; +import nu.marginalia.wmsa.edge.assistant.dict.DictionaryService; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.assistant.dict.SpellChecker; +import nu.marginalia.wmsa.edge.assistant.eval.MathParser; +import nu.marginalia.wmsa.edge.assistant.eval.Units; +import nu.marginalia.wmsa.edge.assistant.screenshot.ScreenshotService; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDao; +import nu.marginalia.wmsa.edge.data.dao.EdgeDataStoreDaoImpl; +import nu.marginalia.wmsa.edge.data.dao.task.EdgeDomainBlacklistImpl; +import nu.marginalia.wmsa.edge.index.client.EdgeIndexClient; +import nu.marginalia.wmsa.edge.search.EdgeSearchOperator; +import nu.marginalia.wmsa.edge.search.EdgeSearchService; +import nu.marginalia.wmsa.edge.search.UnitConversion; +import nu.marginalia.wmsa.edge.search.query.EnglishDictionary; +import nu.marginalia.wmsa.edge.search.query.QueryFactory; +import nu.marginalia.wmsa.edge.search.query.QueryParser; +import nu.marginalia.wmsa.edge.search.results.SearchResultDecorator; +import nu.marginalia.wmsa.edge.search.results.SearchResultValuator; +import nu.marginalia.wmsa.renderer.mustache.RendererFactory; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Spark; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static nu.marginalia.util.TestUtil.getConnection; + +@Tag("nobuild") +@ResourceLock(value = "mariadb", mode = ResourceAccessMode.READ_WRITE) +@Execution(ExecutionMode.SAME_THREAD) +public class EdgeSearchTestLocal { + private static HikariDataSource dataSource; + private static EdgeIndexClient indexClient; + private static Path tempDir; + private static EdgeDataStoreDao edgeStoreDao; + + Logger logger = LoggerFactory.getLogger(getClass()); + @SneakyThrows + public static HikariDataSource provideConnection() { + return getConnection(); + } + + static int testPort = TestUtil.getPort(); + + static Initialization init = new Initialization(); + private QueryParser parser; + private static NGramDict dict; + private static LanguageModels lm = new LanguageModels( + Path.of("/home/vlofgren/Work/ngrams/ngrams-generous-emstr.bin"), + Path.of("/home/vlofgren/Work/ngrams/tfreq-new-algo3.bin"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-sentence-1.0-1.9.3.bin"), + Path.of("/home/vlofgren/Work/ngrams/English.RDR"), + Path.of("/home/vlofgren/Work/ngrams/English.DICT"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-tokens-1.0-1.9.3.bin") + ); + + + @SneakyThrows + @BeforeAll + public static void setUpClass() { + Spark.port(testPort); + System.setProperty("service-name", "edge-index"); + System.setProperty("unit-test", "TRUE"); + Spark.staticFileLocation("/static/edge/"); + + dict = new NGramDict(lm); + + dataSource = new DatabaseModule().provideConnection(); + dataSource.setKeepaliveTime(100); + dataSource.setIdleTimeout(100); + + indexClient = new EdgeIndexClient(); + indexClient.setServiceRoute("127.0.0.1", ServiceDescriptor.EDGE_INDEX.port); + + AssistantClient assistantClient = new AssistantClient(); + assistantClient.setServiceRoute("127.0.0.1", testPort); + + var dataStoreClient = new DataStoreClient(); + dataStoreClient.setServiceRoute("127.0.0.1", testPort); + tempDir = Files.createTempDirectory("EdgeIndexClientTest"); + + edgeStoreDao = new EdgeDataStoreDaoImpl(dataSource); + + var valuator = new SearchResultValuator(dict); + EdgeSearchService searchService = new EdgeSearchService("127.0.0.1", testPort, + edgeStoreDao, indexClient, + new RendererFactory(), new Initialization(), null, + dataStoreClient, assistantClient, new UnitConversion(assistantClient), + new EdgeSearchOperator(assistantClient, edgeStoreDao, indexClient, new QueryFactory(lm, dict, new EnglishDictionary(dict)), new SearchResultDecorator(edgeStoreDao, valuator), valuator), + new EdgeDomainBlacklistImpl(dataSource), new ScreenshotService(edgeStoreDao)); + + EdgeAssistantService assistantService = new EdgeAssistantService("127.0.0.1", testPort, Initialization.already(), null, + new DictionaryService(dataSource, new SpellChecker()), new MathParser(), + new Units(new MathParser()), null, null, + new ScreenshotService(edgeStoreDao), null); + + new DataStoreService("127.0.0.1", testPort, new FileRepository(), dataSource, new EdgeDataStoreService(new EdgeDataStoreDaoImpl(dataSource)), Initialization.already(), null); + + + Spark.awaitInitialization(); + } + + + @SneakyThrows + @Test + // @Disabled + public void run() { + init.setReady(); + + System.err.println("http://localhost:"+testPort + "/public/search?query=putty%20ssh%20download%20site:localhost"); + Thread.currentThread().join(); + } + + @AfterAll + public static void tearDownClass() { + nu.marginalia.util.test.TestUtil.clearTempDir(tempDir); + } + +} diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/MultimapFileTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/MultimapFileTest.java new file mode 100644 index 00000000..003552b2 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/MultimapFileTest.java @@ -0,0 +1,138 @@ +package nu.marginalia.wmsa.edge.index.service; + +import lombok.SneakyThrows; +import nu.marginalia.util.multimap.MultimapFileLong; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MultimapFileTest { + File tmp; + File tmp2; + + @BeforeEach @SneakyThrows + public void setUp() { + + tmp = Files.createTempFile("test", "test").toFile(); + tmp2 = Files.createTempFile("test", "test").toFile(); + + } + @AfterEach + public void tearDown() { + tmp.delete(); + tmp2.delete(); + } + + @SneakyThrows + @Test + void transfer() { + ByteBuffer buf = ByteBuffer.allocateDirect(77); + try (var source = MultimapFileLong.forOutput(tmp.toPath(), 1024); + var dest = new MultimapFileLong(tmp, FileChannel.MapMode.READ_WRITE, 1024, 8); + ) { + for (int i = 0; i < 1024; i++) { + source.put(i, i); + } + source.force(); + dest.transferFromFileChannel(new RandomAccessFile(tmp, "r").getChannel(), 11, 55, 100); + for (int i = 0; i < 45; i++) { + System.out.println("source=" + (11+i) + ", dest = " + dest.get(11+i)); + assertEquals(55+i, dest.get(11+i)); + } + } + } + + @SneakyThrows + @Test + void put() { + var file = new MultimapFileLong(new RandomAccessFile(tmp, "rw"), FileChannel.MapMode.READ_WRITE, 32, 8, false); + for (int i = 0; i < 32; i++) { + file.put(i, i); + } + for (int i = 0; i < 32; i++) { + assertEquals(i, file.get(i)); + } + } + + @SneakyThrows + @Test + void read() { + var file = new MultimapFileLong(new RandomAccessFile(tmp, "rw"), FileChannel.MapMode.READ_WRITE, 32, 8, false); + for (int i = 0; i < 32; i++) { + file.put(i, i); + } + + for (int i = 0; i < 32-6; i++) { + long[] vals = new long[6]; + file.read(vals, i); + for (int j = 0; j < 6; j++) { + assertEquals(i+j, vals[j]); + } + } + + } + + @Test + void write() throws IOException { + var file = new MultimapFileLong(new RandomAccessFile(tmp, "rw"), FileChannel.MapMode.READ_WRITE, 32, 8, false); + + for (int i = 0; i < 32-6; i++) { + file.write(new long[] { 0,1,2,3,4,5}, i); + for (int j = 0; j < 6; j++) { + assertEquals(j, file.get(i+j)); + } + } + + } + + @Test + void sortInternal() throws IOException { + var file = new MultimapFileLong(new RandomAccessFile(tmp, "rw"), FileChannel.MapMode.READ_WRITE, 32, 8, false); + var sorter = file.createSorter(Path.of("/tmp"), 16); + var searcher = file.createSearcher(); + for (int i = 0; i < 32; i++) { + file.put(i, 32-i); + } + + sorter.sort( 2, 14); + + for (int i = 2+1; i < 16; i++) { + assertTrue(file.get(i) > file.get(i-1)); + assertTrue(searcher.binarySearch(file.get(i), 2, 18)); + } + } + + @Test + void sortExternal() throws IOException { + var file = new MultimapFileLong(new RandomAccessFile(tmp, "rw"), FileChannel.MapMode.READ_WRITE, 32, 8, false); + var sorter = file.createSorter(Path.of("/tmp"), 2); + var searcher = file.createSearcher(); + + for (int i = 0; i < 32; i++) { + file.put(i, 32-i); + } + + sorter.sort( 2, 14); + file.force(); + + for (int i = 2+1; i < 16; i++) { + assertTrue(file.get(i) > file.get(i-1)); + assertTrue(searcher.binarySearch(file.get(i), 2, 18)); + } + } + + @Test + void close() { + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/SearchIndexConverterTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/SearchIndexConverterTest.java new file mode 100644 index 00000000..f69ad27c --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/SearchIndexConverterTest.java @@ -0,0 +1,88 @@ +package nu.marginalia.wmsa.edge.index.service; + +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.index.service.index.SearchIndexConverter; +import nu.marginalia.wmsa.edge.index.service.query.SearchIndexPartitioner; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +class SearchIndexConverterTest { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Test @Disabled + public void test() throws IOException { + // File dictFile = new File("/home/vlofgren/dictionary.dat"); + File inFile = new File("/home/vlofgren/Work/converter/3/page-index.dat"); + + new SearchIndexConverter(IndexBlock.Title, 0, Path.of("/tmp"), inFile, + new File("/home/vlofgren/Work/converter/words.dat"), + new File("/home/vlofgren/Work/converter/urls.dat"), new SearchIndexPartitioner(null), val -> false); + + // sanityCheck(); + } + + @Test @Disabled + public void sanityCheck() throws IOException { + File inFile = new File("/home/vlofgren/write/6/page-index.dat"); + +// SearchIndexReader sir = new SearchIndexReader(new SearchIndex[]{ +// new SearchIndex("body", Path.of("/tmp"), +// new File("/home/vlofgren/data/urls.dat"), +// new File("/home/vlofgren/data/words.dat")), +// new SearchIndex("body", Path.of("/tmp"), +// new File("/home/vlofgren/data/urls.dat"), +// new File("/home/vlofgren/data/words.dat")) +// , +// new SearchIndex("body", Path.of("/tmp"), +// new File("/home/vlofgren/data/urls.dat"), +// new File("/home/vlofgren/data/words.dat")) +// , +// new SearchIndex("body", Path.of("/tmp"), +// new File("/home/vlofgren/data/urls.dat"), +// new File("/home/vlofgren/data/words.dat")) +// }); + +// getQuery(sir, new EdgeIndexSearchTerms(List.of(152, 106), Collections.emptyList())).stream().forEach(System.out::println); +// sir.findWord(152).also(106).stream().forEach(System.out::println); +// scanFile(inFile, (url, word) -> { +// //System.out.println(url + " " + word); +// if (!sir.findWord(word).stream().anyMatch(url::equals)) { +// logger.error("Can't find word {} in {}", word, url); +// } +// }); + + + } +/* + private SearchIndexReader.Query getQuery(SearchIndexReader indexReader, EdgeIndexSearchTerms searchTerms) { + var orderedIncludes = searchTerms.includes + .stream() + .sorted(Comparator.comparingLong(indexReader::numHits)) + .distinct() + .mapToInt(Integer::intValue) + .toArray(); + + logger.info("Includes: ({}); excludes: ({})", Arrays. + stream(orderedIncludes) + .mapToObj(String::valueOf) + .collect(Collectors.joining(",")), + searchTerms.excludes.stream().map(String::valueOf).collect(Collectors.joining(","))); + SearchIndexReader.Query query = indexReader.findWord(orderedIncludes[0]); + for (int i = 1; i < orderedIncludes.length; i++) { + query = query.also(orderedIncludes[i]); + } + for (int term : searchTerms.excludes) { + query = query.not(term); + } + return query; + } + +*/ +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/SearchIndexWriterTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/SearchIndexWriterTest.java new file mode 100644 index 00000000..4a1e3e0d --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/SearchIndexWriterTest.java @@ -0,0 +1,90 @@ +package nu.marginalia.wmsa.edge.index.service; + +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.index.model.IndexBlock; +import nu.marginalia.wmsa.edge.index.service.dictionary.DictionaryWriter; +import nu.marginalia.wmsa.edge.index.service.index.SearchIndex; +import nu.marginalia.wmsa.edge.index.service.index.SearchIndexConverter; +import nu.marginalia.wmsa.edge.index.service.index.SearchIndexReader; +import nu.marginalia.wmsa.edge.index.service.index.SearchIndexWriterImpl; +import nu.marginalia.wmsa.edge.index.service.query.IndexSearchBudget; +import nu.marginalia.wmsa.edge.index.service.query.SearchIndexPartitioner; +import nu.marginalia.wmsa.edge.model.EdgeId; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.EnumMap; + +import static nu.marginalia.util.dict.DictionaryHashMap.NO_VALUE; +import static org.junit.jupiter.api.Assertions.*; + +class SearchIndexWriterTest { + DictionaryWriter dictionaryWriter; + SearchIndexWriterImpl writer; + + Path indexFile; + Path wordsFile1; + Path urlsFile1; + Path dictionaryFile; + + @BeforeEach @SneakyThrows + void setUp() { + dictionaryFile = Files.createTempFile("tmp", ".dict"); + dictionaryFile.toFile().deleteOnExit(); + + dictionaryWriter = new DictionaryWriter(dictionaryFile.toFile(), 1L<<16, false); + + indexFile = Files.createTempFile("tmp", ".idx"); + indexFile.toFile().deleteOnExit(); + writer = new SearchIndexWriterImpl(dictionaryWriter, indexFile.toFile()); + + wordsFile1 = Files.createTempFile("words1", ".idx"); + urlsFile1 = Files.createTempFile("urls1", ".idx"); + } + + @SneakyThrows + @AfterEach + void tearDown() { + dictionaryWriter.close(); + writer.close(); + indexFile.toFile().delete(); + dictionaryFile.toFile().delete(); + urlsFile1.toFile().delete(); + wordsFile1.toFile().delete(); + } + + public long[] findWord(SearchIndexReader reader, String word, IndexBlock block) { + IndexSearchBudget budget = new IndexSearchBudget(1_000_000); + return reader.findWord(block, budget, lv->true, dictionaryWriter.getReadOnly(word)).stream().toArray(); + } + + @Test + void put() throws IOException { + writer.put(new EdgeId<>(0), new EdgeId<>(1), IndexBlock.Words, Arrays.asList("Hello", "Salvete", "everyone!", "This", "is", "Bob")); + writer.put(new EdgeId<>(0), new EdgeId<>(2), IndexBlock.Words, Arrays.asList("Salvete", "omnes!", "Bob", "sum", "Hello")); + writer.forceWrite(); + + new SearchIndexConverter(IndexBlock.Words, 0, Path.of("/tmp"), indexFile.toFile(), wordsFile1.toFile(), urlsFile1.toFile(), new SearchIndexPartitioner(null), val -> false); + + EnumMap indices = new EnumMap(IndexBlock.class); + indices.put(IndexBlock.Words, new SearchIndex("0", urlsFile1.toFile(), wordsFile1.toFile())); + + var reader = new SearchIndexReader(indices); + + int bobId = dictionaryWriter.getReadOnly("Bob"); + assertNotEquals(NO_VALUE, bobId); + + assertEquals(2, reader.numHits(IndexBlock.Words, bobId)); + assertArrayEquals(new long[] { 1, 2 }, findWord(reader,"Bob", IndexBlock.Words)); + assertArrayEquals(new long[] { 2 }, findWord(reader,"sum", IndexBlock.Words)); + assertArrayEquals(new long[] { }, findWord(reader,"New Word", IndexBlock.Words)); + + writer.close(); + } + +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/TokenCompressorTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/TokenCompressorTest.java new file mode 100644 index 00000000..ee84472e --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/TokenCompressorTest.java @@ -0,0 +1,28 @@ +package nu.marginalia.wmsa.edge.index.service; + +import nu.marginalia.wmsa.edge.index.service.dictionary.TokenCompressor; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +class TokenCompressorTest { + + @Test + public void getWordBytes() { + final Map map = new HashMap<>(); + TokenCompressor tc = new TokenCompressor(word -> { + map.put(word, map.size()); + return map.size()-1; + }); + + System.out.println(Arrays.toString(tc.getWordBytes("308"))); + System.out.println(Arrays.toString(tc.getWordBytes(".308"))); + System.out.println(Arrays.toString(tc.getWordBytes("308."))); + System.out.println(Arrays.toString(tc.getWordBytes("30.8."))); + System.out.println(Arrays.toString(tc.getWordBytes("30..."))); + + map.entrySet().forEach(System.out::println); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/ByteFolderTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/ByteFolderTest.java new file mode 100644 index 00000000..2fc21ac1 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/ByteFolderTest.java @@ -0,0 +1,35 @@ +package nu.marginalia.wmsa.edge.index.service.util; + +import nu.marginalia.util.ByteFolder; +import org.junit.jupiter.api.Test; + +import static nu.marginalia.util.ByteFolder.decodeBytes; +import static org.junit.jupiter.api.Assertions.*; + +class ByteFolderTest { + + @Test + void foldBytes() { + ByteFolder folder = new ByteFolder(); + // Edge cases + assertArrayEquals(new byte[]{1,0}, folder.foldBytes(0,0)); + assertArrayEquals(new int[]{Integer.MAX_VALUE-1,Integer.MAX_VALUE}, decodeBytes(folder.foldBytes(Integer.MAX_VALUE-1,Integer.MAX_VALUE))); + assertArrayEquals(new int[]{128, 1}, decodeBytes(folder.foldBytes(128,1))); + + // 1 byte boundary + for (int i = 0; i < 512; i++) { + for (int j = 0; j < 512; j++) { + assertArrayEquals(new int[]{i,j}, decodeBytes(folder.foldBytes(i,j)), "Discrepancy @ " + i + " ," + j ); + } + } + + // Scattershot + for (int i = 0; i < 1_000_000; i++) { + int p = (int) (Integer.MAX_VALUE * Math.random()); + int q = (int) (Integer.MAX_VALUE * Math.random()); + assertArrayEquals(new int[]{q,p}, decodeBytes(folder.foldBytes(q,p)), "Discrepancy @ " + q + " ," + p ); + } + + } + +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/DictionaryDataTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/DictionaryDataTest.java new file mode 100644 index 00000000..cd063ea8 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/DictionaryDataTest.java @@ -0,0 +1,21 @@ +package nu.marginalia.wmsa.edge.index.service.util; + +import nu.marginalia.util.dict.DictionaryData; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class DictionaryDataTest { + + @Test + public void testDataBankGrow2() { + var dataBank = new DictionaryData(65535); + for (int i = 0; i < 64; i++) { + String s = "" + i; + int offset = dataBank.add(s.getBytes(), i); + System.out.println(s + " " + offset + " " + new String(dataBank.getBytes(i)) + " " + dataBank.getValue(i)); + + Assertions.assertEquals(s, new String(dataBank.getBytes(i))); + Assertions.assertEquals(i, dataBank.getValue(i)); + } + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/DictionaryHashMapTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/DictionaryHashMapTest.java new file mode 100644 index 00000000..b9a54237 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/DictionaryHashMapTest.java @@ -0,0 +1,67 @@ +package nu.marginalia.wmsa.edge.index.service.util; + +import nu.marginalia.util.dict.DictionaryHashMap; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class DictionaryHashMapTest { + + @Test + public void testDictionaryHashMap() { + var dhm = new DictionaryHashMap(1<<6); + System.out.println(dhm.put("hello".getBytes(), 23)); + System.out.println(dhm.put("hello".getBytes(), 23)); + System.out.println(dhm.put("world".getBytes(), 54)); + assertEquals(23, dhm.get("hello".getBytes())); + assertEquals(54, dhm.get("world".getBytes())); + + } + + @Test + public void testDictionaryHashMapMissing() { + var dhm = new DictionaryHashMap(1<<8); + assertEquals(DictionaryHashMap.NO_VALUE, dhm.get(new byte[] { 1,2,3})); + + } + + @Test + public void randomTest() { + Set strings = new HashSet<>(); + var dhm = new DictionaryHashMap(1<<14); + + for (int i = 0; i < 10000; i++) { + strings.add(Double.toString(Math.random())); + } + + for (String s : strings) { + dhm.put(s.getBytes(), s.hashCode()); + } + + for (String s : strings) { + assertEquals(s.hashCode(), dhm.get(s.getBytes())); + } + + assertEquals(strings.size(), dhm.size()); + } + + @Test + public void fillHerUp2() { + var dhm = new DictionaryHashMap(1<<13); + + try { + for (int i = 0; i < 10000; i++) { + dhm.put(Double.toString(Math.random()).getBytes(), i); + } + Assertions.fail("Expected exception"); + } + catch (IllegalStateException ex) { + ex.printStackTrace(); + } + } + +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/PrimeUtilTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/PrimeUtilTest.java new file mode 100644 index 00000000..703ed8cd --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/PrimeUtilTest.java @@ -0,0 +1,31 @@ +package nu.marginalia.wmsa.edge.index.service.util; + +import nu.marginalia.util.PrimeUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class PrimeUtilTest { + + @Test + void isPrime() { + assertTrue(PrimeUtil.isPrime(1)); + assertTrue(PrimeUtil.isPrime(2)); + assertTrue(PrimeUtil.isPrime(3)); + assertFalse(PrimeUtil.isPrime(4)); + assertTrue(PrimeUtil.isPrime(5)); + assertFalse(PrimeUtil.isPrime(6)); + assertTrue(PrimeUtil.isPrime(7)); + assertFalse(PrimeUtil.isPrime(8)); + assertFalse(PrimeUtil.isPrime(9)); + assertFalse(PrimeUtil.isPrime(10)); + assertTrue(PrimeUtil.isPrime(11)); + } + + @Test + void nextPrime() { + System.out.println(PrimeUtil.nextPrime(1L<<31, -1)); + System.out.println(PrimeUtil.nextPrime(1L<<31, 1)); + + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/RandomWriteFunnelTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/RandomWriteFunnelTest.java new file mode 100644 index 00000000..1780b6bb --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/index/service/util/RandomWriteFunnelTest.java @@ -0,0 +1,70 @@ +package nu.marginalia.wmsa.edge.index.service.util; + +import nu.marginalia.util.RandomWriteFunnel; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class RandomWriteFunnelTest { + + @Test + public void test() { + new File("/tmp/test.bin").delete(); + try (var funnel = new RandomWriteFunnel(Path.of("/tmp"), 10_000, 5001); + var out = new RandomAccessFile("/tmp/test.bin", "rw")) { + for (int i = 10_000-1; i >= 0; i--) { + System.out.println(i); + funnel.put(i, 10_000-i); + } + funnel.write(out.getChannel()); + + } catch (Exception e) { + e.printStackTrace(); + } + + try (var in = new RandomAccessFile("/tmp/test.bin", "r")) { + for (int i = 0; i < 10_000; i++) { + assertEquals(10_000-i, in.readLong()); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Test + public void testSparse() { + new File("/tmp/test.bin").delete(); + for (int j = 1; j <= 20; j++) { + try (var funnel = new RandomWriteFunnel(Path.of("/tmp"), 10, j); + var out = new RandomAccessFile("/tmp/test.bin", "rw")) { + for (int i = 10 - 1; i >= 0; i -= 2) { + funnel.put(i, 10 - i); + } + funnel.write(out.getChannel()); + + } catch (Exception e) { + e.printStackTrace(); + } + + try (var in = new RandomAccessFile("/tmp/test.bin", "r")) { + assertEquals(0, in.readLong()); + assertEquals(9, in.readLong()); + assertEquals(0, in.readLong()); + assertEquals(7, in.readLong()); + assertEquals(0, in.readLong()); + assertEquals(5, in.readLong()); + assertEquals(0, in.readLong()); + assertEquals(3, in.readLong()); + assertEquals(0, in.readLong()); + assertEquals(1, in.readLong()); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/integration/arxiv/ArxivParserTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/integration/arxiv/ArxivParserTest.java new file mode 100644 index 00000000..d522261b --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/integration/arxiv/ArxivParserTest.java @@ -0,0 +1,50 @@ +package nu.marginalia.wmsa.edge.integration.arxiv; + +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.DocumentKeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.model.DocumentLanguageData; +import nu.marginalia.wmsa.edge.integration.arxiv.model.ArxivMetadata; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +@Disabled // this isn't used and the test is hella slow +class ArxivParserTest { + LanguageModels lm = new LanguageModels( + Path.of("/home/vlofgren/Work/ngrams/ngrams-generous-emstr.bin"), + Path.of("/home/vlofgren/Work/ngrams/tfreq-new-algo.bin"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-sentence-1.0-1.9.3.bin"), + Path.of("/home/vlofgren/Work/ngrams/English.RDR"), + Path.of("/home/vlofgren/Work/ngrams/English.DICT"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-tokens-1.0-1.9.3.bin") + ); + + @Test + void parse() throws IOException { + var parser = new ArxivParser(); + var data = parser.parse(new File("/home/vlofgren/Work/arxiv/arxiv-metadata-oai-snapshot.json")); + + data.stream().map(ArxivMetadata::getAbstract).limit(100).forEach(System.out::println); + } + + @Test + void extractKeywords() throws IOException { + var dict = new NGramDict(lm); + + DocumentKeywordExtractor documentKeywordExtractor = new DocumentKeywordExtractor(dict); + + var parser = new ArxivParser(); + var data = parser.parse(new File("/home/vlofgren/Work/arxiv/arxiv-metadata-oai-snapshot.json")); + + var se = new SentenceExtractor(lm); + + data.stream().map(meta -> documentKeywordExtractor.extractKeywords(se.extractSentences(meta.getAbstract(), meta.getTitle()))).limit(100).forEach(System.out::println); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/integration/stackoverflow/StackOverflowPostsTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/integration/stackoverflow/StackOverflowPostsTest.java new file mode 100644 index 00000000..05f66976 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/integration/stackoverflow/StackOverflowPostsTest.java @@ -0,0 +1,54 @@ +package nu.marginalia.wmsa.edge.integration.stackoverflow; + +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.DocumentKeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.util.ParallelPipe; +import nu.marginalia.wmsa.edge.integration.model.BasicDocumentData; +import nu.marginalia.wmsa.edge.integration.stackoverflow.model.StackOverflowPost; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import org.junit.jupiter.api.Test; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.nio.file.Path; + +public class StackOverflowPostsTest { + LanguageModels lm = new LanguageModels( + Path.of("/home/vlofgren/Work/ngrams/ngrams-generous-emstr.bin"), + Path.of("/home/vlofgren/Work/ngrams/tfreq-new-algo4.bin"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-sentence-1.0-1.9.3.bin"), + Path.of("/home/vlofgren/Work/ngrams/English.RDR"), + Path.of("/home/vlofgren/Work/ngrams/English.DICT"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-tokens-1.0-1.9.3.bin") + ); + + @Test + public void test() throws IOException, ParserConfigurationException, SAXException, InterruptedException { + var documentKeywordExtractor = new DocumentKeywordExtractor(new NGramDict(lm)); + + ThreadLocal processor = ThreadLocal.withInitial(() -> { + return new StackOverflowPostProcessor(new SentenceExtractor(lm), documentKeywordExtractor); + }); + + var pipe = new ParallelPipe("pipe", 10, 5, 2) { + @Override + public BasicDocumentData onProcess(StackOverflowPost stackOverflowPost) { + return processor.get().process(stackOverflowPost); + } + + @Override + public void onReceive(BasicDocumentData stackOverflowIndexData) { + System.out.println(stackOverflowIndexData.url); + } + }; + + var reader = new StackOverflowPostsReader("/mnt/storage/downloads.new/stackexchange/sites/philosophy/Posts.xml", new EdgeDomain("philosophy.stackexchange.com"), + pipe::accept); + reader.join(); + System.out.println("Waiting for pipe"); + pipe.join(); + } +} diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/integration/wikipedia/WikipediaTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/integration/wikipedia/WikipediaTest.java new file mode 100644 index 00000000..41c6b362 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/integration/wikipedia/WikipediaTest.java @@ -0,0 +1,78 @@ +package nu.marginalia.wmsa.edge.integration.wikipedia; + +import lombok.SneakyThrows; +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.DocumentDebugger; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.DocumentKeywordExtractor; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import nu.marginalia.util.ParallelPipe; +import nu.marginalia.wmsa.edge.integration.model.BasicDocumentData; +import nu.marginalia.wmsa.edge.integration.wikipedia.model.WikipediaArticle; +import nu.marginalia.wmsa.edge.model.EdgeDomain; +import org.jsoup.Jsoup; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Path; + +public class WikipediaTest { + LanguageModels lm = new LanguageModels( + Path.of("/home/vlofgren/Work/ngrams/ngrams-generous-emstr.bin"), + Path.of("/home/vlofgren/Work/ngrams/tfreq-new-algo4.bin"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-sentence-1.0-1.9.3.bin"), + Path.of("/home/vlofgren/Work/ngrams/English.RDR"), + Path.of("/home/vlofgren/Work/ngrams/English.DICT"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-tokens-1.0-1.9.3.bin") + ); + + @Test @SneakyThrows + public void test() { + var documentKeywordExtractor = new DocumentKeywordExtractor(new NGramDict(lm)); + ThreadLocal processor = ThreadLocal.withInitial(() -> { + return new WikipediaProcessor(new SentenceExtractor(lm), documentKeywordExtractor); + }); + + var pipe = new ParallelPipe("pipe", 10, 5, 2) { + @Override + public BasicDocumentData onProcess(WikipediaArticle stackOverflowPost) { + return processor.get().process(stackOverflowPost); + } + + @Override + public void onReceive(BasicDocumentData indexData) { + System.out.println(indexData.url); + System.out.println(indexData.title); + System.out.println(indexData.description); + } + }; + + var reader = new WikipediaReader("/home/vlofgren/Work/wikipedia_en_100_nopic_2021-06.zim", new EdgeDomain("encyclopedia.marginalia.nu"), + pipe::accept); + reader.join(); + } + + + @Test @SneakyThrows + public void test2() { + var documentKeywordExtractor = new DocumentKeywordExtractor(new NGramDict(lm)); + var debugger = new DocumentDebugger(lm); + + ThreadLocal processor = ThreadLocal.withInitial(() -> { + return new WikipediaProcessor(new SentenceExtractor(lm), documentKeywordExtractor); + }); + + var reader = new WikipediaReader("/home/vlofgren/Work/wikipedia_en_100_nopic_2021-06.zim", new EdgeDomain("encyclopedia.marginalia.nu"), + article -> { + try { + debugger.debugDocument(article.url.getPath(), Jsoup.parse(article.body)); + + } catch (IOException e) { + e.printStackTrace(); + } + }); + + reader.join(); + debugger.writeIndex(); + } +} diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/model/EdgeDomainTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/model/EdgeDomainTest.java new file mode 100644 index 00000000..9493e638 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/model/EdgeDomainTest.java @@ -0,0 +1,106 @@ +package nu.marginalia.wmsa.edge.model; + +import org.junit.jupiter.api.Test; + +import java.net.URISyntaxException; + +import static org.junit.jupiter.api.Assertions.*; + +class EdgeDomainTest { + + @Test + public void testSkepdic() throws URISyntaxException { + var domain = new EdgeUrl("http://www.skepdic.com/astrology.html"); + assertEquals("skepdic", domain.getDomain().getDomainKey()); + var domain2 = new EdgeUrl("http://skepdic.com/astrology.html"); + assertEquals("skepdic", domain2.getDomain().getDomainKey()); + } + + @Test + public void testHkDomain() throws URISyntaxException { + var domain = new EdgeUrl("http://l7072i3.l7c.net"); + assertEquals("http", domain.proto); + assertEquals("l7072i3", domain.domain.subDomain); + assertEquals("l7c.net", domain.domain.domain); + } + + @Test + public void testEduSubDomain() throws URISyntaxException { + var domain = new EdgeUrl("http://uj.edu.pl"); + assertEquals("http", domain.proto); + assertEquals("", domain.domain.subDomain); + assertEquals("uj.edu.pl", domain.domain.domain); + } + + + @Test + public void testGetDomain() throws URISyntaxException { + var domain = new EdgeUrl("http://www.marginalia.nu"); + assertEquals("http", domain.proto); + assertEquals("www", domain.domain.subDomain); + assertEquals("marginalia.nu", domain.domain.domain); + assertEquals("http://www.marginalia.nu/", domain.toString()); + } + + @Test + public void testUkDomain2() throws URISyntaxException { + var domain = new EdgeUrl("http://marginalia.co.uk"); + assertEquals("marginalia.co.uk", domain.domain.domain); + assertEquals("http", domain.proto); + assertEquals("", domain.domain.subDomain); + assertEquals("http://marginalia.co.uk/", domain.toString()); + } + + @Test + public void testUkDomain3() throws URISyntaxException { + var domain = new EdgeUrl("http://withcandour.co.uk"); + assertEquals("withcandour.co.uk", domain.domain.domain); + assertEquals("http", domain.proto); + assertEquals("", domain.domain.subDomain); + assertEquals("http://withcandour.co.uk/", domain.toString()); + } + + @Test + public void testUkDomain() throws URISyntaxException { + var domain = new EdgeUrl("http://www.marginalia.co.uk"); + assertEquals("http", domain.proto); + assertEquals("www", domain.domain.subDomain); + assertEquals("marginalia.co.uk", domain.domain.domain); + assertEquals("http://www.marginalia.co.uk/", domain.toString()); + } + + @Test + public void testThreeLetterDomain() throws URISyntaxException { + var domain = new EdgeUrl("http://www.marginalia.abcf.de"); + assertEquals("http", domain.proto); + assertEquals("abcf.de", domain.domain.domain); + assertEquals("www.marginalia", domain.domain.subDomain); + } + + @Test + public void testGetDomainNoSubdomain() throws URISyntaxException { + var domain = new EdgeUrl("http://marginalia.nu"); + assertEquals("http", domain.proto); + assertEquals("", domain.domain.subDomain); + assertEquals("marginalia.nu", domain.domain.domain); + assertEquals("http://marginalia.nu/", domain.toString()); + } + + @Test + public void testIpPort() throws URISyntaxException { + var domain = new EdgeUrl("https://127.0.0.1:8080"); + assertEquals("https", domain.proto); + assertEquals("", domain.domain.subDomain); + assertEquals("127.0.0.1", domain.domain.domain); + assertEquals("https://127.0.0.1:8080/", domain.toString()); + } + + @Test + public void testIp() throws URISyntaxException { + var domain = new EdgeUrl("https://192.168.1.32"); + assertEquals("https", domain.proto); + assertEquals("", domain.domain.subDomain); + assertEquals("192.168.1.32", domain.domain.domain); + assertEquals("https://192.168.1.32/", domain.toString()); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/model/EdgeUrlTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/model/EdgeUrlTest.java new file mode 100644 index 00000000..a6e690fe --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/model/EdgeUrlTest.java @@ -0,0 +1,22 @@ +package nu.marginalia.wmsa.edge.model; + +import org.junit.jupiter.api.Test; + +import java.net.URISyntaxException; + +import static org.junit.jupiter.api.Assertions.*; + +class EdgeUrlTest { + + @Test + public void testHashCode() throws URISyntaxException { + System.out.println(new EdgeUrl("https://memex.marginalia.nu").hashCode()); + } + + @Test + void urlencodeFixer() throws URISyntaxException { + System.out.println(EdgeUrl.urlencodeFixer("https://www.example.com/%-sign")); + System.out.println(EdgeUrl.urlencodeFixer("https://www.example.com/%22-sign")); + System.out.println(EdgeUrl.urlencodeFixer("https://www.example.com/\n \"huh\"")); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/search/query/BodyQueryParserTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/search/query/BodyQueryParserTest.java new file mode 100644 index 00000000..8f159817 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/search/query/BodyQueryParserTest.java @@ -0,0 +1,106 @@ +package nu.marginalia.wmsa.edge.search.query; + +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import org.junit.BeforeClass; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class BodyQueryParserTest { + private QueryParser parser; + private static NGramDict dict; + private static EnglishDictionary englishDictionary; + private static LanguageModels lm = new LanguageModels( + Path.of("/home/vlofgren/Work/ngrams/ngrams-generous-emstr.bin"), + Path.of("/home/vlofgren/Work/ngrams/tfreq-new-algo4.bin"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-sentence-1.0-1.9.3.bin"), + Path.of("/home/vlofgren/Work/ngrams/English.RDR"), + Path.of("/home/vlofgren/Work/ngrams/English.DICT"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-tokens-1.0-1.9.3.bin") + ); + + @BeforeClass + public static void init() { + dict = new NGramDict(lm); + englishDictionary = new EnglishDictionary(dict); + } + + @BeforeEach + public void setUp() { + parser = new QueryParser(englishDictionary, new QueryVariants(lm, dict, englishDictionary)); + } + + @Test + public void testTitleMatcher() { + List terms = List.of("3d", "realms"); + assertEquals(2, terms.stream().map(String::toLowerCase).filter("3D Realms Site: Forums".toLowerCase()::contains).count()); + } + @Test + void parseSimple() { + var results = parser.parse("hello"); + results.forEach(System.out::println); + assertEquals(1, results.size()); + assertEquals(TokenType.LITERAL_TERM, results.get(0).type); + assertEquals("hello", results.get(0).str); + } + + @Test + void parseQuotes() { + var results = parser.parse("\u201Chello world\u201D"); + results.forEach(System.out::println); + assertEquals(TokenType.QUOT_TERM, results.get(0).type); + assertEquals("hello_world", results.get(0).str); + assertEquals("\u201Chello world\u201D", results.get(0).displayStr); + } + + @Test + void parseExclude() { + var results = parser.parse("-Hello"); + results.forEach(System.out::println); + assertEquals(TokenType.EXCLUDE_TERM, results.get(0).type); + assertEquals("hello", results.get(0).str); + assertEquals("-hello", results.get(0).displayStr); + } + + @Test + void parseCombined() { + for (var list : parser.permuteQueries(parser.parse("dune 2 remake"))) { + for (var t: list) { + System.out.printf("%s ", t.str); + } + System.out.println(); + } + } + @Test + void parseCombinedDOS() { + for (var list : parser.permuteQueries(parser.parse("ab ba baa abba baba ab ba"))) { + for (var t: list) { + System.out.printf("%s ", t.str); + } + System.out.println(); + } + } + + @Test + void parseCombinedSuperman() { + for (var list : parser.permuteQueries(parser.parse("wizardry proving grounds of the mad overlord"))) { + for (var t: list) { + System.out.printf("%s ", t.str); + } + System.out.println(); + } + } + @Test + void testEdgeCases() { + parser.parse("site:localhost 3D").forEach(System.out::println); + parser.parse("-wolfenstein 3D").forEach(System.out::println); + parser.parse("-wolfenstein 3D \"").forEach(System.out::println); + } + + +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/search/query/EnglishDictionaryTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/search/query/EnglishDictionaryTest.java new file mode 100644 index 00000000..4c0514ea --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/search/query/EnglishDictionaryTest.java @@ -0,0 +1,27 @@ +package nu.marginalia.wmsa.edge.search.query; + +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +class EnglishDictionaryTest { + + @Test + void getWordVariants() { + LanguageModels lm = new LanguageModels( + Path.of("/home/vlofgren/Work/ngrams/ngrams-generous-emstr.bin"), + Path.of("/home/vlofgren/Work/ngrams/tfreq-new-algo4.bin"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-sentence-1.0-1.9.3.bin"), + Path.of("/home/vlofgren/Work/ngrams/English.RDR"), + Path.of("/home/vlofgren/Work/ngrams/English.DICT"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-tokens-1.0-1.9.3.bin") + ); + + var dict = new NGramDict(lm); + new EnglishDictionary(dict).getWordVariants("dos").forEach(System.out::println); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/search/query/QueryParserTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/search/query/QueryParserTest.java new file mode 100644 index 00000000..0e9aec69 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/search/query/QueryParserTest.java @@ -0,0 +1,40 @@ +package nu.marginalia.wmsa.edge.search.query; + +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import org.junit.BeforeClass; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.util.stream.Collectors; + +class QueryParserTest { + private QueryParser parser; + private static NGramDict dict; + private static EnglishDictionary englishDictionary; + private static LanguageModels lm = new LanguageModels( + Path.of("/home/vlofgren/Work/ngrams/ngrams-generous-emstr.bin"), + Path.of("/home/vlofgren/Work/ngrams/tfreq-new-algo4.bin"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-sentence-1.0-1.9.3.bin"), + Path.of("/home/vlofgren/Work/ngrams/English.RDR"), + Path.of("/home/vlofgren/Work/ngrams/English.DICT"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-tokens-1.0-1.9.3.bin") + ); + + @BeforeEach + public void setUp() { + dict = new NGramDict(lm); + englishDictionary = new EnglishDictionary(dict); + + parser = new QueryParser(englishDictionary, new QueryVariants(lm, dict, englishDictionary)); + } + + @Test + void variantQueries() { + var r = parser.parse("car stemming"); + parser.variantQueries(r).forEach(query -> { + System.out.println(query.stream().map(t -> t.str).collect(Collectors.joining(", "))); + }); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/search/query/QueryVariantsTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/search/query/QueryVariantsTest.java new file mode 100644 index 00000000..91ec77af --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/edge/search/query/QueryVariantsTest.java @@ -0,0 +1,63 @@ +package nu.marginalia.wmsa.edge.search.query; + +import nu.marginalia.wmsa.edge.assistant.dict.NGramDict; +import nu.marginalia.wmsa.edge.crawler.domain.language.conf.LanguageModels; +import nu.marginalia.wmsa.edge.crawler.domain.language.processing.SentenceExtractor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.util.List; + +class QueryVariantsTest { + QueryVariants variants; + QueryParser parser; + SentenceExtractor se; + @BeforeEach + public void setUp() { + LanguageModels lm = new LanguageModels( + Path.of("/home/vlofgren/Work/ngrams/ngrams-generous-emstr.bin"), + Path.of("/home/vlofgren/Work/ngrams/tfreq-new-algo4.bin"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-sentence-1.0-1.9.3.bin"), + Path.of("/home/vlofgren/Work/ngrams/English.RDR"), + Path.of("/home/vlofgren/Work/ngrams/English.DICT"), + Path.of("/home/vlofgren/Work/ngrams/opennlp-en-ud-ewt-tokens-1.0-1.9.3.bin") + ); + + se = new SentenceExtractor(lm); + + var dict = new NGramDict(lm); + variants = new QueryVariants(lm, dict, new EnglishDictionary(dict)); + parser = new QueryParser(new EnglishDictionary(dict), variants); + } + + @Test + void getQueryVariants() { + System.out.println(se.extractSentence("we are alone")); + testCase("DOS", List.of("DOS")); + testCase("dos", List.of("dos")); + testCase("we are alone", List.of("dos")); + testCase("3D Realms", List.of("dos")); + testCase("I am alone", List.of("dos")); + testCase("plato cave", List.of("dos")); + testCase("The internet is dead", List.of("dos")); + + testCase("TRS80", List.of("trs_80"), List.of("trs80")); + testCase("TRS-80", List.of("trs-80"), List.of("trs80")); + testCase("TRS-80", List.of("trs-80"), List.of("trs80")); + testCase("Raspberry Pi 2", List.of("trs-80"), List.of("trs80")); + testCase("Duke Nukem 3D", List.of("trs-80"), List.of("trs80")); + testCase("The Man of Tomorrow", List.of("trs-80"), List.of("trs80")); + testCase("Computer Manual", List.of("trs-80"), List.of("trs80")); + testCase("Knitting", List.of("trs-80"), List.of("trs80")); + testCase("capcom", List.of("trs-80"), List.of("trs80")); + testCase("the man of tomorrow", List.of("trs-80"), List.of("trs80")); + } + + private void testCase(String input, List... expected) { + var tokens = variants.getQueryVariants(parser.extractBasicTokens(input)); + System.out.println(tokens); +// var result = tokens.stream().map(lst -> lst.terms).collect(Collectors.toSet()); +// assertEquals(Set.of(expected), result, "Case failed: " + input); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/MemexFileWriterTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/MemexFileWriterTest.java new file mode 100644 index 00000000..fb240c25 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/MemexFileWriterTest.java @@ -0,0 +1,43 @@ +package nu.marginalia.wmsa.memex; + +import nu.marginalia.util.test.TestUtil; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import nu.marginalia.wmsa.memex.system.MemexFileWriter; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +class MemexFileWriterTest { + + Path root; + MemexFileWriter renderedResources; + @BeforeEach + void setUp() throws IOException { + root = Files.createTempDirectory(getClass().getSimpleName()); + renderedResources = new MemexFileWriter(root); + } + + @AfterEach + void tearDown() { + TestUtil.clearTempDir(root); + } + + @Test + void exists() throws IOException { + assertFalse(renderedResources.exists(new MemexNodeUrl("/test"))); + renderedResources.write(new MemexNodeUrl("/test"), "A line"); + assertTrue(renderedResources.exists(new MemexNodeUrl("/test"))); + } + + @Test + void write() throws IOException { + renderedResources.write(new MemexNodeUrl("/test"), "A line"); + assertEquals("A line", Files.readString(root.resolve("test"))); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/MemexTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/MemexTest.java new file mode 100644 index 00000000..10b833e9 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/MemexTest.java @@ -0,0 +1,12 @@ +package nu.marginalia.wmsa.memex; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Path; + +class MemexTest { + @Test + public void test() throws IOException { + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/change/GemtextChangeTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/change/GemtextChangeTest.java new file mode 100644 index 00000000..7afa424a --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/change/GemtextChangeTest.java @@ -0,0 +1,274 @@ +package nu.marginalia.wmsa.memex.change; + +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import lombok.SneakyThrows; +import nu.marginalia.gemini.GeminiService; +import nu.marginalia.util.test.TestUtil; +import nu.marginalia.wmsa.memex.*; +import nu.marginalia.wmsa.memex.renderer.MemexHtmlRenderer; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import nu.marginalia.wmsa.memex.renderer.MemexRendererers; +import nu.marginalia.wmsa.memex.system.MemexFileSystemModifiedTimes; +import nu.marginalia.wmsa.memex.system.MemexFileWriter; +import nu.marginalia.wmsa.memex.system.MemexGitRepo; +import nu.marginalia.wmsa.memex.system.MemexSourceFileSystem; +import org.junit.BeforeClass; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +class GemtextChangeTest { + + + private Memex memex; + private Path tempDir; + + private final String tombstonePath = "/special/tombstone.gmi"; + private final String redirectPath = "/special/redirects.gmi"; + private final String testFilePath = "/test.gmi"; + + static Logger logger = LoggerFactory.getLogger(GemtextChangeTest.class); + + @BeforeClass + public static void init() { + + RxJavaPlugins.setErrorHandler(e -> { + if (e.getMessage() == null) { + logger.error("Error", e); + } + else { + logger.error("Error {}: {}", e.getClass().getSimpleName(), e.getMessage()); + } + }); + } + + @SneakyThrows + @BeforeEach + public void setUp() { + tempDir = Files.createTempDirectory("test"); + Files.createDirectory(tempDir.resolve("special")); + var data = new MemexData(); + + memex = new Memex(data, null, + Mockito.mock(MemexGitRepo.class), new MemexLoader(data, new MemexFileSystemModifiedTimes(), + new MemexSourceFileSystem(tempDir, Mockito.mock(MemexGitRepo.class)), + tempDir, tombstonePath, redirectPath), + Mockito.mock(MemexFileWriter.class), + null, + Mockito.mock(MemexRendererers.class), + Mockito.mock(GeminiService.class)); + } + + @SneakyThrows + @AfterEach + public void tearDown() { + TestUtil.clearTempDir(tempDir); + } + + @Test + void appendHeading() throws IOException { + var url = new MemexNodeUrl(testFilePath); + new GemtextCreateOrMutate(url, "# Header", + new GemtextAppend(url, new MemexNodeHeadingId(1), new String[] { + "1", "## Header 2", "# Header 3" + }) + ).visit(memex); + new GemtextAppend(url, new MemexNodeHeadingId(1), new String[] { "3"}).visit(memex); + + + List lines = Files.readAllLines(Path.of(tempDir + testFilePath)); + lines.forEach(System.out::println); + assertEquals(5, lines.size()); + assertEquals("# Header", lines.get(0)); + assertEquals("1", lines.get(1)); + assertEquals("## Header 2", lines.get(2)); + assertEquals("3", lines.get(3)); + assertEquals("# Header 3", lines.get(4)); + } + + @Test + void appendRoot() throws IOException { + var url = new MemexNodeUrl(testFilePath); + new GemtextCreateOrMutate(url, "# Header", + new GemtextAppend(url, new MemexNodeHeadingId(1), new String[] { + "1", "## Header 2", "# Header 3" + }) + ).visit(memex); + new GemtextAppend(url, new MemexNodeHeadingId(0), new String[] { "3"}) + .visit(memex); + + + List lines = Files.readAllLines(Path.of(tempDir + testFilePath)); + lines.forEach(System.out::println); + assertEquals(5, lines.size()); + assertEquals("# Header", lines.get(0)); + assertEquals("1", lines.get(1)); + assertEquals("## Header 2", lines.get(2)); + assertEquals("# Header 3", lines.get(3)); + assertEquals("3", lines.get(4)); + } + + @Test + void appendMissing() throws IOException { + var url = new MemexNodeUrl(testFilePath); + new GemtextCreateOrMutate(url, "# Header", + new GemtextAppend(url, new MemexNodeHeadingId(1), new String[] { + "1", "## Header 2", "# Header 3" + }) + ).visit(memex); + new GemtextAppend(url, new MemexNodeHeadingId(5), new String[] { "3"}) + .visit(memex); + + + List lines = Files.readAllLines(Path.of(tempDir + testFilePath)); + lines.forEach(System.out::println); + assertEquals(5, lines.size()); + assertEquals("# Header", lines.get(0)); + assertEquals("1", lines.get(1)); + assertEquals("## Header 2", lines.get(2)); + assertEquals("# Header 3", lines.get(3)); + assertEquals("3", lines.get(4)); + } + + @Test + void replaceHeading() throws IOException { + var url = new MemexNodeUrl(testFilePath); + new GemtextCreateOrMutate(url, "# Header", + new GemtextAppend(url, new MemexNodeHeadingId(1), new String[] { + "1", "## Header 2", "# Header 3" + }) + ).visit(memex); + new GemtextReplace(url, new MemexNodeHeadingId(1), new String[] { "# New", "3"}) + .visit(memex); + + + List lines = Files.readAllLines(Path.of(tempDir + testFilePath)); + lines.forEach(System.out::println); + assertEquals(3, lines.size()); + assertEquals("# New", lines.get(0)); + assertEquals("3", lines.get(1)); + assertEquals("# Header 3", lines.get(2)); + } + + @Test + void replaceRoot() throws IOException { + var url = new MemexNodeUrl(testFilePath); + new GemtextCreateOrMutate(url, "# Header", + new GemtextAppend(url, new MemexNodeHeadingId(1), new String[] { + "1", "## Header 2", "# Header 3" + }) + ).visit(memex); + new GemtextReplace(url, new MemexNodeHeadingId(0), new String[] { "# New", "3"}) + .visit(memex); + + + List lines = Files.readAllLines(Path.of(tempDir + testFilePath)); + lines.forEach(System.out::println); + assertEquals(2, lines.size()); + assertEquals("# New", lines.get(0)); + assertEquals("3", lines.get(1)); + } + + @Test + void replaceMissing() throws IOException { + var url = new MemexNodeUrl(testFilePath); + new GemtextCreateOrMutate(url, "# Header", + new GemtextAppend(url, new MemexNodeHeadingId(1), new String[] { + "1", "## Header 2", "# Header 3" + }) + ).visit(memex); + new GemtextReplace(url, new MemexNodeHeadingId(5), new String[] { "# New", "3"}) + .visit(memex); + + + List lines = Files.readAllLines(Path.of(tempDir + testFilePath)); + lines.forEach(System.out::println); + assertEquals(7, lines.size()); + + + assertEquals("# Header", lines.get(0)); + assertEquals("1", lines.get(1)); + assertEquals("## Header 2", lines.get(2)); + assertEquals("# Header 3", lines.get(3)); + + assertEquals("# Error! Replace failed!", lines.get(4)); + assertEquals("# New", lines.get(5)); + assertEquals("3", lines.get(6)); + } + + @Test + void prependHeading() throws IOException { + var url = new MemexNodeUrl(testFilePath); + new GemtextCreateOrMutate(url, "# Header", + new GemtextAppend(url, new MemexNodeHeadingId(1), new String[] { + "1", "2" + }) + ).visit(memex); + new GemtextPrepend(url, new MemexNodeHeadingId(1), new String[] { "3"}) + .visit(memex); + + + List lines = Files.readAllLines(Path.of(tempDir + testFilePath)); + lines.forEach(System.out::println); + assertEquals(4, lines.size()); + assertEquals("# Header", lines.get(0)); + assertEquals("3", lines.get(1)); + assertEquals("1", lines.get(2)); + assertEquals("2", lines.get(3)); + } + + @Test + void prependRoot() throws IOException { + var url = new MemexNodeUrl(testFilePath); + new GemtextCreateOrMutate(url, "# Header", + new GemtextAppend(url, new MemexNodeHeadingId(1), new String[] { + "1", "2" + }) + ).visit(memex); + new GemtextPrepend(url, new MemexNodeHeadingId(0), new String[] { "3" }) + .visit(memex); + + List lines = Files.readAllLines(Path.of(tempDir + testFilePath)); + + lines.forEach(System.out::println); + assertEquals(4, lines.size()); + assertEquals("3", lines.get(0)); + assertEquals("# Header", lines.get(1)); + assertEquals("1", lines.get(2)); + assertEquals("2", lines.get(3)); + } + + + @Test + void prependMissing() throws IOException { + var url = new MemexNodeUrl(testFilePath); + new GemtextCreateOrMutate(url, "# Header", + new GemtextAppend(url, new MemexNodeHeadingId(1), new String[] { + "1", "2" + }) + ).visit(memex); + new GemtextPrepend(url, new MemexNodeHeadingId(5), new String[] { "3" }) + .visit(memex); + + List lines = Files.readAllLines(Path.of(tempDir + testFilePath)); + + lines.forEach(System.out::println); + assertEquals(4, lines.size()); + assertEquals("# Header", lines.get(0)); + assertEquals("1", lines.get(1)); + assertEquals("2", lines.get(2)); + assertEquals("3", lines.get(3)); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/change/GemtextTaskUpdateTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/change/GemtextTaskUpdateTest.java new file mode 100644 index 00000000..bdfe83dc --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/change/GemtextTaskUpdateTest.java @@ -0,0 +1,226 @@ +package nu.marginalia.wmsa.memex.change; + +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import lombok.SneakyThrows; +import nu.marginalia.gemini.GeminiService; +import nu.marginalia.gemini.gmi.GemtextDocument; +import nu.marginalia.util.test.TestUtil; +import nu.marginalia.wmsa.memex.*; +import nu.marginalia.wmsa.memex.change.update.GemtextDocumentUpdateCalculator; +import nu.marginalia.wmsa.memex.renderer.MemexHtmlRenderer; +import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import nu.marginalia.wmsa.memex.renderer.MemexRendererers; +import nu.marginalia.wmsa.memex.system.MemexFileSystemModifiedTimes; +import nu.marginalia.wmsa.memex.system.MemexFileWriter; +import nu.marginalia.wmsa.memex.system.MemexGitRepo; +import nu.marginalia.wmsa.memex.system.MemexSourceFileSystem; +import org.junit.BeforeClass; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GemtextTaskUpdateTest { + + + private Memex memex; + private Path tempDir; + + private final String tombstonePath = "/special/tombstone.gmi"; + private final String redirectPath = "/special/redirects.gmi"; + private final String testFilePath = "/test.gmi"; + private final String todoFilePath = "/todo.gmi"; + private final String doneFilePath = "/done.gmi"; + + static Logger logger = LoggerFactory.getLogger(GemtextTaskUpdateTest.class); + + @BeforeClass + public static void init() { + + RxJavaPlugins.setErrorHandler(e -> { + if (e.getMessage() == null) { + logger.error("Error", e); + } + else { + logger.error("Error {}: {}", e.getClass().getSimpleName(), e.getMessage()); + } + }); + } + + @SneakyThrows + @BeforeEach + public void setUp() { + tempDir = Files.createTempDirectory("test"); + Files.createDirectory(tempDir.resolve("special")); + var data = new MemexData(); + + memex = new Memex(data, null, Mockito.mock(MemexGitRepo.class), new MemexLoader(data, new MemexFileSystemModifiedTimes(), + new MemexSourceFileSystem(tempDir, Mockito.mock(MemexGitRepo.class)), tempDir, tombstonePath, redirectPath), + Mockito.mock(MemexFileWriter.class), + null, + Mockito.mock(MemexRendererers.class), + Mockito.mock(GeminiService.class)); + } + + @SneakyThrows + @AfterEach + public void tearDown() { + TestUtil.clearTempDir(tempDir); + } + + @Test + void updateTodoFileWithTodoTask() throws IOException { + + var url = new MemexNodeUrl(testFilePath); + + GemtextMutation.createOrAppend(url, "%%%TASKS\n# Header", new MemexNodeHeadingId(1), + "%%% TASKS", "## Todo", "- A task yet finished").visit(memex); + + GemtextDocumentUpdateCalculator updateCalculator = new GemtextDocumentUpdateCalculator(memex); + var updates = updateCalculator.calculateUpdates(memex.getDocument(url), MemexNodeHeadingId.ROOT, + GemtextDocument.of(url, "%%% TASKS", "# Header", "## Todo", "- A task yet finished (?)", "- One More Task")); + updates.forEach(System.out::println); + for (var update : updates) { + update.visit(memex); + } + + verifyFile(testFilePath, + "%%% TASKS", + "# Header", + "## Todo", + "- A task yet finished (?)", + "- One More Task" + ); + } + + @Test + void updateDoneFileWithTodoTask() throws IOException { + + var url = new MemexNodeUrl(testFilePath); + + GemtextMutation.createOrAppend(url, "%%% TASKS\n# Header", new MemexNodeHeadingId(1), + "## Done", "- A task yet finished (/)").visit(memex); + + GemtextDocumentUpdateCalculator updateCalculator = new GemtextDocumentUpdateCalculator(memex); + var updates = updateCalculator.calculateUpdates(memex.getDocument(url), MemexNodeHeadingId.ROOT, + GemtextDocument.of(url, "%%% TASKS", "# Header", "## Done", "- A task yet finished (?)")); + updates.forEach(System.out::println); + for (var update : updates) { + update.visit(memex); + } + + verifyFile(testFilePath, + "%%% TASKS", + "# Header", + "## Done" + ); + + verifyFile(todoFilePath, + "%%% TASKS", + "# Todo", + "- A task yet finished (?)" + ); + } + + @Test + void moveToDoneNewDoneFile() throws IOException { + + var url = new MemexNodeUrl(testFilePath); + + GemtextMutation.createOrAppend(url, "%%% TASKS\n# Header", new MemexNodeHeadingId(1), + "%%% TASKS","## Todo", "- A task yet finished").visit(memex); + + GemtextDocumentUpdateCalculator updateCalculator = new GemtextDocumentUpdateCalculator(memex); + var updates = updateCalculator.calculateUpdates(memex.getDocument(url), MemexNodeHeadingId.ROOT, + GemtextDocument.of(url, "%%% TASKS", "# Header", "## Todo", "- A task yet finished (/)")); + updates.forEach(System.out::println); + for (var update : updates) { + update.visit(memex); + } + + verifyFile(doneFilePath, + "%%% TASKS", + "# Done", + "", + "## Done " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE), + "- A task yet finished (/)"); + + updates = updateCalculator.calculateUpdates(memex.getDocument(url), MemexNodeHeadingId.ROOT, + GemtextDocument.of(url, "%%% TASKS", "# Header", "## Todo", "- Another task yet finished (/)")); + updates.forEach(System.out::println); + for (var update : updates) { + update.visit(memex); + } + + verifyFile(doneFilePath, + "%%% TASKS", + "# Done", + "", + "## Done " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE), + "- Another task yet finished (/)", + "- A task yet finished (/)"); + } + + @Test + void moveToDoneOldDoneFile() throws IOException { + + var doneUrl = new MemexNodeUrl(doneFilePath); + var url = new MemexNodeUrl(testFilePath); + + + GemtextMutation.createOrAppend(doneUrl, "%%% TASKS\n# Done", new MemexNodeHeadingId(1), + "## Done 2012-04-30", "- A very old task (/)").visit(memex); + + GemtextMutation.createOrAppend(url, "%%% TASKS\n# Header", new MemexNodeHeadingId(1), + "## Todo", "- A task yet finished").visit(memex); + + GemtextDocumentUpdateCalculator updateCalculator = new GemtextDocumentUpdateCalculator(memex); + var updates = updateCalculator.calculateUpdates(memex.getDocument(url), MemexNodeHeadingId.ROOT, + GemtextDocument.of(url, "%%% TASKS", "# Header", "## Todo", "- A task yet finished (/)")); + updates.forEach(System.out::println); + for (var update : updates) { + update.visit(memex); + } + + verifyFile(doneFilePath, + "%%% TASKS", + "# Done", + "", + "## Done " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE), + "- A task yet finished (/)", + "## Done 2012-04-30", + "- A very old task (/)"); + + } + + public void verifyFile(String file, String... lines) throws IOException { + Path p = Path.of(tempDir + file); + assertTrue(Files.exists(p), "File " + file + " is missing"); + List actualLines = Files.readAllLines(p); + System.out.println("Expecting: "); + Arrays.stream(lines).forEach(System.out::println); + System.out.println("Got: "); + actualLines.forEach(System.out::println); + System.out.println("-- end -- "); + + assertEquals(lines.length, actualLines.size()); + for (int i = 0; i < lines.length; i++) { + assertEquals(lines[i], actualLines.get(i)); + } + } + +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/change/GemtextTombstoneUpdateCaclulatorTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/change/GemtextTombstoneUpdateCaclulatorTest.java new file mode 100644 index 00000000..4fe30379 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/change/GemtextTombstoneUpdateCaclulatorTest.java @@ -0,0 +1,106 @@ +package nu.marginalia.wmsa.memex.change; + +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import lombok.SneakyThrows; +import nu.marginalia.gemini.GeminiService; +import nu.marginalia.util.test.TestUtil; +import nu.marginalia.wmsa.memex.*; +import nu.marginalia.wmsa.memex.renderer.MemexHtmlRenderer; +import nu.marginalia.wmsa.memex.model.MemexNodeUrl; +import nu.marginalia.wmsa.memex.renderer.MemexRendererers; +import nu.marginalia.wmsa.memex.system.MemexFileSystemModifiedTimes; +import nu.marginalia.wmsa.memex.system.MemexFileWriter; +import nu.marginalia.wmsa.memex.system.MemexGitRepo; +import nu.marginalia.wmsa.memex.system.MemexSourceFileSystem; +import org.junit.BeforeClass; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +class GemtextTombstoneUpdateCaclulatorTest { + + private GemtextTombstoneUpdateCaclulator updateCaclulator; + private Memex memex; + private Path tempDir; + + private final String tombstonePath = "/special/tombstone.gmi"; + private final String redirectPath = "/special/redirects.gmi"; + + static Logger logger = LoggerFactory.getLogger(GemtextTombstoneUpdateCaclulatorTest.class); + + @BeforeClass + public static void init() { + + RxJavaPlugins.setErrorHandler(e -> { + if (e.getMessage() == null) { + logger.error("Error", e); + } + else { + logger.error("Error {}: {}", e.getClass().getSimpleName(), e.getMessage()); + } + }); + } + + @SneakyThrows + @BeforeEach + public void setUp() { + tempDir = Files.createTempDirectory("test"); + Files.createDirectory(tempDir.resolve("special")); + + updateCaclulator = new GemtextTombstoneUpdateCaclulator( + tombstonePath, + redirectPath + ); + var data = new MemexData(); + + memex = new Memex(data, null, + Mockito.mock(MemexGitRepo.class), + new MemexLoader(data, new MemexFileSystemModifiedTimes(), + new MemexSourceFileSystem(tempDir, Mockito.mock(MemexGitRepo.class)), tempDir, tombstonePath, redirectPath), + Mockito.mock(MemexFileWriter.class), + updateCaclulator, + Mockito.mock(MemexRendererers.class), + Mockito.mock(GeminiService.class)); + } + + @SneakyThrows + @AfterEach + public void tearDown() { + TestUtil.clearTempDir(tempDir); + } + + @Test + void addTombstone() throws IOException { + updateCaclulator.addTombstone(new MemexNodeUrl("/deleted.gmi"), "It's gone jimmy").visit(memex); + updateCaclulator.addTombstone(new MemexNodeUrl("/deleted2.gmi"), "RIP").visit(memex); + List lines = Files.readAllLines(Path.of(tempDir + tombstonePath)); + assertEquals(3, lines.size()); + + assertEquals("# Tombstones", lines.get(0)); + assertEquals("=> /deleted.gmi\tIt's gone jimmy", lines.get(1)); + assertEquals("=> /deleted2.gmi\tRIP", lines.get(2)); + } + + @Test + void addRedirect() throws IOException { + updateCaclulator.addRedirect(new MemexNodeUrl("/deleted.gmi"), "/new").visit(memex); + updateCaclulator.addRedirect(new MemexNodeUrl("/deleted2.gmi"), "/new2").visit(memex); + List lines = Files.readAllLines(Path.of(tempDir + redirectPath)); + + assertEquals(3, lines.size()); + assertEquals("# Redirects", lines.get(0)); + assertEquals("=> /deleted.gmi\t/new", lines.get(1)); + assertEquals("=> /deleted2.gmi\t/new2", lines.get(2)); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/model/MemexNodeHeadingIdTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/model/MemexNodeHeadingIdTest.java new file mode 100644 index 00000000..f0163b3b --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/memex/model/MemexNodeHeadingIdTest.java @@ -0,0 +1,33 @@ +package nu.marginalia.wmsa.memex.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class MemexNodeHeadingIdTest { + @Test + public void test() { + var heading = new MemexNodeHeadingId(0); + assertEquals("0", heading.toString()); + assertEquals("1", heading.next(0).toString()); + assertEquals("0.1", heading.next(1).toString()); + assertEquals("0.0.1", heading.next(2).toString()); + assertEquals("0.1", heading.next(2).next(1).toString()); + } + + @Test + public void testParenthood() { + var heading = new MemexNodeHeadingId(1,0,2); + + assertTrue(heading.isChildOf(new MemexNodeHeadingId(1,0))); + assertTrue(heading.isChildOf(new MemexNodeHeadingId(1))); + assertFalse(heading.isChildOf(new MemexNodeHeadingId(1,1))); + assertFalse(heading.isChildOf(new MemexNodeHeadingId(1,0,1))); + } + + @Test + public void testComparator() { + assertTrue(new MemexNodeHeadingId(1).compareTo(new MemexNodeHeadingId(2)) < 0); + assertTrue(new MemexNodeHeadingId(1).compareTo(new MemexNodeHeadingId(1, 1)) > 0); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/podcasts/PodcastFetcherTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/podcasts/PodcastFetcherTest.java new file mode 100644 index 00000000..0033464e --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/podcasts/PodcastFetcherTest.java @@ -0,0 +1,15 @@ +package nu.marginalia.wmsa.podcasts; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class PodcastFetcherTest { + + @Test + void fetchPodcast() { + var result = new PodcastFetcher().fetchPodcast("hopwag", "https://rss.acast.com/readmeapoem"); + assertTrue(result.isPresent()); + System.out.println(result); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/resource_store/ResourceStoreServiceTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/resource_store/ResourceStoreServiceTest.java new file mode 100644 index 00000000..daf43337 --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/resource_store/ResourceStoreServiceTest.java @@ -0,0 +1,119 @@ +package nu.marginalia.wmsa.resource_store; + +import lombok.SneakyThrows; +import nu.marginalia.util.TestUtil; +import nu.marginalia.wmsa.configuration.server.Context; +import nu.marginalia.wmsa.configuration.server.Initialization; +import nu.marginalia.wmsa.resource_store.model.RenderedResource; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Spark; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +class ResourceStoreServiceTest { + static ResourceStoreService service; + static ResourceStoreClient client; + + static int testPort = TestUtil.getPort(); + static ResourceEntityStore resourceStore; + static Path tempDir; + private static Logger logger = LoggerFactory.getLogger(ResourceStoreServiceTest.class); + + @SneakyThrows + @BeforeAll + public static void setUpClass() { + Spark.port(testPort); + System.setProperty("service-name", "renderer"); + + client = new ResourceStoreClient(); + client.setServiceRoute("127.0.0.1", testPort); + tempDir = Files.createTempDirectory("ResourceStoreServiceTest"); + resourceStore = new ResourceEntityStore(tempDir); + service = new ResourceStoreService("127.0.0.1", testPort, null, + resourceStore, new Initialization(), null); + + Spark.awaitInitialization(); + } + + @AfterEach + public void clearTempDir() { + for (File f : tempDir.toFile().listFiles()) { + for (File f2 : f.listFiles()) { + logger.debug("Deleting {} -> {}", f2, f2.delete()); + } + logger.debug("Deleting {} -> {}", f, f.delete()); + } + } + + @AfterAll + public static void tearDownAll() { + tempDir.toFile().delete(); + Spark.awaitStop(); + } + + @Test + public void sunnyDay() throws IOException { + client.putResource(Context.internal(), "test", new RenderedResource("index.html", LocalDateTime.MAX,"Hello World")).blockingSubscribe(); + assertEquals("Hello World", client.getResource(Context.internal(),"test", "index.html").blockingFirst()); + } + + + @Test + public void loadFromDisk() throws IOException, InterruptedException { + client.putResource(Context.internal(), "test", new RenderedResource("index.html", LocalDateTime.MAX,"Hello World")).blockingSubscribe(); + client.putResource(Context.internal(), "test", new RenderedResource("expired.html", LocalDateTime.now().minusDays(14),"Hello World")).blockingSubscribe(); + + var resourceStore2 = new ResourceEntityStore(tempDir, true); + Thread.sleep(1000); + var resource = resourceStore2.getResource("test", "index.html"); + + assertNotNull(resource); + assertEquals("Hello World", resource.data); + + assertNull(resourceStore2.getResource("test", "expired.html")); + } + + @Test + public void testReaper() throws IOException { + client.putResource(Context.internal(), "test", new RenderedResource("index.html", LocalDateTime.now().minusDays(14),"Hello World")).blockingSubscribe(); + assertEquals("Hello World", client.getResource(Context.internal(),"test", "index.html").blockingFirst()); + + resourceStore.reapStaleResources(); + + var ret = client + .getResource(Context.internal(), "test", "index.html") + .onErrorReturnItem("Error") + .blockingFirst(); + assertEquals("Error", ret); + } + + + @Test + public void update() throws IOException { + client.putResource(Context.internal(), "test", new RenderedResource("index.html", LocalDateTime.MAX,"Hello World")).blockingSubscribe(); + assertEquals("Hello World", client.getResource(Context.internal(),"test", "index.html").blockingFirst()); + client.putResource(Context.internal(), "test", new RenderedResource("index.html", LocalDateTime.MAX,"Hello World 2")).blockingSubscribe(); + assertEquals("Hello World 2", client.getResource(Context.internal(), "test", "index.html").blockingFirst()); + } + + @Test + public void missing() throws IOException { + var ret = client + .getResource(Context.internal(), "test", "invalid.html") + .onErrorReturnItem("Error") + .blockingFirst(); + assertEquals("Error", ret); + } + +} \ No newline at end of file diff --git a/marginalia_nu/src/test/java/nu/marginalia/wmsa/smhi/scraper/crawler/SmhiBackendApiTest.java b/marginalia_nu/src/test/java/nu/marginalia/wmsa/smhi/scraper/crawler/SmhiBackendApiTest.java new file mode 100644 index 00000000..b807afad --- /dev/null +++ b/marginalia_nu/src/test/java/nu/marginalia/wmsa/smhi/scraper/crawler/SmhiBackendApiTest.java @@ -0,0 +1,33 @@ +package nu.marginalia.wmsa.smhi.scraper.crawler; + +import nu.marginalia.wmsa.smhi.model.Plats; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.*; + +class SmhiBackendApiTest { + + @Test + void hamtaData() throws Exception { + var api = new SmhiBackendApi("nu.marginalia"); + + + System.out.println(api.hamtaData(new Plats("Ystad", "55.42966", "13.82041")) + .jsonContent + ); + } + + @Test + public void testDatum() { + System.out.println(LocalDateTime.parse("2021-05-29T14:06:48Z", + DateTimeFormatter.ISO_ZONED_DATE_TIME) + .atZone(ZoneId.of("GMT")) + .toOffsetDateTime() + .atZoneSameInstant(ZoneId.of("Europe/Stockholm")) + ); + } +} \ No newline at end of file diff --git a/marginalia_nu/src/test/resources/html/monadnock.html b/marginalia_nu/src/test/resources/html/monadnock.html new file mode 100644 index 00000000..324b8cd3 --- /dev/null +++ b/marginalia_nu/src/test/resources/html/monadnock.html @@ -0,0 +1,17 @@ + + + + Monadnock Valley Press + + + + + + +

      Monadnock Valley Press

      +

      The Monadnock Valley Press is an online publisher of public domain texts that reflect our vision of human potential, with a special focus on the literature of freedom and the classics of Western civilization, the Anglosphere, and America.

      +

      We publish works by a wide range of authors in literature, philosophy, history, the sciences, and the arts. For information about texts we have published so far, consult the chronology. The credits might also be of interest.

      +

      Last Updated: 2021-01-21.

      + + + diff --git a/marginalia_nu/src/test/resources/log4j2.properties b/marginalia_nu/src/test/resources/log4j2.properties new file mode 100644 index 00000000..9c2dbefd --- /dev/null +++ b/marginalia_nu/src/test/resources/log4j2.properties @@ -0,0 +1,15 @@ + +status = info + +appender.console.type = Console +appender.console.name = LogToConsole +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %highlight{%-5level}{FATAL=red, ERROR=red, WARN=yellow} %c{1}- %msg%n + +logger.console.name = nu.marginalia +logger.console.level = debug +logger.console.additivity = false +logger.console.appenderRef.rolling.ref = LogToConsole + +rootLogger.level = info +rootLogger.appenderRef.console.ref = LogToConsole diff --git a/marginalia_nu/src/test/resources/model-data.json b/marginalia_nu/src/test/resources/model-data.json new file mode 100644 index 00000000..063b93e7 --- /dev/null +++ b/marginalia_nu/src/test/resources/model-data.json @@ -0,0 +1 @@ +{"comments":[{"id":{"value":"t1_gku7btj"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-26 17:35:16","author":"TheEmpathyBox","body":"Hello,\n\nHow would you translate in latin \" *unreliable narrator*\" ?","sequenceNumber":2,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":596601000}}},{"id":{"value":"t1_gkudywj"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gku7btj"},"distinguished":false,"created_utc":"21-01-26 18:23:03","author":"kc_kennylau","body":"Literally it would be \"narr?tor ?nfid?lis\", but obviously this is a recent term ([Wiktionary](https://en.wiktionary.org/wiki/unreliable_narrator) says coined in 1961) that does not have an immediate equivalence to Roman literature that comes to mind.","sequenceNumber":3,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":597256000}}},{"id":{"value":"t1_gkxfa9w"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkudywj"},"distinguished":false,"created_utc":"21-01-27 09:46:25","author":"BobTheSCV","body":"Seems strange there wouldn\u0027t be a term for this. Unreliable narrators go back as far as Homer with the Odyssey.\n\nEvery part of the story Odysseus narrates is full of fantastical monsters and extremely unlikely events; a complete break from the story as narrated by the voice of Homer which is more in line with the style of the Illiad, a lot more down to earth except for minor interventions by the gods.\n\nAs to make a point, the guy is shown compulsively deceiving every single person he meets: Gods, beasts, enemies, allies alike. Why *wouldn\u0027t* he deceive the audience as well?","sequenceNumber":4,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":597341000}}},{"id":{"value":"t1_gkuf0n7"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gku7btj"},"distinguished":false,"created_utc":"21-01-26 18:30:35","author":"BaconJudge","body":"I\u0027d convey that idea with an adjective meaning \"untrustworthy,\" either *narrator infidus* or *narrator infidelis*, the former possibly helping to avoid the secondary religious sense of the latter.\n\nThe noun *narrator* traditionally existed only in masculine form, but the explicitly feminine version *narratrix* appears in newer references like the Vatican\u0027s *Lexicon Recentis Latinitatis* and Rene Hoeven\u0027s *Lexique de la Prose Latine de la Renaissance,* so for a female character you\u0027d have the option of *narratrix infida* (note the change at the end of the adjective) or *narratrix infidelis*, if you wanted.","sequenceNumber":5,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":597415000}}},{"id":{"value":"t1_gkug4j7"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gku7btj"},"distinguished":false,"created_utc":"21-01-26 18:38:25","author":"lutetiensis","body":"*incredibilis narrator.*\n\n[*incredibilis*](https://logeion.uchicago.edu/incredibilis) \u003d *in* \\+ [*credibilis*](https://logeion.uchicago.edu/credibilis), from [*credere*](https://logeion.uchicago.edu/credo), to believe, to intrust.","sequenceNumber":6,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":597483000}}},{"id":{"value":"t1_gkue6uo"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gku7btj"},"distinguished":false,"created_utc":"21-01-26 18:24:37","author":"EgoSumInHorto","body":"\"*Narr?tor viti?sissimus*\"","sequenceNumber":7,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":597550000}}},{"id":{"value":"t1_gkv6xf8"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkue6uo"},"distinguished":false,"created_utc":"21-01-26 21:39:47","author":"Tharadin1970","body":"Wouldnt that be more \"a very vice-ful narrator\"?","sequenceNumber":8,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":597617000}}},{"id":{"value":"t1_gkv76xq"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkv6xf8"},"distinguished":false,"created_utc":"21-01-26 21:41:21","author":"EgoSumInHorto","body":"I couldn\u0027t find a word for \"unreliable\"; \"viti?sus\" means \"full of faults, corrupt, vicious, morally faulty, defective\", hence \"unreliable\"","sequenceNumber":9,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":597684000}}},{"id":{"value":"t1_gkv7rzf"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkv76xq"},"distinguished":false,"created_utc":"21-01-26 21:45:06","author":"lutetiensis","body":"\u003e \"unreliable\"\n\nIncredibilis (see my comment).","sequenceNumber":10,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":597750000}}},{"id":{"value":"t1_gkv848d"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkv7rzf"},"distinguished":false,"created_utc":"21-01-26 21:47:13","author":"EgoSumInHorto","body":"That should have been a pretty obvious derivation to make... Doh!\nThanks :)","sequenceNumber":11,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":597823000}}},{"id":{"value":"t1_gkup1zh"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-26 19:39:00","author":"Gopnikcykablyat","body":"How do you say \"Hope never dies, because hope is the killer\" ?","sequenceNumber":12,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":597887000}}},{"id":{"value":"t1_gkuvwow"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkup1zh"},"distinguished":false,"created_utc":"21-01-26 20:26:06","author":"lutetiensis","body":"For stylistic reasons, I would render it as:\n\n*spes non decedit sed caedit.*\n\nHope doesn\u0027t die, but kills. You can replace *non* with *numquam* (\"never\") *sed* with *quia* (\"because \\[it\\]\").","sequenceNumber":13,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":597954000}}},{"id":{"value":"t1_gkuqvlo"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkup1zh"},"distinguished":false,"created_utc":"21-01-26 19:51:09","author":"BluuDuud","body":"Id say, \"sp?s numquam moritur, nam sp?s nec?tor est\"","sequenceNumber":14,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":598026000}}},{"id":{"value":"t1_gkvrzwi"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkuqvlo"},"distinguished":false,"created_utc":"21-01-27 00:08:45","author":"magistramegaera","body":"Should it be necatrix instead of necator, since spes is feminine? Or does that not really matter with an abstract concept like this?","sequenceNumber":15,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":598092000}}},{"id":{"value":"t1_gl60nlx"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkvrzwi"},"distinguished":false,"created_utc":"21-01-29 01:01:50","author":"Sochamelet","body":"I wouldn\u0027t say it\u0027s wrong per se to use *necator*, but in my experience, Roman authors were generally inclined to preserve correspondences between the gender of words, if possible.","sequenceNumber":16,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":598158000}}},{"id":{"value":"t1_gkwj57n"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkvrzwi"},"distinguished":false,"created_utc":"21-01-27 03:48:35","author":"BluuDuud","body":"I forgot that, I think you\u0027re correct","sequenceNumber":17,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":598223000}}},{"id":{"value":"t1_gkzsh08"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkwj57n"},"distinguished":false,"created_utc":"21-01-27 21:15:26","author":"glaraaaaaaah","body":"No it doesn?t, _necator_ is a masculine noun not an adjective, so it doesn?t need to agree. Unless you want to specify that hope is female, male-gendered words are more common for abstract concepts I think","sequenceNumber":18,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":598287000}}},{"id":{"value":"t1_gkuthtv"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-26 20:09:24","author":"Youngerthandumb","body":"There was a quote from a Scandanavian bishop who, on his deathbed suffering from intense pains, cried out \"Do not pray me out of god\u0027s battle!\" when his colleagues gathered round to pray for his recovery. I thought that was kind of metal. How would that translate to Latin?","sequenceNumber":19,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":598351000}}},{"id":{"value":"t1_gkv2ymf"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkuthtv"},"distinguished":false,"created_utc":"21-01-26 21:13:38","author":"nimbleping","body":"*M? (mihi (?)) ? pugn? De? n?n d?prec?re/d?prec?min?* (sg./pl. addressees).\n\n(Do not intercede by prayer on behalf of me away from the battle of God.)\n\nEDIT: I\u0027m not 100% sure if the accusative *m?* or the dative *mihi* should be used here. I\u0027d who knows to offer an opinion. I figure the ablative of motion away from would be appropriate here.","sequenceNumber":20,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":598419000}}},{"id":{"value":"t1_gkv8czl"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkv2ymf"},"distinguished":false,"created_utc":"21-01-26 21:48:49","author":"Youngerthandumb","body":"Thank you I appreciate it! I took 2 years of high school latin but I don\u0027t trust myself to translate anything beyond \"Caecilius ad venit\".","sequenceNumber":21,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":598484000}}},{"id":{"value":"t1_gkv9lku"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-26 21:57:03","author":"XENO-BLAZE","body":"How would you translate:\n\nAllow me to impress upon you the severe mistake you have made. For years my conduct has been largely benign. And yet, without provocation, you have severed our détente and forced me to unleash upon you the vengeful flames of a thousand suns. You shall curse your mother for the day of your birth. So, go now, go, and begin your life of fear, knowing that when you least expect it, the looming sword of Damocles will crash upon you, cleaving you in twain and as you gaze upon the smoking wreckage that was once your life, you will regret the day you crossed me","sequenceNumber":22,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":598545000}}},{"id":{"value":"t1_gkve8fd"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-26 22:28:44","author":"iillltt","body":"Hello! I\u0027m not sure if this fits here but I was wondering if anyone would be able to identify/translate what the chant in the beginning of [this song](https://youtu.be/3oUUG7Mfoc4) is. I think it\u0027s in Latin so forgive me if it\u0027s not","sequenceNumber":23,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":598610000}}},{"id":{"value":"t1_gkvkja8"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkve8fd"},"distinguished":false,"created_utc":"21-01-26 23:12:05","author":"lutetiensis","body":"That\u0027s hard... *regum satus*? *sanctus?* Do you know what they sampled?","sequenceNumber":24,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":598686000}}},{"id":{"value":"t1_gkvm21e"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkvkja8"},"distinguished":false,"created_utc":"21-01-26 23:23:51","author":"iillltt","body":"unfortunately not, i\u0027ve heard other people say it\u0027s \u0027Spiritus Sanctus\u0027 but i haven\u0027t been able to find the original sample- same with regum satus which doesn\u0027t have any search results. thank you so much as well","sequenceNumber":25,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":598753000}}},{"id":{"value":"t1_gkvmwyy"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkvm21e"},"distinguished":false,"created_utc":"21-01-26 23:30:26","author":"lutetiensis","body":"Ok. Sorry. I don\u0027t think I can do more on this one.","sequenceNumber":26,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":598819000}}},{"id":{"value":"t1_gkvnbr3"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkvmwyy"},"distinguished":false,"created_utc":"21-01-26 23:33:31","author":"iillltt","body":"thank you so much for trying!!! i\u0027ll be questioning producers where they get samples from next haha","sequenceNumber":27,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":598880000}}},{"id":{"value":"t1_gkvm2ak"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkve8fd"},"distinguished":false,"created_utc":"21-01-26 23:23:54","author":"ragnrikr","body":"Can\u0027t really help (to my ears it sounds like (sectum/secum) (satu/sato) I.e. gibberish), just wanted to point out this post https://www.reddit.com/r/kpophelp/comments/g49xob/latin_in_gottasadae/\n\nNo source sadly :/","sequenceNumber":28,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":598944000}}},{"id":{"value":"t1_gkvn3fo"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkvm2ak"},"distinguished":false,"created_utc":"21-01-26 23:31:47","author":"iillltt","body":"Yep :( \nI was reminded of this question when someone asked a similar question as to your linked post so it still hasn\u0027t been answered. well on the bright side I got to share a nice song and the mystery will remain unsolved ...","sequenceNumber":29,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":599016000}}},{"id":{"value":"t1_gkw3c5e"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-27 01:38:08","author":"luisafonsoteixeira","body":"I\u0027ve recently came across the US Navy Academy saying \"Ex Scientia Tridens\", ?Through Knowledge, Sea Power?. How could one correctly say \"through knowledge, power\"?","sequenceNumber":30,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":599082000}}},{"id":{"value":"t1_gkwzw4j"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkw3c5e"},"distinguished":false,"created_utc":"21-01-27 06:24:34","author":"lutetiensis","body":"\u003epower\n\n[Potentia](https://logeion.uchicago.edu/potentia), [imperium](https://logeion.uchicago.edu/imperium), [fortitudo](https://logeion.uchicago.edu/fortitudo)...\n\nNote the original motto is poetic. It doesn\u0027t say \"sea power\", but instead \"\\[Neptune\u0027s\\] trident\". You could find an artifact that would represent power for you (a sword? a crown?).","sequenceNumber":31,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":599146000}}},{"id":{"value":"t1_gkw4obz"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-27 01:48:56","author":"Eldonith","body":"Help me fix a cringe tattoo!\n\nBackground: I got a tattoo back when the emo craze was big and Hot Topic was one of the most popular stores in the mall about 12 years ago. It reads \"Nascentes Morimur\" which roughly translated to \"When we are born we begin to die.\" It seemed cool at the time in my addled 18 year old mind, but I\u0027m a family man now and not only does it no longer represent my mindset. but it\u0027s even embarrassing to explain.\n\nWhat line (in Latin) would you suggest I add to brighten it up? Please include a translation, as my highschool Latin days are far behind me. Thanks!","sequenceNumber":32,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":599207000}}},{"id":{"value":"t1_gkxz2rq"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkw4obz"},"distinguished":false,"created_utc":"21-01-27 14:11:03","author":"BaconJudge","body":"Given the constraint that it\u0027ll contain a reference to dying, is there any particular sentiment you want to convey? Because you\u0027re a family man, maybe you could expand it to something like *Amati Nascentes, Morimur Amati\" (inserting the optional comma if there\u0027s room) to imply roughly \"Born loved, we die loved.\"","sequenceNumber":33,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":599267000}}},{"id":{"value":"t1_gl1ij4p"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkxz2rq"},"distinguished":false,"created_utc":"21-01-28 04:12:22","author":"Eldonith","body":"Wow that\u0027s beautiful and exactly the kinda sentiment I\u0027d like it changed to! I love how you transformed it with a word added to the beginning and end instead of a whole 2nd line. I may very well end up going with this unless anybody can top that suggestion.","sequenceNumber":34,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":599334000}}},{"id":{"value":"t1_gl83w6w"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gl1ij4p"},"distinguished":false,"created_utc":"21-01-29 12:52:05","author":"BaconJudge","body":"Happy to help, and I hope the tattoo change works out for you.","sequenceNumber":35,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":599401000}}},{"id":{"value":"t1_gkwk2sa"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-27 03:56:29","author":"reds3232","body":" mater servum vituperavit","sequenceNumber":36,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":599464000}}},{"id":{"value":"t1_gkwlsx3"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-27 04:11:09","author":"megtheedemon","body":"How do you say ?I would rather be studying latin?","sequenceNumber":37,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":599524000}}},{"id":{"value":"t1_gkxgp1b"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkwlsx3"},"distinguished":false,"created_utc":"21-01-27 10:07:50","author":"kc_kennylau","body":"Lat?nae linguae stud?re m?l?","sequenceNumber":38,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":599586000}}},{"id":{"value":"t1_gkx6rax"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-27 07:45:53","author":"ResidentGift","body":"In a game I\u0027m playing, there are characters named Alatus, Bosacius, Indarias, Bonanus, and Menogias. I\u0027m pretty sure Alatus is the Latin word for \"*winged*\" (or at least related to \"*wing*\") and it also suits the character\u0027s motif. The other four names sound Latin-ish, but I can\u0027t find anything on them. Can anyone confirm if the other four names are Latin or rooted in Latin? If yes, how would they be translated?","sequenceNumber":39,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":599646000}}},{"id":{"value":"t1_gkxwqf0"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkx6rax"},"distinguished":false,"created_utc":"21-01-27 13:45:42","author":"BaconJudge","body":"You\u0027re right about Alatus, but I don\u0027t think the others are Latin-derived. Bosacius could have been loosely inspired by words like *boscis* (\"waterfowl\") or Medieval Latin *boscus* (\"woods\") if either of those makes sense for the character. If Bonanus is the good guy, the name might be inspired by the very common Latin adjective *bonus* (\"good\"). Words or names ending in -as are likely to be Greek rather than Latin, and I don\u0027t recognize any promising Latin roots for those two unless Indarias is from India, which is the same in Latin.","sequenceNumber":40,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":599714000}}},{"id":{"value":"t1_gl0mrhe"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkxwqf0"},"distinguished":false,"created_utc":"21-01-28 00:36:11","author":"ResidentGift","body":"Thank you for the help!","sequenceNumber":41,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":599777000}}},{"id":{"value":"t1_gkxpfxd"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-27 12:17:05","author":"stickybeak7","body":"Hi there! Hoping to get a translation similar to \"memento mori\" but for \"remember you are loved\" for a valentines gift :o) thank you so much!","sequenceNumber":42,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":599838000}}},{"id":{"value":"t1_gkxsyty"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkxpfxd"},"distinguished":false,"created_utc":"21-01-27 13:02:11","author":"aveCaecilius","body":"memento amari","sequenceNumber":43,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":599899000}}},{"id":{"value":"t1_gkzyg27"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkxsyty"},"distinguished":false,"created_utc":"21-01-27 21:54:16","author":"stickybeak7","body":"thank you so much! ?","sequenceNumber":44,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":599965000}}},{"id":{"value":"t1_gkxpgd0"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-27 12:17:16","author":"pierro_la_place","body":"Hi there!\n\nI am building a mediaval-ish world in which the people of kingdom A really don\u0027t like kingdom B they are at war with, to the point that they refuse to pronounce its name. Instead they say someting along the lines of \"land of the rapists\", but in Latin to give it an almost religious tone. After a bit of research, the translation I was thinking about was \"terra stuparotes\", but I am not an expert in Latin so I would like to know what you think.\n\nThanks!","sequenceNumber":45,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":600039000}}},{"id":{"value":"t1_gkxvjvf"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkxpgd0"},"distinguished":false,"created_utc":"21-01-27 13:32:32","author":"BaconJudge","body":"That\u0027s probably a typo for *stupratores*, which is the plural form of the word [*stuprator*](http://www.perseus.tufts.edu/hopper/text?doc\u003dPerseus%3Atext%3A1999.04.0059%3Aentry%3Dstuprator) when used as the subject of a sentence. Because you want it as a possessive plural (\"of the rapists\"), the phrase would be *terra stupratorum*.","sequenceNumber":46,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":600100000}}},{"id":{"value":"t1_gkxypyz"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkxvjvf"},"distinguished":false,"created_utc":"21-01-27 14:07:19","author":"pierro_la_place","body":"Yup, I mixed up accusative and genitive. Good thing I asked! Thx.","sequenceNumber":47,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":600161000}}},{"id":{"value":"t1_gkxzjnm"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkxypyz"},"distinguished":false,"created_utc":"21-01-27 14:15:50","author":"BaconJudge","body":"You\u0027re welcome, anytime.","sequenceNumber":48,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":600231000}}},{"id":{"value":"t1_gkylxmm"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-27 16:55:07","author":"Koa1121","body":"How would you say ?Where easy becomes hard? or ?Where easy is hard?","sequenceNumber":49,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":600292000}}},{"id":{"value":"t1_gl38i9x"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkylxmm"},"distinguished":false,"created_utc":"21-01-28 15:09:01","author":"quintus_sub_rosa","body":"in quo facile fit difficile.","sequenceNumber":50,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":600351000}}},{"id":{"value":"t1_gl0zg1x"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-28 01:53:37","author":"GIGABIT","body":"What\u0027s up my dudes. WSB \"investor\" here. \n\nIn light of the ongoing GameStop insanity you might have heard about I thought it could be cool to get a tattoo celebrating the gains. Naturally it has to be the \"Power to the players\" slogan, but from what I understand, the word \"player\" isn\u0027t really a thing in Latin.\n\nI thought \"Power to the Gambler\" might be a more appropriate choice considering the nature of both parties involved, and because there actually might be a proper word for \"gambler\".\n\nSo I thought I\u0027d ask you guys for some advice about the sentence so I don\u0027t go printing myself with something stupid like \"short the market\".\n\nThanks in advance!","sequenceNumber":51,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":600408000}}},{"id":{"value":"t1_gl29fog"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gl0zg1x"},"distinguished":false,"created_utc":"21-01-28 08:38:37","author":"kc_kennylau","body":"potentia ad aleatores\n\nPS: don\u0027t trust internet strangers (such as me!) for tattoo","sequenceNumber":52,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":600486000}}},{"id":{"value":"t1_gl33bxa"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gl29fog"},"distinguished":false,"created_utc":"21-01-28 14:37:06","author":"GIGABIT","body":"Thanks a lot!\n\nI SHOULD know better than to trust internet strangers... Then again, you know what everyone is over at WSB, so..","sequenceNumber":53,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":600548000}}},{"id":{"value":"t1_gl1qbxz"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-28 05:16:51","author":"dpm5150","body":"I want to mount a motto carved in wood in my library. I?m trying to figure out a good noun to express ?usefulness?. I?m want to express to my son that he should strive for contributing to society in ways that return tangible value. Usefulness, itself, is a dull word, so I?m not sure if Latin has something with a little more zing.","sequenceNumber":54,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":600612000}}},{"id":{"value":"t1_gl29810"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gl1qbxz"},"distinguished":false,"created_utc":"21-01-28 08:35:56","author":"kc_kennylau","body":"utilitas / ?tilit?s (whence English \"utility\")","sequenceNumber":55,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":600672000}}},{"id":{"value":"t1_gl3020g"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gl29810"},"distinguished":false,"created_utc":"21-01-28 14:13:35","author":"dpm5150","body":"Yes, this definitely can work. Now I have to decide how inspirational it is for a motto. Thank you so much.","sequenceNumber":56,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":600732000}}},{"id":{"value":"t1_gl4ppso"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-28 20:03:46","author":"Rashed8StringVi","body":"From the popular phrase ?In Vino Veritas?, how would one correctly substitute ?blood? instead of ?wine? such that the phrase becomes ?in blood is the truth??","sequenceNumber":57,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":600790000}}},{"id":{"value":"t1_gl54pgr"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gl4ppso"},"distinguished":false,"created_utc":"21-01-28 21:34:34","author":"kc_kennylau","body":"in sanguine veritas","sequenceNumber":58,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":600847000}}},{"id":{"value":"t1_gl55c0i"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-28 21:38:27","author":"coralcakes","body":"How would An Appeal to Heaven be translated into latin?","sequenceNumber":59,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":600903000}}},{"id":{"value":"t1_gl5nhd2"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gl55c0i"},"distinguished":false,"created_utc":"21-01-28 23:28:44","author":"jayzwasinnirvana","body":"*Obsecratio ad caelum* is one way. I\u0027m not sure if there is an existing phrase. I did not use *appellatio* because I think it\u0027s meaning in Latin is more strictly legal.","sequenceNumber":60,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":600978000}}},{"id":{"value":"t1_gl5quql"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gl55c0i"},"distinguished":false,"created_utc":"21-01-28 23:52:04","author":"kc_kennylau","body":"Quite literally \"appellatio ad caelum\".","sequenceNumber":61,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":601035000}}},{"id":{"value":"t1_gl57uz9"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-28 21:53:32","author":"RandyDautzenberg","body":"Hey all, \n\n\nCurrently I\u0027m looking for a translation of \u0027to the top\u0027 / \u0027the top\u0027 in Latin. Some internet translators are giving me different translations. Most of the time they translate it as \u0027ad summitatem\u0027 or \u0027ad verticem\u0027. \n\n\nI really like the word \u0027verticem\u0027, haha. So I was wondering if you can also use \u0027verticem\u0027 without the preposition \u0027ad\u0027. Or would that be grammatically incorrect? \n\n\nMany thanks! :)","sequenceNumber":62,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":601091000}}},{"id":{"value":"t1_gl5khcv"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gl57uz9"},"distinguished":false,"created_utc":"21-01-28 23:08:37","author":"kc_kennylau","body":"\"the top\" \u003d apex / vertex\n\n\"to the top\" \u003d ad apicem / ad verticem\n\nIt would be better to provide context (e.g. whole sentence).","sequenceNumber":63,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":601147000}}},{"id":{"value":"t1_gl5lazb"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gl5khcv"},"distinguished":false,"created_utc":"21-01-28 23:14:06","author":"RandyDautzenberg","body":"Many thanks for your quick reply! Well, I would like to use it as a brand name, haha. So that means that using verticem without the preposition is not grammatically correct, right?","sequenceNumber":64,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":601203000}}},{"id":{"value":"t1_gl5avpy"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-28 22:11:03","author":"ContuberniumSPQR","body":"Hello \n\nI have to determine the word Casus from Casus,Casus I think it is a nominative but that doesn\u0027t fit with its function in the phrase could it be an accusative?","sequenceNumber":65,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":601268000}}},{"id":{"value":"t1_gl5kal7"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gl5avpy"},"distinguished":false,"created_utc":"21-01-28 23:07:22","author":"kc_kennylau","body":"Perfacilis inventu ex [Wiktionary](https://en.wiktionary.org/wiki/casus#Latin)","sequenceNumber":66,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":601343000}}},{"id":{"value":"t1_gl7qke4"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gl5kal7"},"distinguished":false,"created_utc":"21-01-29 09:59:22","author":"ContuberniumSPQR","body":" Gratias tibi ago","sequenceNumber":67,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":601398000}}},{"id":{"value":"t1_gl6kjfb"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-29 03:30:49","author":"Havatra","body":"Hello!\n\nI need some translation help with this sentence:\n\n\"Remember you must die, so [do] thrive(vigorously), while you are [still] able to.\" \n\"Memento mori; ita vigemusque, dum es possunt.\"\n\nDoes this sound correct? Or is there perhaps a different way you\u0027d rather put it?\n\nI appreciate all help! :-)","sequenceNumber":68,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":601454000}}},{"id":{"value":"t1_gl7b1cb"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-29 07:03:49","author":"Ghost_1243","body":"Hello, I\u0027m trying to translate the following the phrases:\n\n1. A Love of One\u0027s Fate\n2. The Will to Power\n3. Strive for a Higher Purpose\n4. Embrace the Ordinary","sequenceNumber":69,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":601510000}}},{"id":{"value":"t1_gl7bfpk"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-29 07:07:53","author":"DV5161","body":"I?m having trouble translating ?live and die? so when I put that into google translate I get back ?vivere et mori? but when I switch it and go from Latin to English I get back ?live and? but when I put in ?vivamus, moriendum est.? I get back ?live and die? if someone could help me with which is right and wrong it would be greatly appreciated!","sequenceNumber":70,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":601564000}}},{"id":{"value":"t1_gl7hq77"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gl7bfpk"},"distinguished":false,"created_utc":"21-01-29 08:13:05","author":"kc_kennylau","body":"Anything google translate says is wrong.\n\n\"live and die\" \u003d vive et morere (command to 1 person) / vivete et morimini (command to multiple people)\n\nYou can also replace the \"et\" to \"atque\".","sequenceNumber":71,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":601618000}}},{"id":{"value":"t1_gl7hzbb"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gl7hq77"},"distinguished":false,"created_utc":"21-01-29 08:15:50","author":"DV5161","body":"Lmao I had no idea but ok nice, glad to have got that cleared up thank you so much!","sequenceNumber":72,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":601680000}}},{"id":{"value":"t1_gkuwp93"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t3_l5ejhj"},"distinguished":false,"created_utc":"21-01-26 20:31:29","author":"ki4clz","body":"Romanes eunt domis...?","sequenceNumber":73,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":601737000}}},{"id":{"value":"t1_gkuzut8"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkuwp93"},"distinguished":false,"created_utc":"21-01-26 20:52:42","author":"kc_kennylau","body":"People called Romanes, they go, the house?","sequenceNumber":74,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":601792000}}},{"id":{"value":"t1_gkvxdoo"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkuzut8"},"distinguished":false,"created_utc":"21-01-27 00:50:38","author":"ki4clz","body":"It says *Romans Go Home!*...","sequenceNumber":75,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":601848000}}},{"id":{"value":"t1_gkx8lf8"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkvxdoo"},"distinguished":false,"created_utc":"21-01-27 08:10:02","author":"rsotnik","body":"It doesn\u0027t :)\n\nhttps://en.m.wikipedia.org/wiki/Romani_ite_domum","sequenceNumber":76,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":601904000}}},{"id":{"value":"t1_gkxml3q"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkx8lf8"},"distinguished":false,"created_utc":"21-01-27 11:36:21","author":"ki4clz","body":"but latin for Roman is, Romanus...?","sequenceNumber":77,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":601957000}}},{"id":{"value":"t1_gkxn1hb"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkxml3q"},"distinguished":false,"created_utc":"21-01-27 11:42:53","author":"rsotnik","body":"Sorry, you lost me :) Do you want to know what \" Romanes eunt domis\" means or how one says \"Romans, go home!\"?","sequenceNumber":78,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":602016000}}},{"id":{"value":"t1_gkxo45g"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkxn1hb"},"distinguished":false,"created_utc":"21-01-27 11:58:33","author":"ki4clz","body":"Some kind soul was patient with me in this thread, and showed me the way: *(you may need to check their work?)*\n\nhttps://www.reddit.com/r/dankchristianmemes/comments/kp0ua2/a_catholic_a_protestant_and_an_orthodox_walk_into/ghv5c44?utm_medium\u003dandroid_app\u0026utm_source\u003dshare\u0026context\u003d3","sequenceNumber":79,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":602068000}}},{"id":{"value":"t1_gkxoeil"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkxo45g"},"distinguished":false,"created_utc":"21-01-27 12:02:38","author":"rsotnik","body":"Haha, you played me all along :)","sequenceNumber":80,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":602120000}}},{"id":{"value":"t1_gkxok0c"},"submission_id":{"id":"t3_l5ejhj"},"parent_id":{"id":"t1_gkxoeil"},"distinguished":false,"created_utc":"21-01-27 12:04:44","author":"ki4clz","body":"I\u0027ve only ever had one person take the bait... was hoping for a rematch...\n\n***Happy Cake Day!***\n-","sequenceNumber":81,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":602170000}}},{"id":{"value":"t1_gl5au93"},"submission_id":{"id":"t3_l765yl"},"parent_id":{"id":"t3_l765yl"},"distinguished":false,"created_utc":"21-01-28 22:10:49","author":"CabezadeVaca_","body":"Russian priests singing in Latin?? Very beautiful but also very odd","sequenceNumber":83,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":53,"nano":161325000}}},{"id":{"value":"t1_gl5h3k4"},"submission_id":{"id":"t3_l765yl"},"parent_id":{"id":"t1_gl5au93"},"distinguished":false,"created_utc":"21-01-28 22:48:23","author":"HanSo1oCup","body":"*Russian Orthodox* but the monastery is located in WV, USA","sequenceNumber":84,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":53,"nano":161442000}}},{"id":{"value":"t1_gl5i3sq"},"submission_id":{"id":"t3_l765yl"},"parent_id":{"id":"t1_gl5h3k4"},"distinguished":false,"created_utc":"21-01-28 22:54:09","author":"CabezadeVaca_","body":"Well that seems especially odd to me if they?re not even Eastern Catholics. I?ve never understood the Russian or the Greek churches to have much respect for Latin as a liturgical language, but of course that?s just based on my own experiences","sequenceNumber":85,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":53,"nano":161497000}}},{"id":{"value":"t1_gl5pu5j"},"submission_id":{"id":"t3_l765yl"},"parent_id":{"id":"t1_gl5i3sq"},"distinguished":false,"created_utc":"21-01-28 23:45:00","author":"greetings_traveler2","body":"yeah, it\u0027s pretty uncommon for Orthodox priests, I love their chants in ancient Greek though","sequenceNumber":86,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":53,"nano":161547000}}},{"id":{"value":"t1_gl4um7f"},"submission_id":{"id":"t3_l765yl"},"parent_id":{"id":"t3_l765yl"},"distinguished":false,"created_utc":"21-01-28 20:34:13","author":"HanSo1oCup","body":"Correction *It was adapted by the monks in honor of the Holy Prophet David*","sequenceNumber":87,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":53,"nano":161627000}}},{"id":{"value":"t1_gl5n4sd"},"submission_id":{"id":"t3_l765yl"},"parent_id":{"id":"t3_l765yl"},"distinguished":false,"created_utc":"21-01-28 23:26:23","author":"marktwainbrain","body":"Is this a group that has a ?Western Rite? orthodox parish?","sequenceNumber":88,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":53,"nano":161677000}}},{"id":{"value":"t1_gl64il6"},"submission_id":{"id":"t3_l765yl"},"parent_id":{"id":"t1_gl5n4sd"},"distinguished":false,"created_utc":"21-01-29 01:30:08","author":"greetings_traveler2","body":"Is that a thing?","sequenceNumber":89,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":53,"nano":161724000}}},{"id":{"value":"t1_gl64vsg"},"submission_id":{"id":"t3_l765yl"},"parent_id":{"id":"t1_gl64il6"},"distinguished":false,"created_utc":"21-01-29 01:32:52","author":"marktwainbrain","body":"Yes, not at all common. An overture to some Latin traditionalists. I think OCA has Western Rite parishes.\n\nETA: https://en.m.wikipedia.org/wiki/Western_Rite_Orthodoxy","sequenceNumber":90,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":53,"nano":161770000}}},{"id":{"value":"t1_gl5m4eg"},"submission_id":{"id":"t3_l7aez5"},"parent_id":{"id":"t3_l7aez5"},"distinguished":false,"created_utc":"21-01-28 23:19:33","author":"qed1","body":"There is only one appropriate manuscript to share on such an occasion as this: https://digi.vatlib.it/view/MSS_Vat.lat.9850/0016.\n\n^^^^^^^^(Not ^^^^^^^sure ^^^^^^^why ^^^^^^^there ^^^^^^^are ^^^^^^^two ^^^^^^^threads, ^^^^^^^only ^^^^^^^one ^^^^^^^of ^^^^^^^which ^^^^^^^I\u0027m ^^^^^^^seeing ^^^^^^^at ^^^^^^^a ^^^^^^^given ^^^^^^^time... ^^^^^^^but ^^^^^^^this ^^^^^^^should ^^^^^^^obviously ^^^^^^^be ^^^^^^^seen ^^^^^^^in ^^^^^^^both!)","sequenceNumber":92,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":55,"nano":144469000}}},{"id":{"value":"t1_gl730gk"},"submission_id":{"id":"t3_l7aez5"},"parent_id":{"id":"t1_gl5m4eg"},"distinguished":false,"created_utc":"21-01-29 05:52:40","author":"SheepExplosion","body":"Things that make me long for Merovingian chancery hands. Good thing he had 4 scribes or no one would have ever known what he wrote.","sequenceNumber":93,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":55,"nano":144676000}}},{"id":{"value":"t1_gl5ns9x"},"submission_id":{"id":"t3_l7aez5"},"parent_id":{"id":"t1_gl5m4eg"},"distinguished":false,"created_utc":"21-01-28 23:30:47","author":"Kingshorsey","body":"Are you sure that link goes where you want? The title page is nice, but your link goes to a random page in the middle.","sequenceNumber":94,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":55,"nano":144774000}}},{"id":{"value":"t1_gl5oqvi"},"submission_id":{"id":"t3_l7aez5"},"parent_id":{"id":"t1_gl5ns9x"},"distinguished":false,"created_utc":"21-01-28 23:37:26","author":"qed1","body":"That was the point, yes. I was aiming to land at a section of Aquinas\u0027s near incomprehensible handwriting. ;)","sequenceNumber":95,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":55,"nano":144866000}}},{"id":{"value":"t1_gl5ocsz"},"submission_id":{"id":"t3_l7aez5"},"parent_id":{"id":"t1_gl5m4eg"},"distinguished":false,"created_utc":"21-01-28 23:34:45","author":"EmergencySufficient6","body":"... What is *that?* \n\nI thought for sure it\u0027d be his musings upon [the effects of stars on demons](http://www.logicmuseum.com/wiki/Authors/Thomas_Aquinas/Summa_Theologiae/Part_I/Q115#q115a5arg1) or something similar.","sequenceNumber":96,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":55,"nano":144970000}}},{"id":{"value":"t1_gl5ovhj"},"submission_id":{"id":"t3_l7aez5"},"parent_id":{"id":"t1_gl5ocsz"},"distinguished":false,"created_utc":"21-01-28 23:38:18","author":"qed1","body":"It\u0027s Aquinas\u0027s totally incomprehensible handwriting.","sequenceNumber":97,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":55,"nano":145068000}}},{"id":{"value":"t1_gl5rrc7"},"submission_id":{"id":"t3_l7aez5"},"parent_id":{"id":"t1_gl5ovhj"},"distinguished":false,"created_utc":"21-01-28 23:58:24","author":"EmergencySufficient6","body":"How sure are we that there\u0027s a teleological argument against gays in there?\n\nEdit: Forgive me, downvote brigade, for I have sinned.","sequenceNumber":98,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":55,"nano":145174000}}},{"id":{"value":"t1_gl5kbyc"},"submission_id":{"id":"t3_l7aez5"},"parent_id":{"id":"t3_l7aez5"},"distinguished":false,"created_utc":"21-01-28 23:07:37","author":"Kingshorsey","body":"Source: [https://digi.vatlib.it/mss/detail/Urb.lat.136](https://digi.vatlib.it/mss/detail/Urb.lat.136)","sequenceNumber":99,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":55,"nano":145265000}}},{"id":{"value":"t1_gl315u1"},"submission_id":{"id":"t3_l6s45e"},"parent_id":{"id":"t3_l6s45e"},"distinguished":false,"created_utc":"21-01-28 14:21:53","author":"joaojcorreia","body":"Hi u/Irene_SaturaLanx, I was able to see part of the session live, really good. Congratulations. I was really happy, because I was able to follow it. Gratias.","sequenceNumber":103,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":59,"nano":155093000}}},{"id":{"value":"t1_gl3oyaf"},"submission_id":{"id":"t3_l6s45e"},"parent_id":{"id":"t1_gl315u1"},"distinguished":false,"created_utc":"21-01-28 16:28:46","author":"Irene_SaturaLanx","body":"Gratias tibi!","sequenceNumber":104,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":59,"nano":155276000}}},{"id":{"value":"t1_gl31nts"},"submission_id":{"id":"t3_l6s45e"},"parent_id":{"id":"t3_l6s45e"},"distinguished":false,"created_utc":"21-01-28 14:25:30","author":"ironicsadboy","body":"Congratulations on your work!","sequenceNumber":105,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":59,"nano":155374000}}},{"id":{"value":"t1_gl3oz4e"},"submission_id":{"id":"t3_l6s45e"},"parent_id":{"id":"t1_gl31nts"},"distinguished":false,"created_utc":"21-01-28 16:28:53","author":"Irene_SaturaLanx","body":"Thanks!","sequenceNumber":106,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":59,"nano":155466000}}},{"id":{"value":"t1_gl45js7"},"submission_id":{"id":"t3_l6s45e"},"parent_id":{"id":"t3_l6s45e"},"distinguished":false,"created_utc":"21-01-28 17:53:49","author":"logatwork","body":"I\u0027m reading/studying this book! Spoilers ahead!\n\nThank you","sequenceNumber":107,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":59,"nano":155556000}}},{"id":{"value":"t1_gl4w10f"},"submission_id":{"id":"t3_l6s45e"},"parent_id":{"id":"t3_l6s45e"},"distinguished":false,"created_utc":"21-01-28 20:42:52","author":"Redbubbles55","body":"hunc librem nunc perlego, sperans posthac linguam latinam modo eius docere. gaudeo multum sessionem tuam spectauisse, gratias ueras tibi ago !","sequenceNumber":108,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":59,"nano":155643000}}},{"id":{"value":"t1_gl510zz"},"submission_id":{"id":"t3_l6s45e"},"parent_id":{"id":"t3_l6s45e"},"distinguished":false,"created_utc":"21-01-28 21:12:53","author":"Monsieurantipyrine","body":"Truly one (or two) of the best texts out there for Latin students! Very glad I learned from this back in University.","sequenceNumber":109,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":59,"nano":155732000}}},{"id":{"value":"t1_gl58d7w"},"submission_id":{"id":"t3_l6s45e"},"parent_id":{"id":"t3_l6s45e"},"distinguished":false,"created_utc":"21-01-28 21:56:31","author":"scriptapuella","body":"My university is grammar focussed, but I teach with this book (alongside the companion) to aid comprehension and get students used to continuous Latin passages early. Evaluations indicate they like it more than Wheelock, at any rate.","sequenceNumber":110,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":59,"nano":155832000}}},{"id":{"value":"t1_gl7scfq"},"submission_id":{"id":"t3_l7iwsm"},"parent_id":{"id":"t3_l7iwsm"},"distinguished":false,"created_utc":"21-01-29 10:22:23","author":"kc_kennylau","body":"* You dare to drink wine in my presence?\n\n\"you dare to drink wine\" is indeed \"vinum bibere audes\", but \"ante\" takes the accusative instead of the dative, so it would be \"ante me\" instead of \"ante mi\". Other possible translations of \"in my presence\" include \"coram me\" and \"prae me\".\n\nE.g. Exodus 23:3 Non habebis deos alienos **coram me**. \"Thou shalt have no other gods **before me**.\"\n\n\u0026#x200B;\n\n* Because of covid, two million people are dead.\n\n[Latin Wikipedia](https://la.wikipedia.org/wiki/COVID-19) translates COVID-19 as \"morbus coronarii viri anni 2019\" and notes that this is their internal translation only, not found outside Wikipedia. To be safe, I would use COVID-19, but this does not admit cases. I guess in the end I would prefer \"morbus coronarii viri\" which does admit cases.\n\nNote that \"quia\" needs to be followed by a phrase, or put simply, \"quia\" \u003d \"because\" not \"because of\". I would use \"propter\" for \"because of\", or I would just use the ablative.\n\n\"concident\" is the future tense. The perfect tense \"conciderunt\" is more appropriate.\n\nIn conclusion: \"propter morbum coronarii viri, duo milliones homines conciderunt.\" or \"morbo coronarii viri, duo milliones homines conciderunt.\"","sequenceNumber":112,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":1,"nano":89793000}}},{"id":{"value":"t1_gl6k27a"},"submission_id":{"id":"t3_l7carp"},"parent_id":{"id":"t3_l7carp"},"distinguished":false,"created_utc":"21-01-29 03:27:06","author":"jayzwasinnirvana","body":"You\u0027ve almost got it - the verb is right, but consul should be in the nominative. It\u0027s not a direct object, but a subject (nominative? maybe someone can chime in with the grammatical term) complement. Here\u0027s Livy:\n\n\n\u003eDecembri mense summo patrum studio L.\tQuinctius Cincinnatus, pater Caesonis, **consul creatur**","sequenceNumber":114,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":3,"nano":83507000}}},{"id":{"value":"t1_gl6wvd8"},"submission_id":{"id":"t3_l7carp"},"parent_id":{"id":"t1_gl6k27a"},"distinguished":false,"created_utc":"21-01-29 05:03:53","author":"ogorangeduck","body":"It\u0027s an attributive, not a subject.","sequenceNumber":115,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":3,"nano":83662000}}},{"id":{"value":"t1_gl6u4mq"},"submission_id":{"id":"t3_l7gja1"},"parent_id":{"id":"t3_l7gja1"},"distinguished":false,"created_utc":"21-01-29 04:42:50","author":"isolde100","body":"Quis ut Deus refers to St. Michael the Archangel - it means ?who is like God?. It?s the literal translation of the Hebrew Michael or Mika?el.","sequenceNumber":117,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":5,"nano":86997000}}},{"id":{"value":"t1_gl7941t"},"submission_id":{"id":"t3_l7gja1"},"parent_id":{"id":"t1_gl6u4mq"},"distinguished":false,"created_utc":"21-01-29 06:45:24","author":"LurkinOG","body":"You are right..i looked up old latin versions of the I and one is shaped like what i thought what was a 3 but its a capital that looks like this only L only reversed £..what is that quote significant to..i know when translated to todays english its hard to know the meaning or significance without context..and thank you for sharing your knowledge in figuring this out","sequenceNumber":118,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":5,"nano":87152000}}},{"id":{"value":"t1_gl6lb05"},"submission_id":{"id":"t3_l7gja1"},"parent_id":{"id":"t3_l7gja1"},"distinguished":false,"created_utc":"21-01-29 03:36:43","author":"TWFM","body":"Qu3s is apparently an internet personality.","sequenceNumber":119,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":5,"nano":87249000}}},{"id":{"value":"t1_gl6mzss"},"submission_id":{"id":"t3_l7gja1"},"parent_id":{"id":"t1_gl6lb05"},"distinguished":false,"created_utc":"21-01-29 03:49:30","author":"LurkinOG","body":"I thought so to but old latin Ques can be translated to mean seek/ask its where you end up with english words like ques-tion, ques-t..3 could signify both holy trinity and a internet personality","sequenceNumber":120,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":5,"nano":87345000}}},{"id":{"value":"t1_gl88ce7"},"submission_id":{"id":"t3_l7gja1"},"parent_id":{"id":"t3_l7gja1"},"distinguished":false,"created_utc":"21-01-29 13:40:59","author":"BaconJudge","body":"There\u0027s a common Latin scribal abbreviation that resembles a 3, and it can stand in for various letter combinations, such as *-et*. For example, the modern-day abbreviation *viz.* for *videlicet* originated from misinterpreting *vi?* as *viz.* However, it\u0027s normally used at the end of words, and I can\u0027t see what it would represent here, so I mention it mainly to rule it out.","sequenceNumber":121,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":5,"nano":87445000}}},{"id":{"value":"t1_gl1hr7e"},"submission_id":{"id":"t3_l6mdp8"},"parent_id":{"id":"t3_l6mdp8"},"distinguished":false,"created_utc":"21-01-28 04:06:11","author":"antinousrex","body":"feel free to point out errors, please!","sequenceNumber":123,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":7,"nano":177467000}}},{"id":{"value":"t1_gl1tfor"},"submission_id":{"id":"t3_l6mdp8"},"parent_id":{"id":"t1_gl1hr7e"},"distinguished":false,"created_utc":"21-01-28 05:44:51","author":"Thalionwen20","body":"In section 4, it should be \"undecimum,\" accusative. In the last line of section 5, it should be \"et,\" not \"and.\" In 9, you might want to say \"inter se\" instead of \"eorum.\" In 10, it should be \"annulum\" and \"esse\" for indirect speech, or \"annulus delendus\" for a direct quote. In 11, you only need one \"est\" and a \"qui\" before \"nunc.\" In 12, it should be \"mortuos.\" In 15, \"Galadriela\" and \"principe\" are misspelled. In 16, you probably just want \"interficit,\" and I would put another verb such as \"incipit\" with \"iter facere.\"\n\nDespite that, I really enjoyed this and think you did a good job on it! :)","sequenceNumber":124,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":7,"nano":177562000}}},{"id":{"value":"t1_gl2y8de"},"submission_id":{"id":"t3_l6mdp8"},"parent_id":{"id":"t1_gl1tfor"},"distinguished":false,"created_utc":"21-01-28 13:57:47","author":"eglwufdeo","body":"Some more I found: \n\nIn section 1 \n\"ignotum eis\" seems sketchy, I would expect an ablative absolute \n\"eius\" should be \"suae\" \n\nIn section 2 \n\"foedum\" should be \"foedus\", having it as the subject seems a bit weird but I\u0027m not sure \n\"miletes\" should be \"milites\" \nI think the second sentence misses a verb \n\"saeculum\" not \"saecula\" \n\nIn section 3 \n\"pro se\" not \"pro sibi\" \n\nIn section 4 \n\"centesimus\" not \"centisimus\" \ndon\u0027t think \"discedere\" takes an accusative like that \n\"faciat\" not \"faceat\" \n\"suo\" not \"eius\" \n\nIn section 5 \n\"certior\" not \"certiorem\" \nYou need either indirect speech or some subjunction, but as it stands the sentence about Gollum\u0027s torture doesn\u0027t work \n\"aperiens verba\" doesn\u0027t work (match number), should be an ablative absolute \n\"discedat\" not \"discedeat\", also see above \n\nIn section 6 \nsame thing with \"certior\" \nthe relative clause needs a verb \n\nIn Section 7 \n\"Sarumano\" should be \"a Sarumano\" \n\"adiuvantur\" not \"adiuntur\" \n\"a venatore\" not \"venatori\" \n\nIn section 8 \nDon\u0027t know if \"do\" is the best verb here , maybe \"parare\"? \n\nIn section 9 \n\"curatur\" should probably be plural \n\"suum\" not \"eorum\" \n\nIn section 10 \n\"ambo\" doesn\u0027t work \n\nIn section 11 \nI don\u0027t think \"se voluntare\" is a thing \n\"comitatus\" not \"commitatus\" \n\nIn section 12 \n\"quae\" not \"qui\" \n\nIn section 13 \nThink there\u0027s a verb missing after \"vastum antrum\" \n\"cum se\" not \"cum sibi\" \n\nIn section 14 \n\"devastata\" not \"devastatus\" \n\"capturum\" not \"capturus\" \n\nIn section 15 \nI think \"per\" is the wrong preposition \n\nIn section 16 \n\"suos\" not \"eius\" \ndon\u0027t think \"iurandum\" works like that","sequenceNumber":125,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":7,"nano":177599000}}},{"id":{"value":"t1_gl3du83"},"submission_id":{"id":"t3_l6mdp8"},"parent_id":{"id":"t1_gl2y8de"},"distinguished":false,"created_utc":"21-01-28 15:37:05","author":"antinousrex","body":"Thank you so much!","sequenceNumber":126,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":7,"nano":177643000}}},{"id":{"value":"t1_gl3dsb3"},"submission_id":{"id":"t3_l6mdp8"},"parent_id":{"id":"t1_gl1tfor"},"distinguished":false,"created_utc":"21-01-28 15:36:49","author":"antinousrex","body":"Thank you so much! I do these off the seat of my pants and rely on people like you to catch the errors I miss after staring at my own text for 3 hrs","sequenceNumber":127,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":7,"nano":177681000}}},{"id":{"value":"t1_gl27bkf"},"submission_id":{"id":"t3_l6mdp8"},"parent_id":{"id":"t1_gl1hr7e"},"distinguished":false,"created_utc":"21-01-28 08:11:56","author":"Julius_The_Caesar","body":"Happy cake day","sequenceNumber":128,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":7,"nano":177721000}}},{"id":{"value":"t1_gl212je"},"submission_id":{"id":"t3_l6mdp8"},"parent_id":{"id":"t3_l6mdp8"},"distinguished":false,"created_utc":"21-01-28 06:59:30","author":"PinkyPiePerson","body":"You wouldn\u0027t happen to have the link to the Shrek one.\n\nAlso Bee Movie next???","sequenceNumber":129,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":7,"nano":177763000}}},{"id":{"value":"t1_gl3hymr"},"submission_id":{"id":"t3_l6mdp8"},"parent_id":{"id":"t1_gl212je"},"distinguished":false,"created_utc":"21-01-28 15:56:22","author":"antinousrex","body":"here\u0027s shrek. bee movie is in progress [https://docs.google.com/document/d/1-0GY-JqbusyDbOp7aaRJy1q8rY4LSTMQbccTek7673Q/edit?usp\u003dsharing](https://docs.google.com/document/d/1-0GY-JqbusyDbOp7aaRJy1q8rY4LSTMQbccTek7673Q/edit?usp\u003dsharing)","sequenceNumber":130,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":7,"nano":177822000}}},{"id":{"value":"t1_gl2ad9g"},"submission_id":{"id":"t3_l6mdp8"},"parent_id":{"id":"t3_l6mdp8"},"distinguished":false,"created_utc":"21-01-28 08:50:55","author":"-Frind-","body":"Is it classical?","sequenceNumber":131,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":7,"nano":177859000}}},{"id":{"value":"t1_gl2tdy1"},"submission_id":{"id":"t3_l6mdp8"},"parent_id":{"id":"t1_gl2ad9g"},"distinguished":false,"created_utc":"21-01-28 13:07:27","author":"OperaRotas","body":"Of course Lord of the Rings is a classical\n\n_[jocor]_","sequenceNumber":132,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":7,"nano":177893000}}},{"id":{"value":"t1_gl2bz5e"},"submission_id":{"id":"t3_l6mdp8"},"parent_id":{"id":"t3_l6mdp8"},"distinguished":false,"created_utc":"21-01-28 09:12:40","author":"MadScientist2854","body":"happy cake day!","sequenceNumber":133,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":7,"nano":177926000}}},{"id":{"value":"t1_gl42d9n"},"submission_id":{"id":"t3_l6mdp8"},"parent_id":{"id":"t3_l6mdp8"},"distinguished":false,"created_utc":"21-01-28 17:37:36","author":"OperaRotas","body":"\u003eSed potestas Annuli Isildurem corrumpit, qui Annulum pro se capit\n\nThis \"pro se\" sounds a bit weird to me, kinda \"he takes the ring for his own sake\". Maybe \"sibi\" would work better?\n\nI\u0027m also not sure if this \"capit\" means \"takes\", as in right after cutting Sauron\u0027s finger (in which case it\u0027s fine) or \"keeps\", as in \"keeps the ring longer than he should\", in which case \"tenet\" could be better. Any way I don\u0027t know if \"sibi tenet\" works well.","sequenceNumber":134,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":7,"nano":177969000}}},{"id":{"value":"t1_gl2mk6t"},"submission_id":{"id":"t3_l6thod"},"parent_id":{"id":"t3_l6thod"},"distinguished":false,"created_utc":"21-01-28 11:41:58","author":"bandzugfeder","body":"I\u0027ll check with the reference grammars. My guess beforehand (to record my failure for posterity) is that the two genitives would be on either side of the noun,or otherwise separated (eg by a determiner or a case-marked attribute). But I would also guess that it is a comparatively rare occurrence.","sequenceNumber":136,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":9,"nano":96392000}}},{"id":{"value":"t1_gl4dsl6"},"submission_id":{"id":"t3_l6thod"},"parent_id":{"id":"t1_gl2mk6t"},"distinguished":false,"created_utc":"21-01-28 18:38:18","author":"j1bb3r1sh","body":"One example I can recall that may apply here, if I understand the question correctly, is from Sallust?s *Bellum Catilinae* chapter 2.3, ?*Quod si regum atque imperatorum animi virtus in pace ita ut in bello valeret*...? if that would help in your research. \n\nI?m not certain if the situation warrants defining it as its own construction, but it was unique enough that I remembered it six months after reading it. I?d be interested to hear if you are able to find anything else on this. \n\nAlso apologies for formatting if it is messed up, I am on mobile","sequenceNumber":137,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":9,"nano":96530000}}}],"threads":[{"id":{"value":"t3_l5ejhj"},"parent_id":{"value":"t5_2qloa"},"title":"English to Latin translation requests go here!","body":" \n\n1. Ask and answer questions about mottos, tattoos, book titles, lines for your poem, slogans for your bowling club?s t-shirt, etc. in the comments of this thread. **Separate posts for these types of requests will be removed.**\n2. Here are some examples of what types of requests this thread is for: [Example #1](https://www.reddit.com/r/latin/comments/dyqs8p/would_the_correct_translation_of_satans_sister_be/?utm_source\u003dshare\u0026utm_medium\u003dweb2x), [Example #2](https://www.reddit.com/r/latin/comments/dyp18o/translation_from_english/?utm_source\u003dshare\u0026utm_medium\u003dweb2x), [Example #3](https://www.reddit.com/r/latin/comments/dy4o7b/i_need_help_in_translating_correctly_these_2_words/?utm_source\u003dshare\u0026utm_medium\u003dweb2x), [Example #4](https://www.reddit.com/r/latin/comments/dxdzpb/are_there_any_words_that_convey_the_idea_of_a/?utm_source\u003dshare\u0026utm_medium\u003dweb2x), [Example #5](https://www.reddit.com/r/latin/comments/dx5xzc/motto_in_latin/?utm_source\u003dshare\u0026utm_medium\u003dweb2x).\n3. This thread is **not for correcting longer translations and student assignments**. If you have some facility with the Latin language and have made an honest attempt to translate that is **NOT from Google Translate**, Yandex, or any other machine translator, create a separate thread requesting to check and correct your translation: [Separate thread example](https://www.reddit.com/r/latin/comments/dyjz4m/motto_idea_for_motorbike/). Make sure to take a look at Rule 4.\n4. [Previous iterations of this thread](https://www.reddit.com/r/latin/search/?q\u003dLatin%20translation%20requests%20here\u0026restrict_sr\u003d1).","url":"https://www.reddit.com/r/latin/comments/l5ejhj/english_to_latin_translation_requests_go_here/","sub":"latin","author":"NasusSyrae","num_comments":80,"created_utc":"21-01-26 15:00:22","sequenceNumber":82,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":51,"nano":602455000}}},{"id":{"value":"t3_l765yl"},"parent_id":{"value":"t5_2qloa"},"title":"A hymn dedicated to Saint Nicholas, performed by two monks of Holy Cross Monastery.","body":"","url":"https://v.redd.it/4n18nwz2k4e61","sub":"latin","author":"HanSo1oCup","num_comments":8,"created_utc":"21-01-28 20:30:17","sequenceNumber":91,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":53,"nano":161815000}}},{"id":{"value":"t3_l7aez5"},"parent_id":{"value":"t5_2qloa"},"title":"Hodie est Festum S. Thomae de Aquino - Ecce Pagina Illustrata","body":"","url":"https://i.redd.it/nar1o78fc5e61.jpg","sub":"latin","author":"Kingshorsey","num_comments":8,"created_utc":"21-01-28 23:07:28","sequenceNumber":100,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":55,"nano":145355000}}},{"id":{"value":"t3_l7isan"},"parent_id":{"value":"t5_2qloa"},"title":"Verbum Diei, die Jovis, A D V KAL FEB, anni AUC MMDCCLXXIV: excaeco","body":"Verbum diei hodie est:\n\nexcaeco, excaecare, excaecvi, excaecatum: to blind, to make blind\n\n1st conjugation verb\n\n*Frequens curatio est uenas in temporibus adurere, quae fere quidem in eiusmodi malo tument: sed tamen, ut inflentur magisque se ostendant, ceruix ante modice deliganda est, tenuibusque ferramentis et retussis uenae adurendae, donec in oculis pituitae cursus conquiescat. Id enim signum est quasi excaecatorum itinerum, per quae umor ferebatur*\n\nCelsus, *de Medicina*, 7.7","url":"https://www.reddit.com/r/latin/comments/l7isan/verbum_diei_die_jovis_a_d_v_kal_feb_anni_auc/","sub":"latin","author":"Glofkill","num_comments":0,"created_utc":"21-01-29 05:10:12","sequenceNumber":101,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":56,"nano":63285000}}},{"id":{"value":"t3_l7sduu"},"parent_id":{"value":"t5_2qloa"},"title":"I can?t understand this sub sometimes.","body":"I?m feeling lost when it comes to the natural method vs grammar method as laid out on this sub as the best way to learn and understand Latin. I am using LLPSI to learn Latin. Everyone posts about the subjunctive and the perfect and all the other what I believe are grammar rules or modes etc, and I?m over here thinking that none of that is In LLPSI. I feel like there is a whole world of information that I?m not getting because while I know that est is for singular and sunt is for plural, I have no idea what anything else is. I can parse our meaning from reading and context clues, but when or how should I learn to get into the grammar? Right now it?s all story and vocab- I know what I?m reading but I have no idea why it goes its changing in spelling etc. is this normal for reading LLPSI?","url":"https://www.reddit.com/r/latin/comments/l7sduu/i_cant_understand_this_sub_sometimes/","sub":"latin","author":"Squeeks0","num_comments":0,"created_utc":"21-01-29 14:22:11","sequenceNumber":102,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":57,"nano":85717000}}},{"id":{"value":"t3_l6s45e"},"parent_id":{"value":"t5_2qloa"},"title":"If you want to see how a lesson with \"Familia Romana\" works, you can now watch the first lesson on my YouTube channel! ? I can\u0027t recommend that book more to anyone who decides to learn Latin, be it with a teacher or alone.","body":"","url":"https://www.youtube.com/watch?v\u003dwCO_McKXEzA\u0026lc\u003dUgwFAsByoLpr_pnXNqZ4AaABAg.9IykSUZhPwo9J0N2_fqeL2\u0026ab_channel\u003dSaturaLanx","sub":"latin","author":"Irene_SaturaLanx","num_comments":8,"created_utc":"21-01-28 09:50:50","sequenceNumber":111,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":59,"nano":155917000}}},{"id":{"value":"t3_l7iwsm"},"parent_id":{"value":"t5_2qloa"},"title":"I like to do random translations; could someone check these and point out the errors?","body":"Vinum bibere audes ante mi?\n\nYou dare to drink wine in my presence?\n\nQuia coronavirus-morbus, duo milliones homines concident.\n\nBecause of covid, two million people are dead.\n\nI know these will probably be full of errors, can someone check them pwease.","url":"https://www.reddit.com/r/latin/comments/l7iwsm/i_like_to_do_random_translations_could_someone/","sub":"latin","author":"seaweedWorkers","num_comments":1,"created_utc":"21-01-29 05:16:07","sequenceNumber":113,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":1,"nano":90014000}}},{"id":{"value":"t3_l7carp"},"parent_id":{"value":"t5_2qloa"},"title":"Passive translation","body":"How would you say this in latin: The man is elected consul. \n\nIs elected in the passive? but it doesn?t take a direct object\n\nVir consulem creatur?","url":"https://www.reddit.com/r/latin/comments/l7carp/passive_translation/","sub":"latin","author":"SnooDoggos8723","num_comments":2,"created_utc":"21-01-29 00:23:35","sequenceNumber":116,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":3,"nano":83765000}}},{"id":{"value":"t3_l7gja1"},"parent_id":{"value":"t5_2qloa"},"title":"can someone translate. \"Qu3s Ut Deus\" i saw it tattooed on a stranger and made me wonder if 3 was just the way some use 3 in place of E and that was a personal choice or am i missing hidden meaning. Ques Ut Deus, would that not translate roughly to who is god. Am i missing something?","body":"","url":"https://www.reddit.com/r/latin/comments/l7gja1/can_someone_translate_qu3s_ut_deus_i_saw_it/","sub":"latin","author":"LurkinOG","num_comments":5,"created_utc":"21-01-29 03:27:25","sequenceNumber":122,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":5,"nano":87535000}}},{"id":{"value":"t3_l6mdp8"},"parent_id":{"value":"t5_2qloa"},"title":"From the guy who brought you Phineas and Ferb \u0026 Shrek, here\u0027s the story of Lord of the Rings, The Fellowship of the Ring, in Latin!","body":"[https://docs.google.com/document/d/1gZ2lLzlrOuzxNWyNHczDNonVRlMAvwfIT--W3xLMTzk/edit?usp\u003dsharing](https://docs.google.com/document/d/1gZ2lLzlrOuzxNWyNHczDNonVRlMAvwfIT--W3xLMTzk/edit?usp\u003dsharing)","url":"https://www.reddit.com/r/latin/comments/l6mdp8/from_the_guy_who_brought_you_phineas_and_ferb/","sub":"latin","author":"antinousrex","num_comments":12,"created_utc":"21-01-28 04:05:36","sequenceNumber":135,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":7,"nano":178008000}}},{"id":{"value":"t3_l6thod"},"parent_id":{"value":"t5_2qloa"},"title":"Is \"double genitive\" possible in Latin?","body":"There are verbs that take double accusative (e.g. doce?) and there is also double dative (e.g. cu? bon?). These duplicated cases take on different meanings, e.g. in the double dative, [one dative is the dative of purpose and the other dative is the dative of reference](http://dcc.dickinson.edu/grammar/latin/dative-purpose).\n\nIs it possible to have double genitive for a noun that can take on two genitives out of (a) the objective genitive, (b) the partitive genitive, and (c) the genitive of possession?\n\nFor example, odium *barbar?rum* **civilizati?nis**, where *barbar?rum* is the genitive of possession and **civilizati?nis** is the objective genitive.\n\nAre such formations attested?","url":"https://www.reddit.com/r/latin/comments/l6thod/is_double_genitive_possible_in_latin/","sub":"latin","author":"kc_kennylau","num_comments":2,"created_utc":"21-01-28 11:25:02","sequenceNumber":138,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":9,"nano":96574000}}}],"subreddits":[{"name":"latin","title":"The Latin Language","id":{"value":"t5_2qloa"},"description":"This is a community for discussions related to the Latin language.","sequenceNumber":139,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":36,"second":10,"nano":75346000}}}],"top":[{"sequenceNumber":1,"crawlTime":{"date":{"year":2021,"month":1,"day":29},"time":{"hour":14,"minute":35,"second":47,"nano":893685000}}}]} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..cb3868c8 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,4 @@ +rootProject.name = 'wmsa' + +include 'marginalia_nu' +include 'third_party' \ No newline at end of file diff --git a/third_party/README.md b/third_party/README.md new file mode 100644 index 00000000..b72dec53 --- /dev/null +++ b/third_party/README.md @@ -0,0 +1,11 @@ +# Third Party Code + +This is a mix of code from other projects, that has either been aggressively modified to suite the needs of the project, +or lack an artifact. + +## Sources and Licenses +* [RDRPosTagger](https://github.com/datquocnguyen/RDRPOSTagger) - GPL3 +* [PorterStemmer](https://github.com/caarmen/porter-stemmer) - LGPL3 +* [Uppend](https://github.com/upserve/uppend) - MIT +* [OpenZIM](https://github.com/openzim/libzim) - GPL-2.0 +* [XZ for Java](https://tukaani.org/xz/) - Public Domain \ No newline at end of file diff --git a/third_party/build.gradle b/third_party/build.gradle new file mode 100644 index 00000000..2ff91d9f --- /dev/null +++ b/third_party/build.gradle @@ -0,0 +1,111 @@ +plugins { + id 'java' +} + +repositories { + mavenLocal() + maven { url "https://artifactory.cronapp.io/public-release/" } + maven { url "https://repo1.maven.org/maven2/" } + maven { url "https://www2.ph.ed.ac.uk/maven2/" } + maven { url "https://jitpack.io/" } + exclusiveContent { + forRepository { + maven { + url = uri("https://jitpack.io") + } + } + filter { + // Only use JitPack for the `gson-record-type-adapter-factory` library + includeModule("com.github.Marcono1234", "gson-record-type-adapter-factory") + } + } +} + +dependencies { + implementation 'junit:junit:4.13.2' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' + + implementation 'org.projectlombok:lombok:1.18.22' + annotationProcessor 'org.projectlombok:lombok:1.18.22' + + testCompileOnly 'org.projectlombok:lombok:1.18.22' + testImplementation 'org.projectlombok:lombok:1.18.22' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.22' + + implementation 'com.github.jknack:handlebars:4.3.0' + implementation 'com.github.jknack:handlebars-markdown:4.2.1' + + implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.0' + implementation 'io.reactivex.rxjava3:rxjava:3.1.4' + implementation "com.sparkjava:spark-core:2.9.3" + implementation 'com.opencsv:opencsv:5.6' + + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.17.1' + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.17.1' + implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.17.1' + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.17.1' + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.17.1' + implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.17.1' + + implementation 'org.slf4j:slf4j-api:1.7.36' + + implementation 'com.google.guava:guava:31.1-jre' + implementation 'com.google.inject:guice:5.1.0' + implementation 'com.github.jnr:jnr-ffi:2.1.1' + implementation 'org.apache.httpcomponents:httpcore:4.4.15' + implementation 'org.apache.httpcomponents:httpclient:4.5.13' + implementation 'com.github.ThatJavaNerd:JRAW:1.1.0' + + implementation group: 'com.h2database', name: 'h2', version: '2.1.210' + testImplementation group: 'org.mockito', name: 'mockito-core', version: '4.3.1' + + implementation 'org.jsoup:jsoup:1.14.3' + implementation group: 'com.github.crawler-commons', name: 'crawler-commons', version: '1.2' + + implementation 'org.mariadb.jdbc:mariadb-java-client:3.0.3' + implementation group: 'net.sf.trove4j', name: 'trove4j', version: '3.0.3' + + implementation 'com.zaxxer:HikariCP:5.0.1' + + implementation 'org.apache.opennlp:opennlp-tools:1.9.4' + implementation 'io.prometheus:simpleclient:0.15.0' + implementation 'io.prometheus:simpleclient_servlet:0.15.0' + implementation 'io.prometheus:simpleclient_httpserver:0.15.0' + implementation 'io.prometheus:simpleclient_hotspot:0.15.0' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2.1' + implementation 'org.apache.opennlp:opennlp-tools:1.9.4' + implementation 'io.prometheus:simpleclient:0.15.0' + implementation 'io.prometheus:simpleclient_servlet:0.15.0' + implementation 'io.prometheus:simpleclient_httpserver:0.15.0' + implementation 'io.prometheus:simpleclient_hotspot:0.15.0' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2.1' + + implementation group: 'org.yaml', name: 'snakeyaml', version: '1.30' + + implementation 'com.syncthemall:boilerpipe:1.2.2' + implementation 'com.github.luben:zstd-jni:1.5.2-2' + implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.3.0' + implementation 'de.rototor.jeuclid:jeuclid-core:3.1.14' + + implementation 'org.imgscalr:imgscalr-lib:4.2' + implementation 'org.jclarion:image4j:0.7' + + implementation 'commons-net:commons-net:3.6' + implementation 'org.eclipse.jgit:org.eclipse.jgit:5.12.0.202106070339-r' + implementation 'org.eclipse.jgit:org.eclipse.jgit.ssh.jsch:5.12.0.202106070339-r' + implementation 'com.jcraft:jsch:0.1.55' + + implementation group: 'org.apache.commons', name: 'commons-compress', version: '1.21' + implementation 'edu.stanford.nlp:stanford-corenlp:4.4.0' + + implementation group: 'it.unimi.dsi', name: 'fastutil', version: '8.5.8' + implementation 'org.roaringbitmap:RoaringBitmap:[0.6,)' + implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.29' + + implementation 'com.github.Marcono1234:gson-record-type-adapter-factory:0.2.0' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/third_party/src/main/java/ca/rmen/porterstemmer/PorterStemmer.java b/third_party/src/main/java/ca/rmen/porterstemmer/PorterStemmer.java new file mode 100644 index 00000000..bae1fa1a --- /dev/null +++ b/third_party/src/main/java/ca/rmen/porterstemmer/PorterStemmer.java @@ -0,0 +1,373 @@ +/* + * Copyright (c) 2016 Carmen Alvarez + * + * This file is part of Porter Stemmer. + * + * Porter Stemmer is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Porter Stemmer 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Porter Stemmer. If not, see . + */ + +package ca.rmen.porterstemmer; + +import java.util.Locale; + +/** + * This is a simple implementation of the Porter stemming algorithm, defined here: + * http://tartarus.org/martin/PorterStemmer/def.txt + *

      + * This implementation has not been tuned for high performance on large amounts of text. It is + * a simple (naive perhaps) implementation of the rules. + */ +public class PorterStemmer { + + /** + * @param word the word to stem + * @return the stem of the word, in lowercase. + */ + public String stemWord(String word) { + String stem = word.toLowerCase(Locale.getDefault()); + if (stem.length() < 3) return stem; + stem = stemStep1a(stem); + stem = stemStep1b(stem); + stem = stemStep1c(stem); + stem = stemStep2(stem); + stem = stemStep3(stem); + stem = stemStep4(stem); + stem = stemStep5a(stem); + stem = stemStep5b(stem); + return stem; + } + + String stemStep1a(String input) { + // SSES -> SS + if (input.endsWith("sses")) { + return input.substring(0, input.length() - 2); + } + // IES -> I + if (input.endsWith("ies")) { + return input.substring(0, input.length() - 2); + } + // SS -> SS + if (input.endsWith("ss")) { + return input; + } + // S -> + if (input.endsWith("s")) { + return input.substring(0, input.length() - 1); + } + return input; + } + + String stemStep1b(String input) { + // (m>0) EED -> EE + if (input.endsWith("eed")) { + String stem = input.substring(0, input.length() - 1); + String letterTypes = getLetterTypes(stem); + int m = getM(letterTypes); + if (m > 0) return stem; + return input; + } + // (*v*) ED -> + if (input.endsWith("ed")) { + String stem = input.substring(0, input.length() - 2); + String letterTypes = getLetterTypes(stem); + if (letterTypes.contains("v")) { + return step1b2(stem); + } + return input; + } + // (*v*) ING -> + if (input.endsWith("ing")) { + String stem = input.substring(0, input.length() - 3); + String letterTypes = getLetterTypes(stem); + if (letterTypes.contains("v")) { + return step1b2(stem); + } + return input; + } + return input; + } + + private String step1b2(String input) { + // AT -> ATE + if (input.endsWith("at")) { + return input + "e"; + } + // BL -> BLE + else if (input.endsWith("bl")) { + return input + "e"; + } + // IZ -> IZE + else if (input.endsWith("iz")) { + return input + "e"; + } else { + // (*d and not (*L or *S or *Z)) + // -> single letter + char lastDoubleConsonant = getLastDoubleConsonant(input); + if (lastDoubleConsonant != 0 && + lastDoubleConsonant != 'l' + && lastDoubleConsonant != 's' + && lastDoubleConsonant != 'z') { + return input.substring(0, input.length() - 1); + } + // (m=1 and *o) -> E + else { + String letterTypes = getLetterTypes(input); + int m = getM(letterTypes); + if (m == 1 && isStarO(input)) { + return input + "e"; + } + + } + } + return input; + } + + String stemStep1c(String input) { + if (input.endsWith("y")) { + String stem = input.substring(0, input.length() - 1); + String letterTypes = getLetterTypes(stem); + if (letterTypes.contains("v")) return stem + "i"; + } + return input; + } + + String stemStep2(String input) { + String[] s1 = new String[]{ + "ational", + "tional", + "enci", + "anci", + "izer", + "bli", // the published algorithm specifies abli instead of bli. + "alli", + "entli", + "eli", + "ousli", + "ization", + "ation", + "ator", + "alism", + "iveness", + "fulness", + "ousness", + "aliti", + "iviti", + "biliti", + "logi", // the published algorithm doesn't contain this + }; + String[] s2 = new String[]{ + "ate", + "tion", + "ence", + "ance", + "ize", + "ble", // the published algorithm specifies able instead of ble + "al", + "ent", + "e", + "ous", + "ize", + "ate", + "ate", + "al", + "ive", + "ful", + "ous", + "al", + "ive", + "ble", + "log" // the published algorithm doesn't contain this + }; + // (m>0) ATIONAL -> ATE + // (m>0) TIONAL -> TION + for (int i = 0; i < s1.length; i++) { + if (input.endsWith(s1[i])) { + String stem = input.substring(0, input.length() - s1[i].length()); + String letterTypes = getLetterTypes(stem); + int m = getM(letterTypes); + if (m > 0) return stem + s2[i]; + return input; + } + } + return input; + } + + String stemStep3(String input) { + String[] s1 = new String[]{ + "icate", + "ative", + "alize", + "iciti", + "ical", + "ful", + "ness", + }; + String[] s2 = new String[]{ + "ic", + "", + "al", + "ic", + "ic", + "", + "", + }; + // (m>0) ICATE -> IC + // (m>0) ATIVE -> + for (int i = 0; i < s1.length; i++) { + if (input.endsWith(s1[i])) { + String stem = input.substring(0, input.length() - s1[i].length()); + String letterTypes = getLetterTypes(stem); + int m = getM(letterTypes); + if (m > 0) return stem + s2[i]; + return input; + } + } + return input; + + } + + String stemStep4(String input) { + String[] suffixes = new String[]{ + "al", + "ance", + "ence", + "er", + "ic", + "able", + "ible", + "ant", + "ement", + "ment", + "ent", + "ion", + "ou", + "ism", + "ate", + "iti", + "ous", + "ive", + "ize", + }; + // (m>1) AL -> + // (m>1) ANCE -> + for(String suffix : suffixes) { + if (input.endsWith(suffix)) { + String stem = input.substring(0, input.length() - suffix.length()); + String letterTypes = getLetterTypes(stem); + int m = getM(letterTypes); + if (m > 1) { + if (suffix.equals("ion")) { + if (stem.charAt(stem.length() - 1) == 's' || stem.charAt(stem.length() - 1) == 't') { + return stem; + } + } else { + return stem; + } + } + return input; + } + } + return input; + } + + String stemStep5a(String input) { + if (input.endsWith("e")) { + String stem = input.substring(0, input.length() - 1); + String letterTypes = getLetterTypes(stem); + int m = getM(letterTypes); + // (m>1) E -> + if (m > 1) { + return stem; + } + // (m=1 and not *o) E -> + if (m == 1 && !isStarO(stem)) { + return stem; + } + } + return input; + } + + String stemStep5b(String input) { + // (m > 1 and *d and *L) -> single letter + String letterTypes = getLetterTypes(input); + int m = getM(letterTypes); + if (m > 1 && input.endsWith("ll")) { + return input.substring(0, input.length() - 1); + } + return input; + } + + private char getLastDoubleConsonant(String input) { + if (input.length() < 2) return 0; + char lastLetter = input.charAt(input.length() - 1); + char penultimateLetter = input.charAt(input.length() - 2); + if (lastLetter == penultimateLetter && getLetterType((char) 0, lastLetter) == 'c') { + return lastLetter; + } + return 0; + } + + // *o - the stem ends cvc, where the second c is not W, X or Y (e.g. + // -WIL, -HOP) + private boolean isStarO(String input) { + if (input.length() < 3) return false; + + char lastLetter = input.charAt(input.length() - 1); + if (lastLetter == 'w' || lastLetter == 'x' || lastLetter == 'y') return false; + + char secondToLastLetter = input.charAt(input.length() - 2); + char thirdToLastLetter = input.charAt(input.length() - 3); + char fourthToLastLetter = input.length() == 3 ? 0 : input.charAt(input.length() - 4); + return getLetterType(secondToLastLetter, lastLetter) == 'c' + && getLetterType(thirdToLastLetter, secondToLastLetter) == 'v' + && getLetterType(fourthToLastLetter, thirdToLastLetter) == 'c'; + } + + String getLetterTypes(String input) { + StringBuilder letterTypes = new StringBuilder(input.length()); + for (int i = 0; i < input.length(); i++) { + char letter = input.charAt(i); + char previousLetter = i == 0 ? 0 : input.charAt(i - 1); + char letterType = getLetterType(previousLetter, letter); + if (letterTypes.length() == 0 || letterTypes.charAt(letterTypes.length() - 1) != letterType) { + letterTypes.append(letterType); + } + } + return letterTypes.toString(); + } + + int getM(String letterTypes) { + if (letterTypes.length() < 2) return 0; + if (letterTypes.charAt(0) == 'c') return (letterTypes.length() - 1) / 2; + return letterTypes.length() / 2; + } + + private char getLetterType(char previousLetter, char letter) { + switch (letter) { + case 'a': + case 'e': + case 'i': + case 'o': + case 'u': + return 'v'; + case 'y': + if (previousLetter == 0 || getLetterType((char) 0, previousLetter) == 'v') { + return 'c'; + } + return 'v'; + default: + return 'c'; + } + } +} \ No newline at end of file diff --git a/third_party/src/main/java/com/github/datquocnguyen/FWObject.java b/third_party/src/main/java/com/github/datquocnguyen/FWObject.java new file mode 100644 index 00000000..4d89465d --- /dev/null +++ b/third_party/src/main/java/com/github/datquocnguyen/FWObject.java @@ -0,0 +1,39 @@ +package com.github.datquocnguyen; + +import java.util.Arrays; + +/** + * @author DatQuocNguyen + * + */ + +/* + * Define a 5-word/tag window object to capture the context surrounding a word + */ +public class FWObject +{ + public String[] context; + private final static String[] contextPrototype; + static { + contextPrototype = new String[13]; + for (int i = 0; i < 10; i += 2) { + contextPrototype[i] = ""; + contextPrototype[i + 1] = ""; + } + contextPrototype[10] = ""; + contextPrototype[11] = ""; + contextPrototype[12] = ""; + } + public FWObject(boolean check) + { + // Previous2ndWord, Previous2ndTag, PreviousWord, PreviousTag, Word, + // Tag, NextWord, NextTag, Next2ndWord, Next2ndTag, 2-chars suffix, + // 3-char suffix, 4-char suffix + if (check) { + context = Arrays.copyOf(contextPrototype, 13); + } + else { + context = new String[13]; + } + } +} diff --git a/third_party/src/main/java/com/github/datquocnguyen/InitialTagger.java b/third_party/src/main/java/com/github/datquocnguyen/InitialTagger.java new file mode 100644 index 00000000..84819408 --- /dev/null +++ b/third_party/src/main/java/com/github/datquocnguyen/InitialTagger.java @@ -0,0 +1,78 @@ +package com.github.datquocnguyen; + +import java.util.HashMap; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** GPLv3 + * @author DatQuocNguyen + * + */ +public class InitialTagger +{ + private static final Pattern QUOTATION = Pattern.compile("(“)|(”)|(\")"); + + private static final Predicate CD = Pattern.compile("[0-9]+").asPredicate(); + private static final Predicate URL = Pattern.compile("[A-Za-z]\\w*(\\.[A-Za-z]\\w+)+").asPredicate(); + private static final Predicate JJ1 = Pattern.compile("([0-9]+-)|(-[0-9]+)").asPredicate(); + private static final Predicate JJ2 = Pattern.compile("(^[Ii]nter.*)|(^[nN]on.*)|(^[Dd]is.*)|(^[Aa]nti.*)").asPredicate(); + private static final Predicate JJ3 = Pattern.compile("(.*ful$)|(.*ous$)|(.*ble$)|(.*ic$)|(.*ive$)|(.*est$)|(.*able$)|(.*al$)").asPredicate(); + private static final Predicate NN = Pattern.compile("(.*ness$)|(.*ment$)|(.*ship$)|(^[Ee]x-.*)|(^[Ss]elf-.*)").asPredicate(); + private static final Predicate NNS = Pattern.compile(".*s$").asPredicate(); + private static final Predicate VBG = Pattern.compile(".*ing$").asPredicate(); + private static final Predicate VBN = Pattern.compile(".*ed$").asPredicate(); + private static final Predicate RB = Pattern.compile(".*ly$").asPredicate(); + + public static String[] EnInitTagger4Sentence( + HashMap DICT, String[] sentence) + { + String[] wordtags = new String[sentence.length]; + + for (int i = 0; i < sentence.length; i++) { + wordtags[i] = getTagForWordEn(DICT, sentence[i]); + } + return wordtags; + } + + private static String getTagForWordEn(HashMap DICT, String word) { + if (QUOTATION.matcher(word).find()) { + return DICT.get("''"); + } + if ("[]()<>!".contains(word)) { + return "?"; + } + + if (DICT.containsKey(word)) + return DICT.get(word); + String lowerW = word.toLowerCase(); + if (DICT.containsKey(lowerW)) + return DICT.get(lowerW); + if (JJ1.test(word)) + return "JJ"; + if (URL.test(word)) + return "NN"; + if (CD.test(word)) + return "CD"; + if (NN.test(word)) + return "NN"; + if (NNS.test(word) + && Character.isLowerCase(word.charAt(0))) + return "NNS"; + if (Character.isUpperCase(word.charAt(0))) + return "NNP"; + if (JJ2.test(word)) + return "JJ"; + if (VBG.test(word)) + return "VBG"; + if (VBN.test(word)) + return "VBN"; + if (word.contains("-") || JJ3.test(word)) + return "JJ"; + if (RB.test(word)) + return "RB"; + + return "NN"; + } + + +} diff --git a/third_party/src/main/java/com/github/datquocnguyen/Node.java b/third_party/src/main/java/com/github/datquocnguyen/Node.java new file mode 100644 index 00000000..9a0db5c5 --- /dev/null +++ b/third_party/src/main/java/com/github/datquocnguyen/Node.java @@ -0,0 +1,67 @@ +package com.github.datquocnguyen; + +/** + * @author DatQuocNguyen + * + */ + +public class Node +{ + FWObject condition; + String conclusion; + Node exceptNode; + Node ifnotNode; + Node fatherNode; + int depth; + + public Node(FWObject inCondition, String inConclusion, Node inFatherNode, + Node inExceptNode, Node inIfnotNode, int inDepth) + { + this.condition = inCondition; + this.conclusion = inConclusion; + this.fatherNode = inFatherNode; + this.exceptNode = inExceptNode; + this.ifnotNode = inIfnotNode; + this.depth = inDepth; + } + + public void setIfnotNode(Node node) + { + this.ifnotNode = node; + } + + public void setExceptNode(Node node) + { + this.exceptNode = node; + } + + public void setFatherNode(Node node) + { + this.fatherNode = node; + } + + public int countNodes() + { + int count = 1; + if (exceptNode != null) { + count += exceptNode.countNodes(); + } + if (ifnotNode != null) { + count += ifnotNode.countNodes(); + } + return count; + } + + public boolean satisfy(FWObject object) + { + for (int i = 0; i < 13; i++) { + String key = condition.context[i]; + if (key != null) { + if (!key.equals(object.context[i])) { + return false; + } + } + } + return true; + } +} diff --git a/third_party/src/main/java/com/github/datquocnguyen/RDRPOSTagger.java b/third_party/src/main/java/com/github/datquocnguyen/RDRPOSTagger.java new file mode 100644 index 00000000..f2f67fee --- /dev/null +++ b/third_party/src/main/java/com/github/datquocnguyen/RDRPOSTagger.java @@ -0,0 +1,113 @@ +package com.github.datquocnguyen; + +import java.io.*; +import java.nio.file.Path; +import java.util.HashMap; + +/** + * @author DatQuocNguyen + * + */ +public class RDRPOSTagger +{ + private final HashMap FREQDICT; + public final Node root; + + public RDRPOSTagger(Path dictPath, Path rulesFilePath) throws IOException { + this.FREQDICT = Utils.getDictionary(dictPath.toString()); + + BufferedReader buffer = new BufferedReader(new InputStreamReader( + new FileInputStream(rulesFilePath.toFile()), "UTF-8")); + String line = buffer.readLine(); + + this.root = new Node(new FWObject(false), "NN", null, null, null, 0); + + Node currentNode = this.root; + int currentDepth = 0; + + for (; (line = buffer.readLine()) != null;) { + int depth = 0; + for (int i = 0; i <= 6; i++) { // Supposed that the maximum + // exception level is up to 6. + if (line.charAt(i) == '\t') + depth += 1; + else + break; + } + + line = line.trim(); + if (line.length() == 0) + continue; + + if (line.contains("cc:")) + continue; + + FWObject condition = Utils + .getCondition(line.split(" : ")[0].trim()); + String conclusion = Utils.getConcreteValue(line.split(" : ")[1] + .trim()); + + Node node = new Node(condition, conclusion, null, null, null, depth); + + if (depth > currentDepth) { + currentNode.setExceptNode(node); + } + else if (depth == currentDepth) { + currentNode.setIfnotNode(node); + } + else { + while (currentNode.depth != depth) + currentNode = currentNode.fatherNode; + currentNode.setIfnotNode(node); + } + node.setFatherNode(currentNode); + + currentNode = node; + currentDepth = depth; + } + buffer.close(); + } + + public Node findFiredNode(FWObject object) + { + Node currentN = root; + Node firedN = null; + while (true) { + if (currentN.satisfy(object)) { + firedN = currentN; + if (currentN.exceptNode == null) { + break; + } + else { + currentN = currentN.exceptNode; + } + } + else { + if (currentN.ifnotNode == null) { + break; + } + else { + currentN = currentN.ifnotNode; + } + } + + } + + return firedN; + } + + public String[] tagsForEnSentence(String[] sentence) + { + + var initialTags = InitialTagger.EnInitTagger4Sentence(FREQDICT, sentence); + + String[] tags = new String[initialTags.length]; + for (int i = 0; i < initialTags.length; i++) { + FWObject object = Utils.getObject(sentence, initialTags, initialTags.length, i); + tags[i] = findFiredNode(object).conclusion; + } + + return tags; + } + +} diff --git a/third_party/src/main/java/com/github/datquocnguyen/Utils.java b/third_party/src/main/java/com/github/datquocnguyen/Utils.java new file mode 100644 index 00000000..cd157174 --- /dev/null +++ b/third_party/src/main/java/com/github/datquocnguyen/Utils.java @@ -0,0 +1,185 @@ +package com.github.datquocnguyen; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * @author DatQuocNguyen + * + */ +public class Utils +{ + public static List getWordTagList(String initializedSentence) + { + List wordTagList = new ArrayList(); + for (String wordTag : initializedSentence.split("\\s+")) { + wordTag = wordTag.trim(); + if (wordTag.length() == 0) + continue; + + if (wordTag.equals("///")) + wordTagList.add(new WordTag("/", "/")); + else { + int index = wordTag.lastIndexOf("/"); + wordTagList.add(new WordTag(wordTag.substring(0, index), + wordTag.substring(index + 1))); + } + } + return wordTagList; + } + + public static HashMap getDictionary(String dictPath) + { + HashMap dict = new HashMap(); + BufferedReader buffer; + try { + buffer = new BufferedReader(new InputStreamReader( + new FileInputStream(dictPath), "UTF-8")); + for (String line; (line = buffer.readLine()) != null;) { + String[] wordTag = line.split(" "); + dict.put(wordTag[0], wordTag[1]); + } + buffer.close(); + } + catch (FileNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + return dict; + } + + public static boolean isAbbre(String word) + { + for (int i = 0; i < word.length(); i++) { + if (Character.isLowerCase(word.charAt(i)) || word.charAt(i) == '_') + return false; + } + return true; + } + + public static FWObject getCondition(String strCondition) + { + FWObject condition = new FWObject(false); + + for (String rule : strCondition.split(" and ")) { + rule = rule.trim(); + String key = rule.substring(rule.indexOf(".") + 1, + rule.indexOf(" ")); + String value = getConcreteValue(rule); + + if (key.equals("prevWord2")) { + condition.context[4] = value; + } + else if (key.equals("prevTag2")) { + condition.context[5] = value; + } + else if (key.equals("prevWord1")) { + condition.context[2] = value; + } + else if (key.equals("prevTag1")) { + condition.context[3] = value; + } + else if (key.equals("word")) { + condition.context[1] = value; + } + else if (key.equals("tag")) { + condition.context[0] = value; + } + else if (key.equals("nextWord1")) { + condition.context[6] = value; + } + else if (key.equals("nextTag1")) { + condition.context[7] = value; + } + else if (key.equals("nextWord2")) { + condition.context[8] = value; + } + else if (key.equals("nextTag2")) { + condition.context[9] = value; + } + else if (key.equals("suffixL2")) { + condition.context[10] = value; + } + else if (key.equals("suffixL3")) { + condition.context[11] = value; + } + else if (key.equals("suffixL4")) { + condition.context[12] = value; + } + } + + return condition; + } + + public static FWObject getObject(String[] words, String[] tags, int size, int index) + { + FWObject object = new FWObject(true); + + if (index > 1) { + object.context[4] = words[index-2]; + object.context[5] = tags[index-2]; + } + + if (index > 0) { + object.context[2] = words[index-1]; + object.context[3] = tags[index-1]; + } + + String currentWord = words[index]; + String currentTag = tags[index]; + + object.context[1] = currentWord; + object.context[0] = currentTag; + + int numChars = currentWord.length(); + if (numChars >= 4) { + object.context[10] = currentWord.substring(numChars - 2); + object.context[11] = currentWord.substring(numChars - 3); + } + if (numChars >= 5) { + object.context[12] = currentWord.substring(numChars - 4); + } + + if (index < size - 1) { + object.context[6] = words[index+1]; + object.context[7] = tags[index+1]; + } + + if (index < size - 2) { + object.context[8] = words[index+2]; + object.context[9] = tags[index+2]; + } + + return object; + } + + public static String getConcreteValue(String str) + { + if (str.contains("\"\"")) { + if (str.contains("Word")) + return ""; + else if (str.contains("suffixL")) + return ""; + else + return ""; + } + String conclusion = str.substring(str.indexOf("\"") + 1, + str.length() - 1); + return conclusion; + } + + public static void main(String args[]) + { + } +} diff --git a/third_party/src/main/java/com/github/datquocnguyen/WordTag.java b/third_party/src/main/java/com/github/datquocnguyen/WordTag.java new file mode 100644 index 00000000..06ba123f --- /dev/null +++ b/third_party/src/main/java/com/github/datquocnguyen/WordTag.java @@ -0,0 +1,21 @@ +package com.github.datquocnguyen; + +/** + * @author DatQuocNguyen + * + */ +public class WordTag +{ + public String word; + public String tag; + + public WordTag(String iword, String itag) + { + word = iword; + tag = itag; + } + + public String getTag() { + return tag; + } +} diff --git a/third_party/src/main/java/com/upserve/uppend/blobs/NativeIO.java b/third_party/src/main/java/com/upserve/uppend/blobs/NativeIO.java new file mode 100644 index 00000000..80e05c64 --- /dev/null +++ b/third_party/src/main/java/com/upserve/uppend/blobs/NativeIO.java @@ -0,0 +1,75 @@ +package com.upserve.uppend.blobs; + + +import jnr.ffi.*; +import jnr.ffi.types.size_t; +import org.slf4j.Logger; +import com.kenai.jffi.MemoryIO; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.nio.*; + +// https://github.com/upserve/uppend/blob/70967c6f24d7f1a3bbc18799f485d981da93f53b/src/main/java/com/upserve/uppend/blobs/NativeIO.java +// MIT License + +public class NativeIO { + private static final Logger log = org.slf4j.LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private static final NativeC nativeC = LibraryLoader.create(NativeC.class).load("c"); + public static final int pageSize = nativeC.getpagesize(); // 4096 on most Linux + + public enum Advice { + // These seem to be fairly stable https://github.com/torvalds/linux + // TODO add to https://github.com/jnr/jnr-constants + Normal(0), Random(1), Sequential(2), WillNeed(3), DontNeed(4); + private final int value; + Advice(int val) { + this.value = val; + } + } + + public interface NativeC { + int madvise(@size_t long address, @size_t long size, int advice); + int getpagesize(); + } + + static long alignedAddress(long address) { + return address & (- pageSize); + } + + static long alignedSize(long address, int capacity) { + long end = address + capacity; + end = (end + pageSize - 1) & (-pageSize); + return end - alignedAddress(address); + } + + public static void madvise(MappedByteBuffer buffer, Advice advice) throws IOException { + + final long address = MemoryIO.getInstance().getDirectBufferAddress(buffer); + final int capacity = buffer.capacity(); + + long alignedAddress = alignedAddress(address); + long alignedSize = alignedSize(alignedAddress, capacity); + + int val = nativeC.madvise(alignedAddress, alignedSize, advice.value); + + if (val != 0) { + throw new IOException(String.format("System call madvise failed with code: %d", val)); + } + } + + public static void madviseRange(MappedByteBuffer buffer, Advice advice, long offset, int length) throws IOException { + + final long address = MemoryIO.getInstance().getDirectBufferAddress(buffer); + + long alignedAddress = alignedAddress(address+offset); + long alignedSize = alignedSize(alignedAddress, length); + + int val = nativeC.madvise(alignedAddress, alignedSize, advice.value); + + if (val != 0) { + throw new IOException(String.format("System call madvise failed with code: %d", val)); + } + } +} \ No newline at end of file diff --git a/third_party/src/main/java/org/openzim/ZIMTypes/ArticleEntry.java b/third_party/src/main/java/org/openzim/ZIMTypes/ArticleEntry.java new file mode 100644 index 00000000..c39478b0 --- /dev/null +++ b/third_party/src/main/java/org/openzim/ZIMTypes/ArticleEntry.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2011 Arunesh Mathur + * + * This file is a part of zimreader-java. + * + * zimreader-java is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3.0 as + * published by the Free Software Foundation. + * + * zimreader-java 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 Lesser General Public License + * along with zimreader-java. If not, see . + */ + + +package org.openzim.ZIMTypes; + +public class ArticleEntry extends DirectoryEntry { + + int clusterNumber; + + int blobnumber; + + public ArticleEntry(int mimeType, char namespace, int revision, + int clusterNumber, int blobNumber, String url, String title, + int urlListindex) { + + super(mimeType, namespace, revision, url, title, urlListindex); + + this.clusterNumber = clusterNumber; + this.blobnumber = blobNumber; + } + + public int getClusterNumber() { + return clusterNumber; + } + + public int getBlobnumber() { + return blobnumber; + } + +} diff --git a/third_party/src/main/java/org/openzim/ZIMTypes/DirectoryEntry.java b/third_party/src/main/java/org/openzim/ZIMTypes/DirectoryEntry.java new file mode 100644 index 00000000..d2a5eab7 --- /dev/null +++ b/third_party/src/main/java/org/openzim/ZIMTypes/DirectoryEntry.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2011 Arunesh Mathur + * + * This file is a part of zimreader-java. + * + * zimreader-java is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3.0 as + * published by the Free Software Foundation. + * + * zimreader-java 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 Lesser General Public License + * along with zimreader-java. If not, see . + */ + +package org.openzim.ZIMTypes; + +public abstract class DirectoryEntry { + + int mimeType; + + char namespace; + + int revision; + + String url; + + String title; + + int urlListindex; + + public DirectoryEntry(int mimeType, char namespace, int revision, + String url, String title, int index) { + this.mimeType = mimeType; + this.namespace = namespace; + this.revision = revision; + this.url = url; + this.title = title; + this.urlListindex = index; + } + + public int getMimeType() { + return mimeType; + } + + public char getNamespace() { + return namespace; + } + + public int getRevision() { + return revision; + } + + public String getUrl() { + return url; + } + + public String getTitle() { + return title; + } + + public int getUrlListindex() { + return urlListindex; + } + +} diff --git a/third_party/src/main/java/org/openzim/ZIMTypes/RedirectEntry.java b/third_party/src/main/java/org/openzim/ZIMTypes/RedirectEntry.java new file mode 100644 index 00000000..74967041 --- /dev/null +++ b/third_party/src/main/java/org/openzim/ZIMTypes/RedirectEntry.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2011 Arunesh Mathur + * + * This file is a part of zimreader-java. + * + * zimreader-java is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3.0 as + * published by the Free Software Foundation. + * + * zimreader-java 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 Lesser General Public License + * along with zimreader-java. If not, see . + */ + +package org.openzim.ZIMTypes; + +public class RedirectEntry extends DirectoryEntry { + + int redirectIndex; + + public RedirectEntry(int mimeType, char namespace, int revision, + int redirectIndex, String url, String title, int urlListindex) { + + super(mimeType, namespace, revision, url, title, urlListindex); + + this.redirectIndex = redirectIndex; + } + + public int getRedirectIndex() { + return redirectIndex; + } + +} diff --git a/third_party/src/main/java/org/openzim/ZIMTypes/ZIMFile.java b/third_party/src/main/java/org/openzim/ZIMTypes/ZIMFile.java new file mode 100644 index 00000000..ae8632b1 --- /dev/null +++ b/third_party/src/main/java/org/openzim/ZIMTypes/ZIMFile.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2011 Arunesh Mathur + * + * This file is a part of zimreader-java. + * + * zimreader-java is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3.0 as + * published by the Free Software Foundation. + * + * zimreader-java 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 Lesser General Public License + * along with zimreader-java. If not, see . + */ + +package org.openzim.ZIMTypes; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.RandomAccessFile; +import java.util.*; + +import org.openzim.util.RandomAcessFileZIMInputStream; + +/** + * @author Arunesh Mathur + * + * A ZIM file implementation that stores the Header and the MIMETypeList + * + */ +public class ZIMFile extends File { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private Header mHeader; + + private Map mMIMETypeList = new HashMap<>(); // Can be removed if not needed + + public ZIMFile(String path) { + super(path); + + try { + readHeader(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } + + private void readHeader() throws FileNotFoundException { + + // Helpers + int len = 0; + StringBuffer mimeBuffer = null; + + // The byte[] that will help us in reading bytes out of the file + byte[] buffer = new byte[16]; + + // Check whether the file exists + if (!(this.exists())) { + throw new FileNotFoundException( + "The file that you specified was not found."); + } + + // The reader that will be used to read contents from the file + + RandomAcessFileZIMInputStream reader = new RandomAcessFileZIMInputStream( + new RandomAccessFile(this, "r")); + + // The ZIM file header + mHeader = new Header(); + + // Read the contents of the header + try { + mHeader.magicNumber = reader.readFourLittleEndianBytesValue(buffer); + // System.out.println(mHeader.magicNumber); + + mHeader.version = reader.readFourLittleEndianBytesValue(buffer); + // System.out.println(mHeader.version); + + mHeader.uuid = reader.readSixteenLittleEndianBytesValue(buffer); + // System.out.println(mHeader.uuid); reader.read(buffer, 0, 4); + + mHeader.articleCount = reader + .readFourLittleEndianBytesValue(buffer); + // System.out.println(mHeader.articleCount); + + mHeader.clusterCount = reader + .readFourLittleEndianBytesValue(buffer); + // System.out.println(mHeader.clusterCount); + + mHeader.urlPtrPos = reader.readEightLittleEndianBytesValue(buffer); + // System.out.println(mHeader.urlPtrPos); + + mHeader.titlePtrPos = reader + .readEightLittleEndianBytesValue(buffer); + // System.out.println(mHeader.titlePtrPos); + + mHeader.clusterPtrPos = reader + .readEightLittleEndianBytesValue(buffer); + // System.out.println(mHeader.clusterPtrPos); + + mHeader.mimeListPos = reader + .readEightLittleEndianBytesValue(buffer); + // System.out.println(mHeader.mimeListPos); + + mHeader.mainPage = reader.readFourLittleEndianBytesValue(buffer); + // System.out.println(mHeader.mainPage); + + mHeader.layoutPage = reader.readFourLittleEndianBytesValue(buffer); + // System.out.println(mHeader.layoutPage); + + reader.seek(mHeader.mimeListPos); + // Initialise the MIMETypeList + while (true) { + reader.read(buffer, 0, 1); + len = 0; + mimeBuffer = new StringBuffer(); + while (buffer[0] != '\0') { + mimeBuffer.append((char) buffer[0]); + reader.read(buffer, 0, 1); + len++; + } + if (len == 0) { + break; + } + mMIMETypeList.put(mMIMETypeList.size(), mimeBuffer.toString()); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + public int getVersion() { + return mHeader.version; + } + + public int getUuid() { + return mHeader.uuid; + } + + public int getArticleCount() { + return mHeader.articleCount; + } + + public int getClusterCount() { + return mHeader.clusterCount; + } + + public long getUrlPtrPos() { + return mHeader.urlPtrPos; + } + + public long getTitlePtrPos() { + return mHeader.titlePtrPos; + } + + public long getClusterPtrPos() { + return mHeader.clusterPtrPos; + } + + public String getMIMEType(int mimeNumber) { + return mMIMETypeList.get(mimeNumber); + } + public Map getMIMETypes() { + return Collections.unmodifiableMap(mMIMETypeList); + } + + public long getHeaderSize() { + return mHeader.mimeListPos; + } + + public int getMainPage() { + return mHeader.mainPage; + } + + public int getLayoutPage() { + return mHeader.layoutPage; + } + + public class Header { + int magicNumber; + int version; + int uuid; + int articleCount; + int clusterCount; + long urlPtrPos; + long titlePtrPos; + long clusterPtrPos; + long mimeListPos; + int mainPage; + int layoutPage; + } + +} diff --git a/third_party/src/main/java/org/openzim/ZIMTypes/ZIMReader.java b/third_party/src/main/java/org/openzim/ZIMTypes/ZIMReader.java new file mode 100644 index 00000000..e4cd83ee --- /dev/null +++ b/third_party/src/main/java/org/openzim/ZIMTypes/ZIMReader.java @@ -0,0 +1,677 @@ +/* + * Copyright (C) 2011 Arunesh Mathur + * + * This file is a part of zimreader-java. + * + * zimreader-java is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3.0 as + * published by the Free Software Foundation. + * + * zimreader-java 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 Lesser General Public License + * along with zimreader-java. If not, see . + */ + +package org.openzim.ZIMTypes; + +import java.io.*; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import com.github.luben.zstd.RecyclingBufferPool; +import com.github.luben.zstd.ZstdInputStream; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import org.tukaani.xz.SingleXZInputStream; +import org.openzim.util.RandomAcessFileZIMInputStream; +import org.openzim.util.Utilities; + +/** + * @author Arunesh Mathur + * + * A ZIMReader that reads data from the ZIMFile + * + */ +public class ZIMReader { + + private ZIMFile mFile; + private RandomAcessFileZIMInputStream mReader; + private int targetMime; + + public ZIMReader(ZIMFile file) { + this.mFile = file; + targetMime = file.getMIMETypes().entrySet().stream().filter(e -> "text/html".equals(e.getValue())).map(Map.Entry::getKey).findFirst().orElseThrow(); + try { + mReader = new RandomAcessFileZIMInputStream(new RandomAccessFile( + mFile, "r")); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } + + public List getURLListByURL() throws IOException { + + long i = 0, pos, mimeType; + + byte[] buffer = new byte[8]; + + // The list that will eventually return the list of URL's + ArrayList returnList = new ArrayList(); + + // Move to the spot where URL's are listed + mReader.seek(mFile.getUrlPtrPos()); + + for (i = 0; i < mFile.getArticleCount(); i++) { + + // The position of URL i + pos = mReader.readEightLittleEndianBytesValue(buffer); + + // Mark the current position that we need to return to + mReader.mark(); + + // Move to the position of URL i + mReader.seek(pos); + + // Article or Redirect entry? + mimeType = mReader.readTwoLittleEndianBytesValue(buffer); + + if (mimeType == 65535) { + mReader.seek(pos + 12); + returnList.add(mReader.readString()); + } else { + mReader.seek(pos + 16); + returnList.add(mReader.readString()); + } + + mReader.reset(); + } + + return returnList; + } + + public List getURLListByTitle() throws IOException { + + long i = 0, pos, mimeType, articleNumber, urlPtrPos; + + byte[] buffer = new byte[8]; + + // The list that will eventually return the list of URL's + ArrayList returnList = new ArrayList(); + + // Get the UrlPtrPos or one time storage + urlPtrPos = mFile.getUrlPtrPos(); + + // Move to the spot where URL's are listed + mReader.seek(mFile.getTitlePtrPos()); + + for (i = 0; i < mFile.getArticleCount(); i++) { + + // The articleNumber of the position of URL i + articleNumber = mReader.readFourLittleEndianBytesValue(buffer); + + // Mark the current position that we need to return to + mReader.mark(); + + mReader.seek(urlPtrPos + (8 * (articleNumber))); + + // The position of URL i + pos = mReader.readEightLittleEndianBytesValue(buffer); + mReader.seek(pos); + + // Article or Redirect entry? + mimeType = mReader.readTwoLittleEndianBytesValue(buffer); + + if (mimeType == 65535) { + mReader.seek(pos + 12); + String url = mReader.readString(); + returnList.add(url); + } else { + mReader.seek(pos + 16); + String url = mReader.readString(); + returnList.add(url); + } + + // Return to the marked position + mReader.reset(); + } + + return returnList; + } + + // Gives the minimum required information needed for the given articleName + public DirectoryEntry getDirectoryInfo(String articleName, char namespace) + throws IOException { + + DirectoryEntry entry; + String cmpStr; + long numberOfArticles = mFile.getArticleCount(); + long beg = mFile.getTitlePtrPos(), end = beg + (numberOfArticles * 4), mid; + + articleName = namespace + "/" + articleName; + + System.out.print((end - beg)/4 + " entries"); + + while (beg <= end) { + mid = beg + 4 * (((end - beg) / 4) / 2); + entry = getDirectoryInfoAtTitlePosition(mid); + if (entry == null) { + return null; + } + cmpStr = entry.getNamespace() + "/" + entry.getUrl(); + if (articleName.compareTo(cmpStr) < 0) { + end = mid - 4; + + } else if (articleName.compareTo(cmpStr) > 0) { + beg = mid + 4; + + } else { + return entry; + } + } + + return null; + + } + + public DirectoryEntry enumerateArticles(PrintWriter writer) + throws IOException { + + int numberOfArticles = mFile.getArticleCount(); + long beg = mFile.getTitlePtrPos(); + long end = beg + (numberOfArticles * 4L); + + System.out.println(numberOfArticles); + long start = System.currentTimeMillis(); + + for (long i = beg; i < end; i+=4) { + var entry = getDirectoryInfoAtTitlePosition(i); + + if (entry.mimeType == targetMime && entry instanceof ArticleEntry) { + ArticleEntry ae = (ArticleEntry) entry; + writer.printf("%d\t%d\t%s\n", ae.clusterNumber, ae.getBlobnumber(), ae.url); + } + } + + return null; + + } + + + @Getter @AllArgsConstructor + static class DataKey implements Comparable { + public final long cluster; + public final long blob; + + @Override + public int compareTo(@NotNull DataKey o) { + if (o.cluster != cluster) { + return (int)(cluster - o.cluster); + } + return (int)(blob - o.blob); + } + } + + // Gives the minimum required information needed for the given articleName + public DirectoryEntry forEachArticles(BiConsumer consumer, Predicate blobPred) + throws IOException { + + int numberOfArticles = mFile.getArticleCount(); + long beg = mFile.getTitlePtrPos(); + long end = beg + (numberOfArticles * 4L); + + System.out.println(numberOfArticles); + long start = System.currentTimeMillis(); + + Map> data = new TreeMap<>(); + + System.out.println("Indexing"); + + for (long i = beg; i < end; i+=4) { + var entry = getDirectoryInfoAtTitlePosition(i); + + if (((i-beg)%100_000) == 0) { + System.out.printf("%f%%\n", ((i-beg) * 100.) / (end-beg)); + } + + if (entry.mimeType == targetMime && entry instanceof ArticleEntry) { + ArticleEntry ae = (ArticleEntry) entry; + data.computeIfAbsent(ae.clusterNumber, (cn) -> new HashMap<>()).put(ae.blobnumber, ae.url); + } + } + + System.out.println("Iterating over " + data.keySet().stream().mapToInt(Integer::intValue).max() + "clusters"); + + data.forEach((pos,blobs) -> { + if (!blobPred.test(pos)) { + return; + } + + try { + getArticleData(consumer, pos, blobs); + } + catch (IOException ex) { + + } + }); + + return null; + + } + + + + // Gives the minimum required information needed for the given articleName + public DirectoryEntry forEachTitles(Consumer aeConsumer, Consumer reConsumer) + throws IOException { + + int numberOfArticles = mFile.getArticleCount(); + long beg = mFile.getTitlePtrPos(); + long end = beg + (numberOfArticles * 4L); + + System.err.println(numberOfArticles); + long start = System.currentTimeMillis(); + + Map> data = new TreeMap<>(); + + System.err.println("Indexing"); + + for (long i = beg; i < end; i+=4) { + var entry = getDirectoryInfoAtTitlePosition(i); + + if (((i-beg)%100_000) == 0) { + System.err.printf("%f%%\n", ((i-beg) * 100.) / (end-beg)); + } + + if (entry.mimeType == targetMime && entry instanceof ArticleEntry) { + aeConsumer.accept((ArticleEntry) entry); + } + else if (entry.mimeType == 65535 && entry instanceof RedirectEntry) { + + reConsumer.accept((RedirectEntry) entry); + + } + + } + + return null; + + } + + public String getArticleData(BiConsumer consumer, int clusterNumber, Map blobToUrl) throws IOException { + + byte[] buffer = new byte[8]; + + // Cast to ArticleEntry + + // Get the cluster and blob numbers from the article + + // Move to the cluster entry in the clusterPtrPos + mReader.seek(mFile.getClusterPtrPos() + clusterNumber * 8L); + + // Read the location of the cluster + long clusterPos = mReader + .readEightLittleEndianBytesValue(buffer); + + // Move to the cluster + mReader.seek(clusterPos); + + // Read the first byte, for compression information + int compressionType = mReader.read(); + + InputStream is; + InputStream cis; + switch (compressionType) { + case 0: + case 1: + is = mReader; + cis = null; + break; + case 4: + cis = is = new SingleXZInputStream(mReader, 4194304); + break; + case 5: + cis = is = new ZstdInputStream(new BufferedInputStream(mReader, 65535), RecyclingBufferPool.INSTANCE); + break; + default: + throw new IllegalArgumentException(); + } + + try { + buffer = new byte[4]; + is.read(buffer); + var firstOffset = Utilities.toFourLittleEndianInteger(buffer); + int numberOfBlobs = firstOffset / 4; + + long offsets[] = new long[numberOfBlobs]; + offsets[0] = firstOffset; + + buffer = new byte[4*(numberOfBlobs-1)]; + int rb = 0, trb = 0; + while (trb < buffer.length) { + rb = is.read(buffer, trb, buffer.length - trb); + trb += rb; + } + + for (int blobNumber = 0; blobNumber < numberOfBlobs-1; blobNumber++) { + offsets[blobNumber+1] = ((buffer[4*blobNumber] & 0xFF) | ((buffer[4*blobNumber+1] & 0xFF) << 8) + | ((buffer[4*blobNumber+2] & 0xFF) << 16) | ((buffer[4*blobNumber+3] & 0xFF) << 24)); + } + + int minRelBlob = blobToUrl.keySet().stream().mapToInt(Integer::intValue).min().orElse(0); + int maxRelBlob = blobToUrl.keySet().stream().mapToInt(Integer::intValue).max().orElse(0); + + if (minRelBlob > 0) { + Utilities.skipFully(is, offsets[minRelBlob] - offsets[0]); + } + + for (int blobNumber = minRelBlob; blobNumber < maxRelBlob; blobNumber++) { + int differenceOffset = (int)(offsets[blobNumber+1] - offsets[blobNumber]); + + if (!blobToUrl.containsKey(blobNumber)) { + Utilities.skipFully(is, differenceOffset); + } + else { + byte[] data = new byte[differenceOffset]; + trb = rb = 0; + while (trb < data.length) { + rb = is.read(data, trb, data.length - trb); + trb += rb; + } + consumer.accept(blobToUrl.get(blobNumber), new String(data)); + } + } + System.out.println(clusterNumber + " " + blobToUrl.size()); + + } + finally { + if (null != cis) { + cis.close(); + } + } + + + return null; + + } + + public String getArticleData(DirectoryEntry mainEntry) throws IOException { + + byte[] buffer = new byte[8]; + + if (mainEntry != null) { + + // Check what kind of an entry was mainEnrty + if (mainEntry.getClass() == ArticleEntry.class) { + + // Cast to ArticleEntry + ArticleEntry article = (ArticleEntry) mainEntry; + + // Get the cluster and blob numbers from the article + long clusterNumber = article.getClusterNumber(); + int blobNumber = article.getBlobnumber(); + + // Move to the cluster entry in the clusterPtrPos + mReader.seek(mFile.getClusterPtrPos() + clusterNumber * 8); + + // Read the location of the cluster + long clusterPos = mReader + .readEightLittleEndianBytesValue(buffer); + + // Move to the cluster + mReader.seek(clusterPos); + + // Read the first byte, for compression information + int compressionType = mReader.read(); + + // Reference declaration + SingleXZInputStream xzReader = null; + int firstOffset, numberOfBlobs, offset1, + offset2, + location, + differenceOffset; + + ByteArrayOutputStream baos; + + // Check the compression type that was read + switch (compressionType) { + + // TODO: Read uncompressed data directly + case 0: + case 1: + + // Read the first 4 bytes to find out the number of artciles + buffer = new byte[4]; + + // Create a dictionary with size 40MiB, the zimlib uses this + // size while creating + + // Read the first offset + mReader.read(buffer); + + // The first four bytes are the offset of the zeroth blob + firstOffset = Utilities + .toFourLittleEndianInteger(buffer); + + // The number of blobs + numberOfBlobs = firstOffset / 4; + + // The blobNumber has to be lesser than the numberOfBlobs + assert blobNumber < numberOfBlobs; + + + if (blobNumber == 0) { + // The first offset is what we read earlier + offset1 = firstOffset; + } else { + + location = (blobNumber - 1) * 4; + Utilities.skipFully(mReader, location); + mReader.read(buffer); + offset1 = Utilities.toFourLittleEndianInteger(buffer); + } + + mReader.read(buffer); + offset2 = Utilities.toFourLittleEndianInteger(buffer); + + differenceOffset = offset2 - offset1; + buffer = new byte[differenceOffset]; + + Utilities.skipFully(mReader, + (offset1 - 4 * (blobNumber + 2))); + + mReader.read(buffer, 0, differenceOffset); + + return new String(buffer); + + // LZMA2 compressed data + case 4: + + // Read the first 4 bytes to find out the number of artciles + buffer = new byte[4]; + + // Create a dictionary with size 40MiB, the zimlib uses this + // size while creating + xzReader = new SingleXZInputStream(mReader, 4194304); + + // Read the first offset + xzReader.read(buffer); + + // The first four bytes are the offset of the zeroth blob + firstOffset = Utilities + .toFourLittleEndianInteger(buffer); + + // The number of blobs + numberOfBlobs = firstOffset / 4; + + // The blobNumber has to be lesser than the numberOfBlobs + assert blobNumber < numberOfBlobs; + + if(blobNumber == 0) { + // The first offset is what we read earlier + offset1 = firstOffset; + } else { + + location = (blobNumber - 1) * 4; + Utilities.skipFully(xzReader, location); + xzReader.read(buffer); + offset1 = Utilities.toFourLittleEndianInteger(buffer); + } + + xzReader.read(buffer); + offset2 = Utilities.toFourLittleEndianInteger(buffer); + + differenceOffset = offset2 - offset1; + buffer = new byte[differenceOffset]; + + Utilities.skipFully(xzReader, + (offset1 - 4 * (blobNumber + 2))); + + xzReader.read(buffer, 0, differenceOffset); + return new String(buffer); + + case 5: + // Read the first 4 bytes to find out the number of artciles + buffer = new byte[4]; + + // Create a dictionary with size 40MiB, the zimlib uses this + // size while creating + var zstdInputStream = new com.github.luben.zstd.ZstdInputStream(new BufferedInputStream(mReader)); + + // Read the first offset + zstdInputStream.read(buffer); + + // The first four bytes are the offset of the zeroth blob + firstOffset = Utilities + .toFourLittleEndianInteger(buffer); + + // The number of blobs + numberOfBlobs = firstOffset / 4; + + // The blobNumber has to be lesser than the numberOfBlobs + assert blobNumber < numberOfBlobs; + + if(blobNumber == 0) { + // The first offset is what we read earlier + offset1 = firstOffset; + } else { + + location = (blobNumber - 1) * 4; + Utilities.skipFully(zstdInputStream, location); + zstdInputStream.read(buffer); + offset1 = Utilities.toFourLittleEndianInteger(buffer); + } + + zstdInputStream.read(buffer); + offset2 = Utilities.toFourLittleEndianInteger(buffer); + + differenceOffset = offset2 - offset1; + buffer = new byte[differenceOffset]; + + Utilities.skipFully(zstdInputStream, + (offset1 - 4 * (blobNumber + 2))); + + zstdInputStream.read(buffer, 0, differenceOffset); + + return new String(buffer); + + default: + System.err.print("What is compression = " + compressionType); + + } + + } + } + + return null; + + } + + public DirectoryEntry getDirectoryInfoAtTitlePosition(long position) + throws IOException { + + // Helpers + long pos; + byte[] buffer = new byte[8]; + + // At the appropriate position in the titlePtrPos + mReader.seek(position); + + // Get value of article at index + pos = mReader.readFourLittleEndianBytesValue(buffer); + + // Move to the position in urlPtrPos + mReader.seek(mFile.getUrlPtrPos() + 8L * pos); + + // Get value of article in urlPtrPos + pos = mReader.readEightLittleEndianBytesValue(buffer); + + // Go to the location of the directory entry + mReader.seek(pos); + + int type = mReader.readTwoLittleEndianBytesValue(buffer); + + // Ignore the parameter length + mReader.read(); + + char namespace = (char) mReader.read(); + // System.out.println("Namepsace: " + namespace); + + int revision = mReader.readFourLittleEndianBytesValue(buffer); + // System.out.println("Revision: " + revision); + + // TODO: Remove redundant if condition code + // Article or Redirect entry + if (type == 65535) { + + // System.out.println("MIMEType: " + type); + + int redirectIndex = mReader.readFourLittleEndianBytesValue(buffer); + // System.out.println("RedirectIndex: " + redirectIndex); + + String url = mReader.readString(); + // System.out.println("URL: " + url); + + String title = mReader.readString(); + title = title.equals("") ? url : title; + // System.out.println("Title: " + title); + + return new RedirectEntry(type, namespace, revision, redirectIndex, + url, title, (int)(position - mFile.getUrlPtrPos()) / 8); + + } else { + + // System.out.println("MIMEType: " + mFile.getMIMEType(type)); + + int clusterNumber = mReader.readFourLittleEndianBytesValue(buffer); + // System.out.println("Cluster Number: " + clusterNumber); + + int blobNumber = mReader.readFourLittleEndianBytesValue(buffer); + // System.out.println("Blob Number: " + blobNumber); + + String url = mReader.readString(); + // System.out.println("URL: " + url); + + String title = mReader.readString(); + title = title.equals("") ? url : title; + // System.out.println("Title: " + title); + + // Parameter data ignored + + return new ArticleEntry(type, namespace, revision, clusterNumber, + blobNumber, url, title, + (int)(position - mFile.getUrlPtrPos()) / 8); + } + + } + + public ZIMFile getZIMFile() { + return mFile; + } +} diff --git a/third_party/src/main/java/org/openzim/util/RandomAcessFileZIMInputStream.java b/third_party/src/main/java/org/openzim/util/RandomAcessFileZIMInputStream.java new file mode 100644 index 00000000..ce62f156 --- /dev/null +++ b/third_party/src/main/java/org/openzim/util/RandomAcessFileZIMInputStream.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2011 Arunesh Mathur + * + * This file is a part of zimreader-java. + * + * zimreader-java is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3.0 as + * published by the Free Software Foundation. + * + * zimreader-java 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 Lesser General Public License + * along with zimreader-java. If not, see . + */ + +package org.openzim.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; + +/** + * This is an implementation of RandomAccessFile to ensure that it is an + * InputStream as well, specifically designed for reading a ZIM file. Ad-Hoc + * implementation, can be improved. + * + * @author Arunesh Mathur + */ + +public class RandomAcessFileZIMInputStream extends InputStream { + + private RandomAccessFile mRAFReader; + + private long mMarked = -1; + + public RandomAcessFileZIMInputStream(RandomAccessFile reader) { + this.mRAFReader = reader; + } + + // TODO: Remove the parameter buffer + public int readTwoLittleEndianBytesValue(byte[] buffer) throws IOException { + if (buffer.length < 2) { + throw new OutOfMemoryError("buffer too small"); + } else { + mRAFReader.read(buffer, 0, 2); + return Utilities.toTwoLittleEndianInteger(buffer); + } + } + + // TODO: Remove the parameter buffer + public int readFourLittleEndianBytesValue(byte[] buffer) throws IOException { + if (buffer.length < 4) { + throw new OutOfMemoryError("buffer too small"); + } else { + mRAFReader.read(buffer, 0, 4); + return Utilities.toFourLittleEndianInteger(buffer); + } + } + + // TODO: Remove the parameter buffer + public long readEightLittleEndianBytesValue(byte[] buffer) + throws IOException { + if (buffer.length < 8) { + throw new OutOfMemoryError("buffer too small"); + } else { + mRAFReader.read(buffer, 0, 8); + return Utilities.toEightLittleEndianInteger(buffer); + } + } + + // TODO: Remove the parameter buffer + public int readSixteenLittleEndianBytesValue(byte[] buffer) + throws IOException { + if (buffer.length < 16) { + throw new OutOfMemoryError("buffer too small"); + } else { + mRAFReader.read(buffer, 0, 16); + return Utilities.toSixteenLittleEndianInteger(buffer); + } + } + + // Reads characters from the current position into a String and stops when a + // '\0' is encountered + public String readString() throws IOException { + StringBuilder sb = new StringBuilder(); + /* + * int i; byte[] buffer = new byte[100]; while (true) { + * mRAFReader.read(buffer); for (i = 0; i < buffer.length; i++) { if + * (buffer[i] == '\0') { break; } sb.append((char) buffer[i]); } if (i + * != buffer.length) break; } return sb.toString(); + */ + byte[] buffer = new byte[64]; + for (;;) { + mRAFReader.readFully(buffer); + for (int i = 0; i < buffer.length; i++) { + if (buffer[i] == 0) { + sb.append(new String(buffer, 0, i)); + mRAFReader.seek(mRAFReader.getFilePointer() + 1 + (i - buffer.length)); + return sb.toString(); + } + } + sb.append(new String(buffer)); + } + + } + + @Override + public int read() throws IOException { + return mRAFReader.read(); + } + + @Override + public int read(byte b[], int off, int len) throws IOException { + return mRAFReader.read(b, off, len); + } + + + public RandomAccessFile getRandomAccessFile() { + return mRAFReader; + } + + public void seek(long pos) throws IOException { + if (pos < 0) { + System.out.println(pos); + } + mRAFReader.seek(pos); + } + + public long getFilePointer() throws IOException { + return mRAFReader.getFilePointer(); + } + + public void mark() throws IOException { + this.mMarked = mRAFReader.getFilePointer(); + } + + public void reset() throws IOException { + if (this.mMarked == -1) { + return; + } else { + mRAFReader.seek(mMarked); + this.mMarked = -1; + } + } +} diff --git a/third_party/src/main/java/org/openzim/util/Utilities.java b/third_party/src/main/java/org/openzim/util/Utilities.java new file mode 100644 index 00000000..25a262d3 --- /dev/null +++ b/third_party/src/main/java/org/openzim/util/Utilities.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2011 Arunesh Mathur + * + * This file is a part of zimreader-java. + * + * zimreader-java is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3.0 as + * published by the Free Software Foundation. + * + * zimreader-java 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 Lesser General Public License + * along with zimreader-java. If not, see . + */ + + +package org.openzim.util; + +import java.io.IOException; +import java.io.InputStream; + +public class Utilities { + + // TODO: Write a binary search algorithm + public static int binarySearch() { + return -1; + } + + public static int toTwoLittleEndianInteger(byte[] buffer) throws IOException { + if (buffer.length < 2) { + throw new OutOfMemoryError("buffer too small"); + } else { + int result = ((buffer[0] & 0xFF) | ((buffer[1] & 0xFF) << 8)); + return result; + } + } + + public static int toFourLittleEndianInteger(byte[] buffer) throws IOException { + if (buffer.length < 4) { + throw new OutOfMemoryError("buffer too small"); + } else { + int result = ((buffer[0] & 0xFF) | ((buffer[1] & 0xFF) << 8) + | ((buffer[2] & 0xFF) << 16) | ((buffer[3] & 0xFF) << 24)); + return result; + } + } + + public static long toEightLittleEndianInteger(byte[] buffer) throws IOException { + if (buffer.length < 8) { + throw new OutOfMemoryError("buffer too small"); + } else { + long result = ((buffer[0] & 0xFF) | ((buffer[1] & 0xFF) << 8) + | ((buffer[2] & 0xFF) << 16) | ((long) (buffer[3] & 0xFF) << 24L) + | ((long) (buffer[4] & 0xFF) << 32L) | ((long) (buffer[5] & 0xFF) << 40L) + | ((long) (buffer[6] & 0xFF) << 48L) | ((long) (buffer[7] & 0xFF) << 56L)); + return result; + } + } + + public static int toSixteenLittleEndianInteger(byte[] buffer) throws IOException { + if (buffer.length < 16) { + throw new OutOfMemoryError("buffer too small"); + } else { + int result = ((buffer[0] & 0xFF) | ((buffer[1] & 0xFF) << 8) + | ((buffer[2] & 0xFF) << 16) | ((buffer[3] & 0xFF) << 24) + | ((buffer[4] & 0xFF) << 32) | ((buffer[5] & 0xFF) << 40) + | ((buffer[6] & 0xFF) << 48) | ((buffer[7] & 0xFF) << 56) + | ((buffer[8] & 0xFF) << 64) | ((buffer[9] & 0xFF) << 72) + | ((buffer[10] & 0xFF) << 80) | ((buffer[11] & 0xFF) << 88) + | ((buffer[12] & 0xFF) << 96) + | ((buffer[13] & 0xFF) << 104) + | ((buffer[14] & 0xFF) << 112) | ((buffer[15] & 0xFF) << 120)); + return result; + } + } + + public static void skipFully(InputStream stream, long bytes) throws IOException { + for (long i = stream.skip(bytes); i < bytes; i += stream.skip(bytes - i)); + } + +} diff --git a/third_party/src/main/java/org/tukaani/xz/BlockInputStream.java b/third_party/src/main/java/org/tukaani/xz/BlockInputStream.java new file mode 100644 index 00000000..d50fbc78 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/BlockInputStream.java @@ -0,0 +1,212 @@ +/* + * BlockInputStream + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +import java.io.InputStream; +import java.io.DataInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Arrays; +import org.tukaani.xz.common.DecoderUtil; +import org.tukaani.xz.check.Check; + +class BlockInputStream extends InputStream { + private final InputStream in; + private final DataInputStream inData; + private final CountingInputStream inCounted; + private InputStream filterChain; + private final Check check; + + private long uncompressedSizeInHeader = -1; + private long compressedSizeInHeader = -1; + private long compressedSizeLimit; + private int headerSize; + private long uncompressedSize = 0; + + public BlockInputStream(InputStream in, Check check, int memoryLimit) + throws IOException, IndexIndicatorException { + this.in = in; + this.check = check; + inData = new DataInputStream(in); + + byte[] buf = new byte[DecoderUtil.BLOCK_HEADER_SIZE_MAX]; + + // Block Header Size or Index Indicator + inData.readFully(buf, 0, 1); + + // See if this begins the Index field. + if (buf[0] == 0x00) + throw new IndexIndicatorException(); + + // Read the rest of the Block Header. + headerSize = 4 * (buf[0] + 1); + inData.readFully(buf, 1, headerSize - 1); + + // Validate the CRC32. + if (!DecoderUtil.isCRC32Valid(buf, 0, headerSize - 4, headerSize - 4)) + throw new CorruptedInputException("XZ Block Header is corrupt"); + + // Check for reserved bits in Block Flags. + if ((buf[1] & 0x3C) != 0) + throw new UnsupportedOptionsException( + "Unsupported options in XZ Block Header"); + + // Memory for the Filter Flags field + int filterCount = (buf[1] & 0x03) + 1; + long[] filterIDs = new long[filterCount]; + byte[][] filterProps = new byte[filterCount][]; + + // Use a stream to parse the fields after the Block Flags field. + // Exclude the CRC32 field at the end. + ByteArrayInputStream bufStream = new ByteArrayInputStream( + buf, 2, headerSize - 6); + + try { + // Set the maximum valid compressed size. This is overriden + // by the value from the Compressed Size field if it is present. + compressedSizeLimit = (DecoderUtil.VLI_MAX & ~3) + - headerSize - check.getSize(); + + // Decode and validate Compressed Size if the relevant flag + // is set in Block Flags. + if ((buf[1] & 0x40) != 0x00) { + compressedSizeInHeader = DecoderUtil.decodeVLI(bufStream); + + if (compressedSizeInHeader == 0 + || compressedSizeInHeader > compressedSizeLimit) + throw new CorruptedInputException(); + + compressedSizeLimit = compressedSizeInHeader; + } + + // Decode Uncompressed Size if the relevant flag is set + // in Block Flags. + if ((buf[1] & 0x80) != 0x00) + uncompressedSizeInHeader = DecoderUtil.decodeVLI(bufStream); + + // Decode Filter Flags. + for (int i = 0; i < filterCount; ++i) { + filterIDs[i] = DecoderUtil.decodeVLI(bufStream); + + long filterPropsSize = DecoderUtil.decodeVLI(bufStream); + if (filterPropsSize > bufStream.available()) + throw new CorruptedInputException(); + + filterProps[i] = new byte[(int)filterPropsSize]; + bufStream.read(filterProps[i]); + } + + } catch (IOException e) { + throw new CorruptedInputException("XZ Block Header is corrupt"); + } + + // Check that the remaining bytes are zero. + for (int i = bufStream.available(); i > 0; --i) + if (bufStream.read() != 0x00) + throw new UnsupportedOptionsException( + "Unsupported options in XZ Block Header"); + + // Check if the Filter IDs are supported, decode + // the Filter Properties, and check that they are + // supported by this decoder implementation. + FilterDecoder[] filters = new FilterDecoder[filterIDs.length]; + + for (int i = 0; i < filters.length; ++i) { + if (filterIDs[i] == LZMA2Coder.FILTER_ID) + filters[i] = new LZMA2Decoder(filterProps[i]); + + else if (filterIDs[i] == DeltaCoder.FILTER_ID) + filters[i] = new DeltaDecoder(filterProps[i]); + + else + throw new UnsupportedOptionsException( + "Unknown Filter ID " + filterIDs[i]); + } + + RawCoder.validate(filters); + + // Check the memory usage limit. + if (memoryLimit >= 0) { + int memoryNeeded = 0; + for (int i = 0; i < filters.length; ++i) + memoryNeeded += filters[i].getMemoryUsage(); + + if (memoryNeeded > memoryLimit) + throw new MemoryLimitException(memoryNeeded, memoryLimit); + } + + // Use an input size counter to calculate + // the size of the Compressed Data field. + inCounted = new CountingInputStream(in); + + // Initialize the filter chain. + filterChain = inCounted; + for (int i = filters.length - 1; i >= 0; --i) + filterChain = filters[i].getInputStream(filterChain); + } + + public int read() throws IOException { + byte[] buf = new byte[1]; + return read(buf, 0, 1) == -1 ? -1 : (buf[0] & 0xFF); + } + + public int read(byte[] buf, int off, int len) throws IOException { + int ret = filterChain.read(buf, off, len); + long compressedSize = inCounted.getSize(); + + if (ret > 0) { + check.update(buf, off, ret); + uncompressedSize += ret; + + // Catch invalid values. + if (compressedSize < 0 + || compressedSize > compressedSizeLimit + || uncompressedSize < 0 + || (uncompressedSizeInHeader != -1 + && uncompressedSize > uncompressedSizeInHeader)) + throw new CorruptedInputException(); + + } else if (ret == -1) { + // Validate Compressed Size and Uncompressed Size if they were + // present in Block Header. + if ((compressedSizeInHeader != -1 + && compressedSizeInHeader != compressedSize) + || (uncompressedSizeInHeader != -1 + && uncompressedSizeInHeader != uncompressedSize)) + throw new CorruptedInputException(); + + // Block Padding bytes must be zeros. + for (long i = compressedSize; (i & 3) != 0; ++i) + if (inData.readUnsignedByte() != 0x00) + throw new CorruptedInputException(); + + // Validate the integrity check. + byte[] storedCheck = new byte[check.getSize()]; + inData.readFully(storedCheck); + if (!Arrays.equals(check.finish(), storedCheck)) + throw new CorruptedInputException("Integrity (" + + check.getName() + ") check does not match"); + } + + return ret; + } + + public int available() throws IOException { + return filterChain.available(); + } + + public long getUnpaddedSize() { + return headerSize + inCounted.getSize() + check.getSize(); + } + + public long getUncompressedSize() { + return uncompressedSize; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/BlockOutputStream.java b/third_party/src/main/java/org/tukaani/xz/BlockOutputStream.java new file mode 100644 index 00000000..b031116d --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/BlockOutputStream.java @@ -0,0 +1,128 @@ +/* + * BlockOutputStream + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +import java.io.OutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import org.tukaani.xz.common.EncoderUtil; +import org.tukaani.xz.check.Check; + +class BlockOutputStream extends FinishableOutputStream { + private final OutputStream out; + private final CountingOutputStream outCounted; + private FinishableOutputStream filterChain; + private final Check check; + + private final int headerSize; + private final long compressedSizeLimit; + private long uncompressedSize = 0; + + public BlockOutputStream(OutputStream out, FilterEncoder[] filters, + Check check) throws IOException { + this.out = out; + this.check = check; + + // Initialize the filter chain. + outCounted = new CountingOutputStream(out); + filterChain = outCounted; + for (int i = 0; i < filters.length; ++i) + filterChain = filters[i].getOutputStream(filterChain); + + // Prepare to encode the Block Header field. + ByteArrayOutputStream bufStream = new ByteArrayOutputStream(); + + // Write a dummy Block Header Size field. The real value is written + // once everything else except CRC32 has been written. + bufStream.write(0x00); + + // Write Block Flags. Storing Compressed Size or Uncompressed Size + // isn't supported for now. + bufStream.write(filters.length - 1); + + // List of Filter Flags + for (int i = 0; i < filters.length; ++i) { + EncoderUtil.encodeVLI(bufStream, filters[i].getFilterID()); + byte[] filterProps = filters[i].getFilterProps(); + EncoderUtil.encodeVLI(bufStream, filterProps.length); + bufStream.write(filterProps); + } + + // Header Padding + while ((bufStream.size() & 3) != 0) + bufStream.write(0x00); + + byte[] buf = bufStream.toByteArray(); + + // Total size of the Block Header: Take the size of the CRC32 field + // into account. + headerSize = buf.length + 4; + + // This is just a sanity check. + if (headerSize > EncoderUtil.BLOCK_HEADER_SIZE_MAX) + throw new UnsupportedOptionsException(); + + // Block Header Size + buf[0] = (byte)(buf.length / 4); + + // Write the Block Header field to the output stream. + out.write(buf); + EncoderUtil.writeCRC32(out, buf); + + // Calculate the maximum allowed size of the Compressed Data field. + // It is hard to exceed it so this is mostly to be pedantic. + compressedSizeLimit = (EncoderUtil.VLI_MAX & ~3) + - headerSize - check.getSize(); + } + + public void write(int b) throws IOException { + byte[] buf = new byte[1]; + buf[0] = (byte)b; + write(buf, 0, 1); + } + + public void write(byte[] buf, int off, int len) throws IOException { + filterChain.write(buf, off, len); + check.update(buf, off, len); + uncompressedSize += len; + validate(); + } + + public void finish() throws IOException { + // Finish the Compressed Data field. + filterChain.finish(); + validate(); + + // Block Padding + for (long i = outCounted.getSize(); (i & 3) != 0; ++i) + out.write(0x00); + + // Check + out.write(check.finish()); + } + + private void validate() throws IOException { + long compressedSize = outCounted.getSize(); + + // It is very hard to trigger this exception. + // This is just to be pedantic. + if (compressedSize < 0 || compressedSize > compressedSizeLimit + || uncompressedSize < 0) + throw new XZIOException("XZ Stream has grown too big"); + } + + public long getUnpaddedSize() { + return headerSize + outCounted.getSize() + check.getSize(); + } + + public long getUncompressedSize() { + return uncompressedSize; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/CorruptedInputException.java b/third_party/src/main/java/org/tukaani/xz/CorruptedInputException.java new file mode 100644 index 00000000..d7d95207 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/CorruptedInputException.java @@ -0,0 +1,37 @@ +/* + * CorruptedInputException + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +/** + * Thrown when the compressed input data is corrupt. + * However, it is possible that some or all of the data + * already read from the input stream was corrupt too. + */ +public class CorruptedInputException extends XZIOException { + private static final long serialVersionUID = 3L; + + /** + * Creates a new CorruptedInputException with + * the default error detail message. + */ + public CorruptedInputException() { + super("Compressed data is corrupt"); + } + + /** + * Creates a new CorruptedInputException with + * the specified error detail message. + * + * @param s error detail message + */ + public CorruptedInputException(String s) { + super(s); + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/CountingInputStream.java b/third_party/src/main/java/org/tukaani/xz/CountingInputStream.java new file mode 100644 index 00000000..2d85eaf6 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/CountingInputStream.java @@ -0,0 +1,42 @@ +/* + * CountingInputStream + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +import java.io.FilterInputStream; +import java.io.InputStream; +import java.io.IOException; + +class CountingInputStream extends FilterInputStream { + private long size = 0; + + public CountingInputStream(InputStream in) { + super(in); + } + + public int read() throws IOException { + int ret = in.read(); + if (ret != -1 && size >= 0) + ++size; + + return ret; + } + + public int read(byte[] b, int off, int len) throws IOException { + int ret = in.read(b, off, len); + if (ret > 0 && size >= 0) + size += ret; + + return ret; + } + + public long getSize() { + return size; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/CountingOutputStream.java b/third_party/src/main/java/org/tukaani/xz/CountingOutputStream.java new file mode 100644 index 00000000..16ffaa76 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/CountingOutputStream.java @@ -0,0 +1,46 @@ +/* + * CountingOutputStream + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +import java.io.OutputStream; +import java.io.IOException; + +class CountingOutputStream extends FinishableOutputStream { + private OutputStream out; + private long size = 0; + + public CountingOutputStream(OutputStream out) { + this.out = out; + } + + public void write(int b) throws IOException { + out.write(b); + if (size >= 0) + ++size; + } + + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + if (size >= 0) + size += len; + } + + public void flush() throws IOException { + out.flush(); + } + + public void close() throws IOException { + out.close(); + } + + public long getSize() { + return size; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/DeltaCoder.java b/third_party/src/main/java/org/tukaani/xz/DeltaCoder.java new file mode 100644 index 00000000..808834c8 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/DeltaCoder.java @@ -0,0 +1,26 @@ +/* + * DeltaCoder + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +abstract class DeltaCoder implements FilterCoder { + public static final long FILTER_ID = 0x03; + + public boolean changesSize() { + return false; + } + + public boolean nonLastOK() { + return true; + } + + public boolean lastOK() { + return false; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/DeltaDecoder.java b/third_party/src/main/java/org/tukaani/xz/DeltaDecoder.java new file mode 100644 index 00000000..2893bcec --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/DeltaDecoder.java @@ -0,0 +1,32 @@ +/* + * DeltaDecoder + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +import java.io.InputStream; + +class DeltaDecoder extends DeltaCoder implements FilterDecoder { + private int distance; + + DeltaDecoder(byte[] props) throws UnsupportedOptionsException { + if (props.length != 1) + throw new UnsupportedOptionsException( + "Unsupported Delta filter properties"); + + distance = (props[0] & 0xFF) + 1; + } + + public int getMemoryUsage() { + return 1; + } + + public InputStream getInputStream(InputStream in) { + return new DeltaInputStream(in, distance); + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/DeltaInputStream.java b/third_party/src/main/java/org/tukaani/xz/DeltaInputStream.java new file mode 100644 index 00000000..876c7033 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/DeltaInputStream.java @@ -0,0 +1,105 @@ +/* + * DeltaInputStream + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +import java.io.InputStream; +import java.io.IOException; +import org.tukaani.xz.delta.DeltaDecoder; + +/** + * Decodes Delta-filtered data. + *

      + * The delta filter doesn't change the size of the data and thus it + * cannot have an end-of-payload marker. It will simply decode until + * its input stream indicates end of input. + */ +public class DeltaInputStream extends InputStream { + /** + * Smallest supported delta calculation distance. + */ + public static final int DISTANCE_MIN = 1; + + /** + * Largest supported delta calculation distance. + */ + public static final int DISTANCE_MAX = 256; + + private final InputStream in; + private final DeltaDecoder delta; + + /** + * Creates a new Delta decoder with the given delta calculation distance. + * + * @param in input stream from which Delta filtered data + * is read + * + * @param distance delta calculation distance, must be in the + * range [DISTANCE_MIN, + * DISTANCE_MAX] + */ + public DeltaInputStream(InputStream in, int distance) { + this.in = in; + this.delta = new DeltaDecoder(distance); + } + + /** + * Decode the next byte from this input stream. + * + * @return the next decoded byte, or -1 to indicate + * the end of input on the input stream in + * + * @throws IOException may be thrown by in + */ + public int read() throws IOException { + byte[] buf = new byte[1]; + return read(buf, 0, 1) == -1 ? -1 : (buf[0] & 0xFF); + } + + /** + * Decode into an array of bytes. + *

      + * This calls in.read(buf, off, len) and defilters the + * returned data. + * + * @param buf target buffer for decoded data + * @param off start offset in buf + * @param len maximum number of bytes to read + * + * @return number of bytes read, or -1 to indicate + * the end of the input stream in + * + * @throws IOException may be thrown by underlaying input + * stream in + */ + public int read(byte[] buf, int off, int len) throws IOException { + int size = in.read(buf, off, len); + if (size == -1) + return -1; + + delta.decode(buf, off, size); + return size; + } + + /** + * Calls in.available(). + * + * @return the value returned by in.available() + */ + public int available() throws IOException { + return in.available(); + } + + /** + * Calls in.close(). + */ + public void close() throws IOException { + in.close(); + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/FilterCoder.java b/third_party/src/main/java/org/tukaani/xz/FilterCoder.java new file mode 100644 index 00000000..1e95e37f --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/FilterCoder.java @@ -0,0 +1,16 @@ +/* + * FilterCoder + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +interface FilterCoder { + boolean changesSize(); + boolean nonLastOK(); + boolean lastOK(); +} diff --git a/third_party/src/main/java/org/tukaani/xz/FilterDecoder.java b/third_party/src/main/java/org/tukaani/xz/FilterDecoder.java new file mode 100644 index 00000000..8e2d0061 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/FilterDecoder.java @@ -0,0 +1,17 @@ +/* + * FilterDecoder + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +import java.io.InputStream; + +interface FilterDecoder extends FilterCoder { + int getMemoryUsage(); + InputStream getInputStream(InputStream in); +} diff --git a/third_party/src/main/java/org/tukaani/xz/FilterEncoder.java b/third_party/src/main/java/org/tukaani/xz/FilterEncoder.java new file mode 100644 index 00000000..2b2c2a51 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/FilterEncoder.java @@ -0,0 +1,16 @@ +/* + * FilterEncoder + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +interface FilterEncoder extends FilterCoder { + long getFilterID(); + byte[] getFilterProps(); + FinishableOutputStream getOutputStream(FinishableOutputStream out); +} diff --git a/third_party/src/main/java/org/tukaani/xz/FilterOptions.java b/third_party/src/main/java/org/tukaani/xz/FilterOptions.java new file mode 100644 index 00000000..9c5f9d8e --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/FilterOptions.java @@ -0,0 +1,28 @@ +/* + * FilterOptions + * + * Authors: Lasse Collin + * Igor Pavlov + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +import java.io.InputStream; +import java.io.IOException; + +public abstract class FilterOptions implements Cloneable { + public abstract int getEncoderMemoryUsage(); + public abstract FinishableOutputStream getOutputStream( + FinishableOutputStream out); + + public abstract int getDecoderMemoryUsage(); + public abstract InputStream getInputStream(InputStream in) + throws IOException; + + abstract FilterEncoder getFilterEncoder(); + + FilterOptions() {} +} diff --git a/third_party/src/main/java/org/tukaani/xz/FinishableOutputStream.java b/third_party/src/main/java/org/tukaani/xz/FinishableOutputStream.java new file mode 100644 index 00000000..b360628b --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/FinishableOutputStream.java @@ -0,0 +1,31 @@ +/* + * FinishableOutputStream + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +import java.io.OutputStream; +import java.io.IOException; + +/** + * Output stream that supports finishing without closing + * the underlying stream. + */ +public abstract class FinishableOutputStream extends OutputStream { + /** + * Finish the stream without closing the underlying stream. + * No more data may be written to the stream after finishing. + *

      + * The finish method of FinishableOutputStream + * does nothing. Subclasses should override it if they need finishing + * support, which is the case, for example, with compressors. + * + * @throws IOException + */ + public void finish() throws IOException {}; +} diff --git a/third_party/src/main/java/org/tukaani/xz/IndexIndicatorException.java b/third_party/src/main/java/org/tukaani/xz/IndexIndicatorException.java new file mode 100644 index 00000000..fc6bc038 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/IndexIndicatorException.java @@ -0,0 +1,14 @@ +/* + * IndexIndicatorException + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +class IndexIndicatorException extends Exception { + private static final long serialVersionUID = 1L; +} diff --git a/third_party/src/main/java/org/tukaani/xz/LZMA2Coder.java b/third_party/src/main/java/org/tukaani/xz/LZMA2Coder.java new file mode 100644 index 00000000..b0963b75 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/LZMA2Coder.java @@ -0,0 +1,26 @@ +/* + * LZMA2Coder + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +abstract class LZMA2Coder implements FilterCoder { + public static final long FILTER_ID = 0x21; + + public boolean changesSize() { + return true; + } + + public boolean nonLastOK() { + return false; + } + + public boolean lastOK() { + return true; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/LZMA2Decoder.java b/third_party/src/main/java/org/tukaani/xz/LZMA2Decoder.java new file mode 100644 index 00000000..82075c21 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/LZMA2Decoder.java @@ -0,0 +1,35 @@ +/* + * LZMA2Decoder + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +import java.io.InputStream; + +class LZMA2Decoder extends LZMA2Coder implements FilterDecoder { + private int dictSize; + + LZMA2Decoder(byte[] props) throws UnsupportedOptionsException { + // Up to 1.5 GiB dictionary is supported. The bigger ones + // are too big for int. + if (props.length != 1 || (props[0] & 0xFF) > 37) + throw new UnsupportedOptionsException( + "Unsupported LZMA2 properties"); + + dictSize = 2 | (props[0] & 1); + dictSize <<= (props[0] >>> 1) + 11; + } + + public int getMemoryUsage() { + return LZMA2InputStream.getMemoryUsage(dictSize); + } + + public InputStream getInputStream(InputStream in) { + return new LZMA2InputStream(in, dictSize); + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/LZMA2Encoder.java b/third_party/src/main/java/org/tukaani/xz/LZMA2Encoder.java new file mode 100644 index 00000000..a1048082 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/LZMA2Encoder.java @@ -0,0 +1,35 @@ +/* + * LZMA2Encoder + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +class LZMA2Encoder extends LZMA2Coder implements FilterEncoder { + private LZMA2Options options; + private byte[] props = new byte[1]; + + LZMA2Encoder(LZMA2Options options) { + // Make a private copy so that the caller is free to change its copy. + this.options = (LZMA2Options)options.clone(); + + // TODO: Props!!! + + } + + public long getFilterID() { + return FILTER_ID; + } + + public byte[] getFilterProps() { + return props; + } + + public FinishableOutputStream getOutputStream(FinishableOutputStream out) { + return options.getOutputStream(out); + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/LZMA2InputStream.java b/third_party/src/main/java/org/tukaani/xz/LZMA2InputStream.java new file mode 100644 index 00000000..1551a1f4 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/LZMA2InputStream.java @@ -0,0 +1,329 @@ +/* + * LZMA2InputStream + * + * Authors: Lasse Collin + * Igor Pavlov + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +import java.io.InputStream; +import java.io.DataInputStream; +import java.io.IOException; +import org.tukaani.xz.lz.LZDecoder; +import org.tukaani.xz.rangecoder.RangeDecoder; +import org.tukaani.xz.lzma.LZMADecoder; + +/** + * Decompresses a raw LZMA2 stream. + */ +public class LZMA2InputStream extends InputStream { + /** + * Smallest valid LZMA2 dictionary size. + *

      + * Very tiny dictionaries would be a performance problem, so + * the minimum is 4 KiB. + */ + public static final int DICT_SIZE_MIN = 4096; + + /** + * Largest dictionary size supported by this implementation. + *

      + * The LZMA2 algorithm allows dictionaries up to one byte less than 4 GiB. + * This implementation supports only 16 bytes less than 2 GiB for raw + * LZMA2 streams, and for .xz files the maximum is 1.5 GiB. This + * limitation is due to Java using signed 32-bit integers for array + * indexing. The limitation shouldn't matter much in practice since so + * huge dictionaries are not normally used. + */ + public static final int DICT_SIZE_MAX = Integer.MAX_VALUE & ~15; + + private static final int COMPRESSED_SIZE_MAX = 1 << 16; + + private final DataInputStream in; + + private final LZDecoder lz; + private final RangeDecoder rc = new RangeDecoder(COMPRESSED_SIZE_MAX); + private LZMADecoder lzma; + + private int uncompressedSize = 0; + private boolean isLZMAChunk; + + private boolean needDictReset = true; + private boolean needProps = true; + private boolean endReached = false; + + private IOException exception = null; + + /** + * Gets approximate decompressor memory requirements as kibibytes for + * the given dictionary size. + * + * @param dictSize LZMA2 dictionary size as bytes, must be + * in the range [DICT_SIZE_MIN, + * DICT_SIZE_MAX] + * + * @return approximate memory requirements as kibibytes (KiB) + */ + public static int getMemoryUsage(int dictSize) { + // The base state is aroudn 30-40 KiB (probabilities etc.), + // range decoder needs COMPRESSED_SIZE_MAX bytes for buffering, + // and LZ decoder needs a dictionary buffer. + return 40 + COMPRESSED_SIZE_MAX / 1024 + getDictSize(dictSize) / 1024; + } + + private static int getDictSize(int dictSize) { + if (dictSize < DICT_SIZE_MIN || dictSize > DICT_SIZE_MAX) + throw new IllegalArgumentException( + "Unsupported dictionary size " + dictSize); + + // Round dictionary size upward to a multiple of 16. This way LZMA + // can use LZDecoder.getPos() for calculating LZMA's posMask. + // Note that this check is needed only for raw LZMA2 streams; it is + // redundant with .xz. + return (dictSize + 15) & ~15; + } + + /** + * Creates a new input stream that decompresses raw LZMA2 data + * from in. + *

      + * The caller needs to know the dictionary size used when compressing; + * the dictionary size isn't stored as part of a raw LZMA2 stream. + *

      + * Specifying a too small dictionary size will prevent decompressing + * the stream. Specifying a too big dictionary is waste of memory but + * decompression will work. + *

      + * There is no need to specify a dictionary bigger than + * the uncompressed size of the data even if a bigger dictionary + * was used when compressing. If you know the uncompressed size + * of the data, this might allow saving some memory. + * + * @param in input stream from which LZMA2-compressed + * data is read + * + * @param dictSize LZMA2 dictionary size as bytes, must be + * in the range [DICT_SIZE_MIN, + * DICT_SIZE_MAX] + */ + public LZMA2InputStream(InputStream in, int dictSize) { + this.in = new DataInputStream(in); + this.lz = new LZDecoder(getDictSize(dictSize), null); + } + + /** + * Creates a new LZMA2 decompressor using a preset dictionary. + *

      + * This is like LZMAInputStream() except that the + * dictionary may be initialized using a preset dictionary. + * If a preset dictionary was used when compressing the data, the + * same preset dictionary must be provided when decompressing. + * + * @param in input stream from which LZMA2-compressed + * data is read + * + * @param dictSize LZMA2 dictionary size as bytes, must be + * in the range [DICT_SIZE_MIN, + * DICT_SIZE_MAX] + * + * @param presetDict preset dictionary or null + * to use no preset dictionary + */ + public LZMA2InputStream(InputStream in, int dictSize, byte[] presetDict) + throws IOException { + this.in = new DataInputStream(in); + this.lz = new LZDecoder(getDictSize(dictSize), presetDict); + + if (presetDict.length > 0) + needDictReset = false; + } + + /** + * Decompresses the next byte from this input stream. + *

      + * Reading lots of data with read() from this input stream + * may be inefficient. Wrap it in java.io.BufferedInputStream + * if you need to read lots of data one byte at a time. + * + * @return the next decompressed byte, or -1 + * to indicate the end of the compressed stream + * + * @throws CorruptedInputException + * + * @throws EOFException + * compressed input is truncated or corrupt + * + * @throws IOException may be thrown by in + */ + public int read() throws IOException { + byte[] buf = new byte[1]; + return read(buf, 0, 1) == -1 ? -1 : (buf[0] & 0xFF); + } + + /** + * Decompresses into an array of bytes. + *

      + * If len is zero, no bytes are read and 0 + * is returned. Otherwise this will block until len + * bytes have been decompressed, the end of LZMA2 stream is reached, + * or an exception is thrown. + * + * @param buf target buffer for uncompressed data + * @param off start offset in buf + * @param len maximum number of uncompressed bytes to read + * + * @return number of bytes read, or -1 to indicate + * the end of the compressed stream + * + * @throws CorruptedInputException + * + * @throws EOFException + * compressed input is truncated or corrupt + * + * @throws IOException may be thrown by in + */ + public int read(byte[] buf, int off, int len) throws IOException { + if (off < 0 || len < 0 || off + len < 0 || off + len > buf.length) + throw new IllegalArgumentException(); + + if (len == 0) + return 0; + + if (exception != null) + throw exception; + + if (endReached) + return -1; + + try { + int size = 0; + + while (len > 0) { + if (uncompressedSize == 0) { + decodeChunkHeader(); + if (endReached) + return size == 0 ? -1 : size; + } + + int copySizeMax = Math.min(uncompressedSize, len); + + if (!isLZMAChunk) { + lz.copyUncompressed(in, copySizeMax); + } else { + lz.setLimit(copySizeMax); + lzma.decode(); + } + + int copiedSize = lz.flush(buf, off); + off += copiedSize; + len -= copiedSize; + size += copiedSize; + uncompressedSize -= copiedSize; + + if (uncompressedSize == 0) + if (!rc.isFinished() || lz.hasPending()) + throw new CorruptedInputException(); + } + + return size; + + } catch (IOException e) { + exception = e; + throw e; + } + } + + private void decodeChunkHeader() throws IOException { + int control = in.readUnsignedByte(); + + if (control == 0x00) { + endReached = true; + return; + } + + if (control >= 0xE0 || control == 0x01) { + needProps = true; + needDictReset = false; + lz.reset(); + } else if (needDictReset) { + throw new CorruptedInputException(); + } + + if (control >= 0x80) { + isLZMAChunk = true; + + uncompressedSize = (control & 0x1F) << 16; + uncompressedSize += in.readUnsignedShort() + 1; + + int compressedSize = in.readUnsignedShort() + 1; + + if (control >= 0xC0) { + needProps = false; + decodeProps(); + + } else if (needProps) { + throw new CorruptedInputException(); + + } else if (control >= 0xA0) { + lzma.reset(); + } + + rc.prepareInputBuffer(in, compressedSize); + + } else if (control > 0x02) { + throw new CorruptedInputException(); + + } else { + isLZMAChunk = false; + uncompressedSize = in.readUnsignedShort() + 1; + } + } + + private void decodeProps() throws IOException { + int props = in.readUnsignedByte(); + + if (props > (4 * 5 + 4) * 9 + 8) + throw new CorruptedInputException(); + + int pb = props / (9 * 5); + props -= pb * 9 * 5; + int lp = props / 9; + int lc = props - lp * 9; + + if (lc + lp > 4) + throw new CorruptedInputException(); + + lzma = new LZMADecoder(lz, rc, lc, lp, pb); + } + + /** + * Returns the number of uncompressed bytes that can be read + * without blocking. The value is returned with an assumption + * that the compressed input data will be valid. If the compressed + * data is corrupt, CorruptedInputException may get + * thrown before the number of bytes claimed to be available have + * been read from this input stream. + *

      + * In LZMAInputStream, the return value will be non-zero when the + * decompressor is in the middle of an LZMA2 chunk. The return value + * will then be the number of uncompressed bytes remaining from that + * chunk. + * + * @return the number of uncompressed bytes that can be read + * without blocking + */ + public int available() { + return uncompressedSize; + } + + /** + * Calls in.close(). + */ + public void close() throws IOException { + in.close(); + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/LZMA2Options.java b/third_party/src/main/java/org/tukaani/xz/LZMA2Options.java new file mode 100644 index 00000000..58f21bd8 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/LZMA2Options.java @@ -0,0 +1,139 @@ +/* + * LZMA2Options + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +import java.io.InputStream; +import java.io.IOException; + +/** + * Options for LZMA2. + *

      + * FIXME: This is unfinished and things might change. + */ +public class LZMA2Options extends FilterOptions { + /** + * Default compression preset. + */ + public static final int PRESET_DEFAULT = 6; + + /** + * Minimum dictionary size. + */ + public static final int DICT_SIZE_MIN = 4096; + + /** + * Maximum dictionary size for compression. + *

      + * FIXME? Decompression dictionary size can be bigger. + */ + public static final int DICT_SIZE_MAX = 128 << 20; + + /** + * Maximum value for lc + lp. + */ + public static final int LC_LP_MAX = 4; + + /** + * Maximum value for pb. + */ + public static final int PB_MAX = 4; + + /** + * Compression mode: uncompressed. + * The data is wrapped into a LZMA2 stream without compression. + */ + public static final int MODE_UNCOMPRESSED = 0; + + /** + * Compression mode: fast. + * This is usually combined with a hash chain match finder. + */ + public static final int MODE_FAST = 1; + + /** + * Compression mode: normal. + * This is usually combined with a binary tree match finder. + */ + public static final int MODE_NORMAL = 2; + + /** + * Minimum value for niceLen. + */ + public static final int NICE_LEN_MIN = 8; + + /** + * Maximum value for niceLen. + */ + public static final int NICE_LEN_MAX = 273; + + /** + * Match finder: Hash Chain 2-3-4 + */ + public static final int MF_HC4 = 0x04; + + /** + * Match finder: Binary tree 2-3-4 + */ + public static final int MF_BT4 = 0x14; + + private int dictSize; + +/* + public int lc; + public int lp; + public int pb; + public int mode; + public int niceLen; + public int mf; + public int depth; +*/ + + public LZMA2Options() { + setPreset(PRESET_DEFAULT); + } + + public LZMA2Options(int preset) { + setPreset(preset); + } + + public void setPreset(int preset) { + // TODO + dictSize = 8 << 20; + } + + public int getEncoderMemoryUsage() { + return LZMA2OutputStream.getMemoryUsage(this); + } + + public FinishableOutputStream getOutputStream(FinishableOutputStream out) { + return new LZMA2OutputStream(out, this); + } + + public int getDecoderMemoryUsage() { + return LZMA2InputStream.getMemoryUsage(dictSize); + } + + public InputStream getInputStream(InputStream in) throws IOException { + return new LZMA2InputStream(in, dictSize); + } + + FilterEncoder getFilterEncoder() { + return new LZMA2Encoder(this); + } + + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + // Never reached + throw new RuntimeException(); + } + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/LZMA2OutputStream.java b/third_party/src/main/java/org/tukaani/xz/LZMA2OutputStream.java new file mode 100644 index 00000000..156af2d7 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/LZMA2OutputStream.java @@ -0,0 +1,77 @@ +/* + * LZMA2OutputStream + * + * Authors: Lasse Collin + * Igor Pavlov + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +import java.io.IOException; + +// +// TODO: This creates a valid LZMA2 stream but it doesn't compress. +// So this is useless except for testing the .xz container support. +// + +class LZMA2OutputStream extends FinishableOutputStream { + private final FinishableOutputStream out; + + static int getMemoryUsage(LZMA2Options options) { + // TODO + return 1; + } + + LZMA2OutputStream(FinishableOutputStream out, LZMA2Options options) { + this.out = out; + } + + public void write(int b) throws IOException { + byte[] buf = new byte[1]; + buf[0] = (byte)b; + write(buf, 0, 1); + } + + public void write(byte[] buf, int off, int len) throws IOException { + if (off < 0 || len < 0 || off + len < 0 || off + len > buf.length) + throw new IllegalArgumentException(); + + while (off > 0x10000) { + writeChunk(buf, off, 0x10000); + off += 0x10000; + len -= 0x10000; + } + + writeChunk(buf, off, len); + } + + private void writeChunk(byte[] buf, int off, int len) throws IOException { + out.write(0x01); + out.write((len - 1) >>> 8); + out.write(len - 1); + out.write(buf, off, len); + } + + private void writeEndMarker() throws IOException { + // TODO: Flush incomplete chunk. + out.write(0x00); + } + + public void flush() throws IOException { + throw new UnsupportedOptionsException( + "Flushing LZMA2OutputStream not implemented yet"); + } + + public void finish() throws IOException { + writeEndMarker(); + out.finish(); + } + + public void close() throws IOException { + writeEndMarker(); + out.close(); + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/MemoryLimitException.java b/third_party/src/main/java/org/tukaani/xz/MemoryLimitException.java new file mode 100644 index 00000000..1b254527 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/MemoryLimitException.java @@ -0,0 +1,60 @@ +/* + * MemoryLimitException + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +/** + * Thrown when the memory usage limit given to the XZ decompressor + * would be exceeded. + *

      + * The amount of memory required and the memory usage limit are + * included in the error detail message in human readable format. + */ +public class MemoryLimitException extends XZIOException { + private static final long serialVersionUID = 3L; + + private int memoryNeeded; + private int memoryLimit; + + /** + * Creates a new MemoryLimitException. + *

      + * The amount of memory needed and the memory usage limit are + * included in the error detail message. + * + * @param memoryNeeded amount of memory needed as kibibytes (KiB) + * @param memoryLimit specified memory usage limit as kibibytes (KiB) + */ + public MemoryLimitException(int memoryNeeded, int memoryLimit) { + super("" + memoryNeeded + " KiB of memory would be needed; limit was " + + memoryLimit + " KiB"); + + this.memoryNeeded = memoryNeeded; + this.memoryLimit = memoryLimit; + } + + /** + * Gets how much memory is required to decompress the data. + * + * @return amount of memory needed as kibibytes (KiB) + */ + public int getMemoryNeeded() { + return memoryNeeded; + } + + /** + * Gets what the memory usage limit was at the time the exception + * was created. + * + * @return memory usage limit as kibibytes (KiB) + */ + public int getMemoryLimit() { + return memoryLimit; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/RawCoder.java b/third_party/src/main/java/org/tukaani/xz/RawCoder.java new file mode 100644 index 00000000..12c7da8f --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/RawCoder.java @@ -0,0 +1,33 @@ +/* + * RawCoder + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +class RawCoder { + static void validate(FilterCoder[] filters) + throws UnsupportedOptionsException { + for (int i = 0; i < filters.length - 1; ++i) + if (!filters[i].nonLastOK()) + throw new UnsupportedOptionsException( + "Unsupported XZ filter chain"); + + if (!filters[filters.length - 1].lastOK()) + throw new UnsupportedOptionsException( + "Unsupported XZ filter chain"); + + int changesSizeCount = 0; + for (int i = 0; i < filters.length; ++i) + if (filters[i].changesSize()) + ++changesSizeCount; + + if (changesSizeCount > 3) + throw new UnsupportedOptionsException( + "Unsupported XZ filter chain"); + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/SingleXZInputStream.java b/third_party/src/main/java/org/tukaani/xz/SingleXZInputStream.java new file mode 100644 index 00000000..ffac5540 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/SingleXZInputStream.java @@ -0,0 +1,285 @@ +/* + * SingleXZInputStream + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +import java.io.InputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.EOFException; +import org.tukaani.xz.common.DecoderUtil; +import org.tukaani.xz.common.StreamFlags; +import org.tukaani.xz.index.IndexHash; +import org.tukaani.xz.check.Check; + +/** + * Decompresses exactly one XZ Stream in streamed mode (no seeking). + * The decompression stops after the first XZ Stream has been decompressed, + * and the read position in the input stream is left at the first byte + * after the end of the XZ Stream. This can be useful when XZ data has + * been stored inside some other file format or protocol. + *

      + * Unless you know what you are doing, don't use this class to decompress + * standalone .xz files. For that purpose, use XZInputStream. + * + * @see XZInputStream + */ +public class SingleXZInputStream extends InputStream { + private InputStream in; + private int memoryLimit; + private StreamFlags streamHeaderFlags; + private Check check; + private BlockInputStream blockDecoder = null; + private IndexHash indexHash = new IndexHash(); + private boolean endReached = false; + private IOException exception = null; + + /** + * Creates a new input stream that decompresses exactly one XZ Stream + * from in. + *

      + * This constructor reads and parses the XZ Stream Header (12 bytes) + * from in. The header of the first Block is not read + * until read is called. + * + * @param in input stream from which XZ-compressed + * data is read + * + * @throws XZFormatException + * input is not in the XZ format + * + * @throws CorruptedInputException + * XZ header CRC32 doesn't match + * + * @throws UnsupportedOptionsException + * XZ header is valid but specifies options + * not supported by this implementation + * + * @throws EOFException + * less than 12 bytes of input was available + * from in + * + * @throws IOException may be thrown by in + */ + public SingleXZInputStream(InputStream in) throws IOException { + initialize(in, -1); + } + + /** + * Creates a new single-stream XZ decompressor with optional + * memory usage limit. + *

      + * This is identical to SingleXZInputStream(InputStream) + * except that this takes also the memoryLimit argument. + * + * @param in input stream from which XZ-compressed + * data is read + * + * @param memoryLimit memory usage limit as kibibytes (KiB) + * or -1 to impose no memory usage limit + * + * @throws XZFormatException + * input is not in the XZ format + * + * @throws CorruptedInputException + * XZ header CRC32 doesn't match + * + * @throws UnsupportedOptionsException + * XZ header is valid but specifies options + * not supported by this implementation + * + * @throws EOFException + * less than 12 bytes of input was available + * from in + * + * @throws IOException may be thrown by in + */ + public SingleXZInputStream(InputStream in, int memoryLimit) + throws IOException { + initialize(in, memoryLimit); + } + + SingleXZInputStream(InputStream in, int memoryLimit, + byte[] streamHeader) throws IOException { + initialize(in, memoryLimit, streamHeader); + } + + private void initialize(InputStream in, int memoryLimit) + throws IOException { + byte[] streamHeader = new byte[DecoderUtil.STREAM_HEADER_SIZE]; + new DataInputStream(in).readFully(streamHeader); + initialize(in, memoryLimit, streamHeader); + } + + private void initialize(InputStream in, int memoryLimit, + byte[] streamHeader) throws IOException { + this.in = in; + this.memoryLimit = memoryLimit; + streamHeaderFlags = DecoderUtil.decodeStreamHeader(streamHeader); + check = Check.getInstance(streamHeaderFlags.checkType); + } + + /** + * Gets the ID of the integrity check used in this XZ Stream. + * + * @return the Check ID specified in the XZ Stream Header + */ + public int getCheckType() { + return streamHeaderFlags.checkType; + } + + /** + * Gets the name of the integrity check used in this XZ Stream. + * + * @return the name of the check specified in the XZ Stream Header + */ + public String getCheckName() { + return check.getName(); + } + + /** + * Decompresses the next byte from this input stream. + *

      + * Reading lots of data with read() from this input stream + * may be inefficient. Wrap it in java.io.BufferedInputStream + * if you need to read lots of data one byte at a time. + * + * @return the next decompressed byte, or -1 + * to indicate the end of the compressed stream + * + * @throws CorruptedInputException + * @throws UnsupportedOptionsException + * @throws MemoryLimitException + * + * @throws EOFException + * compressed input is truncated or corrupt + * + * @throws IOException may be thrown by in + */ + public int read() throws IOException { + byte[] buf = new byte[1]; + return read(buf, 0, 1) == -1 ? -1 : (buf[0] & 0xFF); + } + + /** + * Decompresses into an array of bytes. + *

      + * If len is zero, no bytes are read and 0 + * is returned. Otherwise this will try to decompress len + * bytes of uncompressed data. Less than len bytes may + * be read only in the following situations: + *

        + *
      • The end of the compressed data was reached successfully.
      • + *
      • An error is detected after at least one but less len + * bytes have already been successfully decompressed. + * The next call with non-zero len will immediately + * throw the pending exception.
      • + *
      • An exception is thrown.
      • + *
      + * + * @param buf target buffer for uncompressed data + * @param off start offset in buf + * @param len maximum number of uncompressed bytes to read + * + * @return number of bytes read, or -1 to indicate + * the end of the compressed stream + * + * @throws CorruptedInputException + * @throws UnsupportedOptionsException + * @throws MemoryLimitException + * + * @throws EOFException + * compressed input is truncated or corrupt + * + * @throws IOException may be thrown by in + */ + public int read(byte[] buf, int off, int len) throws IOException { + if (off < 0 || len < 0 || off + len < 0 || off + len > buf.length) + throw new IllegalArgumentException(); + + if (len == 0) + return 0; + + if (exception != null) + throw exception; + + if (endReached) + return -1; + + int size = 0; + + try { + while (len > 0) { + if (blockDecoder == null) { + try { + blockDecoder = new BlockInputStream(in, check, + memoryLimit); + } catch (IndexIndicatorException e) { + indexHash.validate(in); + validateStreamFooter(); + endReached = true; + return size > 0 ? size : -1; + } + } + + int ret = blockDecoder.read(buf, off, len); + + if (ret > 0) { + size += ret; + off += ret; + len -= ret; + } else if (ret == -1) { + indexHash.add(blockDecoder.getUnpaddedSize(), + blockDecoder.getUncompressedSize()); + blockDecoder = null; + } + } + } catch (IOException e) { + exception = e; + if (size == 0) + throw e; + } + + return size; + } + + private void validateStreamFooter() throws IOException { + byte[] buf = new byte[DecoderUtil.STREAM_HEADER_SIZE]; + new DataInputStream(in).readFully(buf); + StreamFlags streamFooterFlags = DecoderUtil.decodeStreamFooter(buf); + + if (!DecoderUtil.areStreamFlagsEqual(streamHeaderFlags, + streamFooterFlags) + || indexHash.getIndexSize() != streamFooterFlags.backwardSize) + throw new CorruptedInputException( + "XZ Stream Footer does not match Stream Header"); + } + + /** + * Returns the number of uncompressed bytes that can be read + * without blocking. The value is returned with an assumption + * that the compressed input data will be valid. If the compressed + * data is corrupt, CorruptedInputException may get + * thrown before the number of bytes claimed to be available have + * been read from this input stream. + * + * @return the number of uncompressed bytes that can be read + * without blocking + */ + public int available() throws IOException { + return blockDecoder == null ? 0 : blockDecoder.available(); + } + + /** + * Calls in.close(). + */ + public void close() throws IOException { + in.close(); + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/UnsupportedOptionsException.java b/third_party/src/main/java/org/tukaani/xz/UnsupportedOptionsException.java new file mode 100644 index 00000000..9aa16e8c --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/UnsupportedOptionsException.java @@ -0,0 +1,34 @@ +/* + * UnsupportedOptionsException + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +/** + * Thrown when compression options not supported by this implementation + * are detected. Some other implementation might support those options. + */ +public class UnsupportedOptionsException extends XZIOException { + private static final long serialVersionUID = 3L; + + /** + * Creates a new UnsupportedOptionsException with null + * as its error detail message. + */ + public UnsupportedOptionsException() {} + + /** + * Creates a new UnsupportedOptionsException with the given + * error detail message. + * + * @param s error detail message + */ + public UnsupportedOptionsException(String s) { + super(s); + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/XZ.java b/third_party/src/main/java/org/tukaani/xz/XZ.java new file mode 100644 index 00000000..4e0857ff --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/XZ.java @@ -0,0 +1,53 @@ +/* + * XZ + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +/** + * XZ constants. + */ +public class XZ { + /** + * XZ Header Magic Bytes begin a XZ file. + * This can be useful to detect XZ compressed data. + */ + public static final byte[] HEADER_MAGIC = { + (byte)0xFD, '7', 'z', 'X', 'Z', '\0' }; + + /** + * XZ Footer Magic Bytes are the last bytes of a XZ Stream. + */ + public static final byte[] FOOTER_MAGIC = { 'Y', 'Z' }; + + /** + * Integrity check ID indicating that no integrity check is calculated. + *

      + * Omitting the integrity check is strongly discouraged except when + * the integrity of the data will be verified by other means anyway, + * and calculating the check twice would be useless. + */ + public static final int CHECK_NONE = 0; + + /** + * Integrity check ID for CRC32. + */ + public static final int CHECK_CRC32 = 1; + + /** + * Integrity check ID for CRC64. + */ + public static final int CHECK_CRC64 = 4; + + /** + * Integrity check ID for SHA-256. + */ + public static final int CHECK_SHA256 = 10; + + private XZ() {} +} diff --git a/third_party/src/main/java/org/tukaani/xz/XZFormatException.java b/third_party/src/main/java/org/tukaani/xz/XZFormatException.java new file mode 100644 index 00000000..6f63020b --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/XZFormatException.java @@ -0,0 +1,24 @@ +/* + * XZFormatException + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +/** + * Thrown when the input data is not in the XZ format. + */ +public class XZFormatException extends XZIOException { + private static final long serialVersionUID = 3L; + + /** + * Creates a new exception with the default error detail message. + */ + public XZFormatException() { + super("Input is not in the XZ format"); + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/XZIOException.java b/third_party/src/main/java/org/tukaani/xz/XZIOException.java new file mode 100644 index 00000000..1801c70c --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/XZIOException.java @@ -0,0 +1,28 @@ +/* + * XZIOException + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +/** + * Generic IOException specific to this package. + * All IOExceptions thrown by this package are extended from XZIOException. + * This way it is easier to distinguish exceptions thrown by the XZ code + * from other IOExceptions. + */ +public class XZIOException extends java.io.IOException { + private static final long serialVersionUID = 3L; + + public XZIOException() { + super(); + } + + public XZIOException(String s) { + super(s); + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/XZInputStream.java b/third_party/src/main/java/org/tukaani/xz/XZInputStream.java new file mode 100644 index 00000000..3c44af40 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/XZInputStream.java @@ -0,0 +1,257 @@ +/* + * XZInputStream + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +import java.io.InputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.EOFException; +import org.tukaani.xz.common.DecoderUtil; + +/** + * Decompresses a .xz file in streamed mode (no seeking). + *

      + * Use this to decompress regular standalone .xz files. This reads from + * its input stream until the end of the input or until an error occurs. + * This supports decompressing concatenated .xz files. + * + * @see SingleXZInputStream + */ +public class XZInputStream extends InputStream { + private final int memoryLimit; + private final InputStream in; + private SingleXZInputStream xzIn; + private boolean endReached = false; + private IOException exception = null; + + /** + * Creates a new input stream that decompresses XZ-compressed data + * from in. + *

      + * This constructor reads and parses the XZ Stream Header (12 bytes) + * from in. The header of the first Block is not read + * until read is called. + * + * @param in input stream from which XZ-compressed + * data is read + * + * @throws XZFormatException + * input is not in the XZ format + * + * @throws CorruptedInputException + * XZ header CRC32 doesn't match + * + * @throws UnsupportedOptionsException + * XZ header is valid but specifies options + * not supported by this implementation + * + * @throws EOFException + * less than 12 bytes of input was available + * from in + * + * @throws IOException may be thrown by in + */ + public XZInputStream(InputStream in) throws IOException { + this.in = in; + this.memoryLimit = -1; + this.xzIn = new SingleXZInputStream(in, -1); + } + + /** + * Creates a new input stream that decompresses XZ-compressed data + * from in. + *

      + * This is identical to XZInputStream(InputStream) except + * that this takes also the memoryLimit argument. + * + * @param in input stream from which XZ-compressed + * data is read + * + * @param memoryLimit memory usage limit as kibibytes (KiB) + * or -1 to impose no memory usage limit + * + * @throws XZFormatException + * input is not in the XZ format + * + * @throws CorruptedInputException + * XZ header CRC32 doesn't match + * + * @throws UnsupportedOptionsException + * XZ header is valid but specifies options + * not supported by this implementation + * + * @throws EOFException + * less than 12 bytes of input was available + * from in + * + * @throws IOException may be thrown by in + */ + public XZInputStream(InputStream in, int memoryLimit) throws IOException { + this.in = in; + this.memoryLimit = memoryLimit; + this.xzIn = new SingleXZInputStream(in, memoryLimit); + } + + /** + * Decompresses the next byte from this input stream. + *

      + * Reading lots of data with read() from this input stream + * may be inefficient. Wrap it in java.io.BufferedInputStream + * if you need to read lots of data one byte at a time. + * + * @return the next decompressed byte, or -1 + * to indicate the end of the compressed stream + * + * @throws CorruptedInputException + * @throws UnsupportedOptionsException + * @throws MemoryLimitException + * + * @throws EOFException + * compressed input is truncated or corrupt + * + * @throws IOException may be thrown by in + */ + public int read() throws IOException { + byte[] buf = new byte[1]; + return read(buf, 0, 1) == -1 ? -1 : (buf[0] & 0xFF); + } + + /** + * Decompresses into an array of bytes. + *

      + * If len is zero, no bytes are read and 0 + * is returned. Otherwise this will try to decompress len + * bytes of uncompressed data. Less than len bytes may + * be read only in the following situations: + *

        + *
      • The end of the compressed data was reached successfully.
      • + *
      • An error is detected after at least one but less len + * bytes have already been successfully decompressed. + * The next call with non-zero len will immediately + * throw the pending exception.
      • + *
      • An exception is thrown.
      • + *
      + * + * @param buf target buffer for uncompressed data + * @param off start offset in buf + * @param len maximum number of uncompressed bytes to read + * + * @return number of bytes read, or -1 to indicate + * the end of the compressed stream + * + * @throws CorruptedInputException + * @throws UnsupportedOptionsException + * @throws MemoryLimitException + * + * @throws EOFException + * compressed input is truncated or corrupt + * + * @throws IOException may be thrown by in + */ + public int read(byte[] buf, int off, int len) throws IOException { + if (off < 0 || len < 0 || off + len < 0 || off + len > buf.length) + throw new IllegalArgumentException(); + + if (len == 0) + return 0; + + if (exception != null) + throw exception; + + if (endReached) + return -1; + + int size = 0; + + try { + while (len > 0) { + if (xzIn == null) { + prepareNextStream(); + if (endReached) + return size == 0 ? -1 : size; + } + + int ret = xzIn.read(buf, off, len); + + if (ret > 0) { + size += ret; + off += ret; + len -= ret; + } else if (ret == -1) { + xzIn = null; + } + } + } catch (IOException e) { + exception = e; + if (size == 0) + throw e; + } + + return size; + } + + private void prepareNextStream() throws IOException { + DataInputStream inData = new DataInputStream(in); + byte[] buf = new byte[DecoderUtil.STREAM_HEADER_SIZE]; + + // The size of Stream Padding must be a multiple of four bytes, + // all bytes zero. + do { + // First try to read one byte to see if we have reached the end + // of the file. + int ret = inData.read(buf, 0, 1); + if (ret == -1) { + endReached = true; + return; + } + + // Since we got one byte of input, there must be at least + // three more available in a valid file. + inData.readFully(buf, 1, 3); + + } while (buf[0] == 0 && buf[1] == 0 && buf[2] == 0 && buf[3] == 0); + + // Not all bytes are zero. In a valid Stream it indicates the + // beginning of the next Stream. Read the rest of the Stream Header + // and initialize the XZ decoder. + inData.readFully(buf, 4, DecoderUtil.STREAM_HEADER_SIZE - 4); + + try { + xzIn = new SingleXZInputStream(in, memoryLimit, buf); + } catch (XZFormatException e) { + // Since this isn't the first .xz Stream, it is more + // logical to tell that the data is corrupt. + throw new CorruptedInputException( + "Garbage after a valid XZ Stream"); + } + } + + /** + * Returns the number of uncompressed bytes that can be read + * without blocking. The value is returned with an assumption + * that the compressed input data will be valid. If the compressed + * data is corrupt, CorruptedInputException may get + * thrown before the number of bytes claimed to be available have + * been read from this input stream. + * + * @return the number of uncompressed bytes that can be read + * without blocking + */ + public int available() throws IOException { + return xzIn == null ? 0 : xzIn.available(); + } + + /** + * Calls in.close(). + */ + public void close() throws IOException { + in.close(); + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/XZOutputStream.java b/third_party/src/main/java/org/tukaani/xz/XZOutputStream.java new file mode 100644 index 00000000..09ec3175 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/XZOutputStream.java @@ -0,0 +1,290 @@ +/* + * XZOutputStream + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz; + +import java.io.OutputStream; +import java.io.IOException; + +import org.tukaani.xz.common.EncoderUtil; +import org.tukaani.xz.common.StreamFlags; +import org.tukaani.xz.check.Check; +import org.tukaani.xz.index.IndexEncoder; + +/** + * Compresses into the .xz file format. + */ +public class XZOutputStream extends FinishableOutputStream { + private OutputStream out; + private StreamFlags streamFlags = new StreamFlags(); + private Check check; + private IndexEncoder index = new IndexEncoder(); + private FilterEncoder[] filters; + private BlockOutputStream blockEncoder = null; + private IOException exception = null; + private boolean finished = false; + + /** + * Creates a new output stream that compressed data into the .xz format. + * This is takes options for one filter as an argument. This constructor + * is equivalent to passing a single-member filterOptions array to the + * other constructor. + * + * @param out output stream to which the compressed data + * will be written + * + * @param filterOptions + * filter options to use + * + * @param checkType type of the integrity check, + * for example XZ.CHECK_CRC64 + * + * @throws UnsupportedOptionsException + * invalid filter chain + * + * @throws IOException may be thrown from out + */ + public XZOutputStream(OutputStream out, FilterOptions filterOptions, + int checkType) throws IOException { + FilterOptions[] ops = new FilterOptions[1]; + ops[0] = filterOptions; + initialize(out, ops, checkType); + } + + /** + * Creates a new output stream that compressed data into the .xz format. + * This takes an array of filter options, allowing the caller to specify + * a filter chain with 1-4 filters. + * + * @param out output stream to which the compressed data + * will be written + * + * @param filterOptions + * array of filter options to use + * + * @param checkType type of the integrity check, + * for example XZ.CHECK_CRC64 + * + * @throws UnsupportedOptionsException + * invalid filter chain + * + * @throws IOException may be thrown from out + */ + public XZOutputStream(OutputStream out, FilterOptions[] filterOptions, + int checkType) throws IOException { + initialize(out, filterOptions, checkType); + } + + private void initialize(OutputStream out, FilterOptions[] filterOptions, + int checkType) throws IOException { + this.out = out; + updateFilters(filterOptions); + + streamFlags.checkType = checkType; + check = Check.getInstance(checkType); + + encodeStreamHeader(); + } + + /** + * Updates the filter chain. + *

      + * Currently this cannot be used to update e.g. LZMA2 options in the + * middle of a XZ Block. Use flush() to finish the current + * XZ Block before calling this function. The new filter chain will then + * be used for the next XZ Block. + */ + public void updateFilters(FilterOptions[] filterOptions) + throws XZIOException { + if (blockEncoder != null) + throw new UnsupportedOptionsException("Changing filter options " + + "in the middle of a XZ Block not implemented"); + + if (filterOptions.length < 1 || filterOptions.length > 4) + throw new UnsupportedOptionsException( + "XZ filter chain must be 1-4 filters"); + + FilterEncoder[] newFilters = new FilterEncoder[filterOptions.length]; + for (int i = 0; i < filterOptions.length; ++i) + newFilters[i] = filterOptions[i].getFilterEncoder(); + + RawCoder.validate(newFilters); + filters = newFilters; + } + + /** + * Writes one byte to be compressed. + * + * @throws XZIOException + * XZ stream has grown too big + * @throws IOException may be thrown by the underlying output stream + */ + public void write(int b) throws IOException { + byte[] buf = new byte[] { (byte)b }; + write(buf, 0, 1); + } + + /** + * Writes an array of bytes to be compressed. + * The compressors tend to do internal buffering and thus the written + * data won't be readable from the compressed output immediately. + * Use flush() to force everything written so far to + * be written to the underlaying output stream, but be aware that + * flushing reduces compression ratio. + * + * @param buf buffer of bytes to be written + * @param off start offset in buf + * @param len number of bytes to write + * + * @throws XZIOException + * XZ stream has grown too big + * @throws XZIOException + * finish() or close() + * was already called + * @throws IOException may be thrown by the underlying output stream + */ + public void write(byte[] buf, int off, int len) throws IOException { + if (off < 0 || len < 0 || off + len < 0 || off + len > buf.length) + throw new IllegalArgumentException(); + + if (len == 0) + return; + + if (finished) + exception = new XZIOException( + "XZOutputStream.write was called on a finished stream"); + + if (exception != null) + throw exception; + + if (blockEncoder == null) + blockEncoder = new BlockOutputStream(out, filters, check); + + try { + blockEncoder.write(buf, off, len); + } catch (IOException e) { + exception = e; + throw e; + } + } + + /** + * Flushes the encoder and calls out.flush(). + *

      + * FIXME: I haven't decided yet how this will work in the final version. + * In the current implementation, flushing finishes the current .xz Block. + * This is equivalent to LZMA_FULL_FLUSH in liblzma (XZ Utils). + * Equivalent of liblzma's LZMA_SYNC_FLUSH might be implemented in + * the future, and perhaps should be what flush() should do. + */ + public void flush() throws IOException { + if (exception != null) + throw exception; + + if (blockEncoder != null) { + try { + blockEncoder.finish(); + index.add(blockEncoder.getUnpaddedSize(), + blockEncoder.getUncompressedSize()); + blockEncoder = null; + } catch (IOException e) { + exception = e; + throw e; + } + } + + out.flush(); + } + + /** + * Finishes compression without closing the underlying stream. + * No more data can be written to this stream after finishing + * (calling write with an empty buffer is OK). + *

      + * Repeated calls to finish() do nothing unless + * an exception was thrown by this stream earlier. In that case + * the same exception is thrown again. + *

      + * After finishing, the stream may be closed normally with + * close(). If the stream will be closed anyway, there + * usually is no need to call finish() separately. + */ + public void finish() throws IOException { + if (!finished) { + // flush() checks for pending exceptions so we don't need to + // worry about it here. + flush(); + + try { + index.encode(out); + encodeStreamFooter(); + finished = true; + } catch (IOException e) { + exception = e; + throw e; + } + } + } + + /** + * Finishes compression and closes the underlying stream. + * The underlying stream out is closed even if finishing + * fails. If both finishing and closing fail, the exception thrown + * by finish() is thrown and the exception from the failed + * out.close() is lost. + */ + public void close() throws IOException { + // If finish() throws an exception, it stores the exception to + // the variable "exception". So we can ignore the possible + // exception here. + try { + finish(); + } catch (IOException e) {} + + try { + out.close(); + } catch (IOException e) { + // Remember the exception but only if there is no previous + // pending exception. + if (exception == null) + exception = e; + } + + if (exception != null) + throw exception; + } + + private void encodeStreamFlags(byte[] buf, int off) { + buf[off] = 0x00; + buf[off + 1] = (byte)streamFlags.checkType; + } + + private void encodeStreamHeader() throws IOException { + out.write(XZ.HEADER_MAGIC); + + byte[] buf = new byte[2]; + encodeStreamFlags(buf, 0); + out.write(buf); + + EncoderUtil.writeCRC32(out, buf); + } + + private void encodeStreamFooter() throws IOException { + byte[] buf = new byte[6]; + long backwardSize = index.getIndexSize() / 4 - 1; + for (int i = 0; i < 4; ++i) + buf[i] = (byte)(backwardSize >>> (i * 8)); + + encodeStreamFlags(buf, 4); + + EncoderUtil.writeCRC32(out, buf); + out.write(buf); + out.write(XZ.FOOTER_MAGIC); + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/check/CRC32.java b/third_party/src/main/java/org/tukaani/xz/check/CRC32.java new file mode 100644 index 00000000..87bc6567 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/check/CRC32.java @@ -0,0 +1,33 @@ +/* + * CRC32 + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.check; + +public class CRC32 extends Check { + private java.util.zip.CRC32 state = new java.util.zip.CRC32(); + + public CRC32() { + size = 4; + name = "CRC32"; + } + + public void update(byte[] buf, int off, int len) { + state.update(buf, off, len); + } + + public byte[] finish() { + long value = state.getValue(); + byte[] buf = new byte[] { (byte)(value), + (byte)(value >>> 8), + (byte)(value >>> 16), + (byte)(value >>> 24) }; + state.reset(); + return buf; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/check/CRC64.java b/third_party/src/main/java/org/tukaani/xz/check/CRC64.java new file mode 100644 index 00000000..c120ca9c --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/check/CRC64.java @@ -0,0 +1,54 @@ +/* + * CRC64 + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.check; + +public class CRC64 extends Check { + private static final long poly = 0xC96C5795D7870F42L; + private static final long crcTable[] = new long[256]; + + private long crc = -1; + + static { + for (int b = 0; b < crcTable.length; ++b) { + long r = b; + for (int i = 0; i < 8; ++i) { + if ((r & 1) == 1) + r = (r >>> 1) ^ poly; + else + r >>>= 1; + } + + crcTable[b] = r; + } + } + + public CRC64() { + size = 8; + name = "CRC64"; + } + + public void update(byte[] buf, int off, int len) { + int end = off + len; + + while (off < end) + crc = crcTable[(buf[off++] ^ (int)crc) & 0xFF] ^ (crc >>> 8); + } + + public byte[] finish() { + long value = ~crc; + crc = -1; + + byte[] buf = new byte[8]; + for (int i = 0; i < buf.length; ++i) + buf[i] = (byte)(value >> (i * 8)); + + return buf; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/check/Check.java b/third_party/src/main/java/org/tukaani/xz/check/Check.java new file mode 100644 index 00000000..f2fe4bae --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/check/Check.java @@ -0,0 +1,57 @@ +/* + * Check + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.check; + +import org.tukaani.xz.XZ; +import org.tukaani.xz.UnsupportedOptionsException; + +public abstract class Check { + int size; + String name; + + public abstract void update(byte[] buf, int off, int len); + public abstract byte[] finish(); + + public void update(byte[] buf) { + update(buf, 0, buf.length); + } + + public int getSize() { + return size; + } + + public String getName() { + return name; + } + + public static Check getInstance(int checkType) + throws UnsupportedOptionsException { + switch (checkType) { + case XZ.CHECK_NONE: + return new None(); + + case XZ.CHECK_CRC32: + return new CRC32(); + + case XZ.CHECK_CRC64: + return new CRC64(); + + case XZ.CHECK_SHA256: + try { + return new SHA256(); + } catch (java.security.NoSuchAlgorithmException e) {} + + break; + } + + throw new UnsupportedOptionsException( + "Unsupported Check ID " + checkType); + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/check/None.java b/third_party/src/main/java/org/tukaani/xz/check/None.java new file mode 100644 index 00000000..b07c8e66 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/check/None.java @@ -0,0 +1,24 @@ +/* + * None + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.check; + +public class None extends Check { + public None() { + size = 0; + name = "None"; + } + + public void update(byte[] buf, int off, int len) {} + + public byte[] finish() { + byte[] empty = new byte[0]; + return empty; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/check/SHA256.java b/third_party/src/main/java/org/tukaani/xz/check/SHA256.java new file mode 100644 index 00000000..02f0592a --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/check/SHA256.java @@ -0,0 +1,30 @@ +/* + * SHA256 + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.check; + +public class SHA256 extends Check { + private java.security.MessageDigest sha256; + + public SHA256() throws java.security.NoSuchAlgorithmException { + size = 32; + name = "SHA-256"; + sha256 = java.security.MessageDigest.getInstance("SHA-256"); + } + + public void update(byte[] buf, int off, int len) { + sha256.update(buf, off, len); + } + + public byte[] finish() { + byte[] buf = sha256.digest(); + sha256.reset(); + return buf; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/common/DecoderUtil.java b/third_party/src/main/java/org/tukaani/xz/common/DecoderUtil.java new file mode 100644 index 00000000..77ba4413 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/common/DecoderUtil.java @@ -0,0 +1,121 @@ +/* + * DecoderUtil + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.common; + +import java.io.InputStream; +import java.io.IOException; +import java.io.EOFException; +import java.util.zip.CRC32; +import org.tukaani.xz.XZ; +import org.tukaani.xz.XZFormatException; +import org.tukaani.xz.CorruptedInputException; +import org.tukaani.xz.UnsupportedOptionsException; + +public class DecoderUtil extends Util { + public static boolean isCRC32Valid(byte[] buf, int off, int len, + int ref_off) { + CRC32 crc32 = new CRC32(); + crc32.update(buf, off, len); + long value = crc32.getValue(); + + for (int i = 0; i < 4; ++i) + if ((byte)(value >>> (i * 8)) != buf[ref_off + i]) + return false; + + return true; + } + + public static StreamFlags decodeStreamHeader(byte[] buf) + throws IOException { + for (int i = 0; i < XZ.HEADER_MAGIC.length; ++i) + if (buf[i] != XZ.HEADER_MAGIC[i]) + throw new XZFormatException(); + + if (!isCRC32Valid(buf, XZ.HEADER_MAGIC.length, 2, + XZ.HEADER_MAGIC.length + 2)) + throw new CorruptedInputException("XZ Stream Header is corrupt"); + + try { + return decodeStreamFlags(buf, XZ.HEADER_MAGIC.length); + } catch (UnsupportedOptionsException e) { + throw new UnsupportedOptionsException( + "Unsupported options in XZ Stream Header"); + } + } + + public static StreamFlags decodeStreamFooter(byte[] buf) + throws IOException { + if (buf[10] != XZ.FOOTER_MAGIC[0] || buf[11] != XZ.FOOTER_MAGIC[1]) { + // NOTE: The exception could be XZFormatException too. + // It depends on the situation which one is better. + throw new CorruptedInputException("XZ Stream Footer is corrupt"); + } + + if (!isCRC32Valid(buf, 4, 6, 0)) + throw new CorruptedInputException("XZ Stream Footer is corrupt"); + + StreamFlags streamFlags; + try { + streamFlags = decodeStreamFlags(buf, 8); + } catch (UnsupportedOptionsException e) { + throw new UnsupportedOptionsException( + "Unsupported options in XZ Stream Footer"); + } + + streamFlags.backwardSize = 0; + for (int i = 0; i < 4; ++i) + streamFlags.backwardSize |= (buf[i + 4] & 0xFF) << (i * 8); + + streamFlags.backwardSize = (streamFlags.backwardSize + 1) * 4; + + return streamFlags; + } + + private static StreamFlags decodeStreamFlags(byte[] buf, int off) + throws UnsupportedOptionsException { + if (buf[off] != 0x00 || (buf[off + 1] & 0xFF) >= 0x10) + throw new UnsupportedOptionsException(); + + StreamFlags streamFlags = new StreamFlags(); + streamFlags.checkType = buf[off + 1]; + + return streamFlags; + } + + public static boolean areStreamFlagsEqual(StreamFlags a, StreamFlags b) { + // backwardSize is intentionally not compared. + return a.checkType == b.checkType; + } + + public static long decodeVLI(InputStream in) throws IOException { + int b = in.read(); + if (b == -1) + throw new EOFException(); + + long num = b & 0x7F; + int i = 0; + + while ((b & 0x80) != 0x00) { + if (++i >= VLI_SIZE_MAX) + throw new CorruptedInputException(); + + b = in.read(); + if (b == -1) + throw new EOFException(); + + if (b == 0x00) + throw new CorruptedInputException(); + + num |= (long)(b & 0x7F) << (i * 7); + } + + return num; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/common/EncoderUtil.java b/third_party/src/main/java/org/tukaani/xz/common/EncoderUtil.java new file mode 100644 index 00000000..57f688b5 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/common/EncoderUtil.java @@ -0,0 +1,36 @@ +/* + * EncoderUtil + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.common; + +import java.io.OutputStream; +import java.io.IOException; +import java.util.zip.CRC32; + +public class EncoderUtil extends Util { + public static void writeCRC32(OutputStream out, byte[] buf) + throws IOException { + CRC32 crc32 = new CRC32(); + crc32.update(buf); + long value = crc32.getValue(); + + for (int i = 0; i < 4; ++i) + out.write((byte)(value >>> (i * 8))); + } + + public static void encodeVLI(OutputStream out, long num) + throws IOException { + while (num >= 0x80) { + out.write((byte)(num | 0x80)); + num >>>= 7; + } + + out.write((byte)num); + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/common/StreamFlags.java b/third_party/src/main/java/org/tukaani/xz/common/StreamFlags.java new file mode 100644 index 00000000..b306987d --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/common/StreamFlags.java @@ -0,0 +1,15 @@ +/* + * StreamFlags + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.common; + +public class StreamFlags { + public int checkType = -1; + public long backwardSize = -1; +} diff --git a/third_party/src/main/java/org/tukaani/xz/common/Util.java b/third_party/src/main/java/org/tukaani/xz/common/Util.java new file mode 100644 index 00000000..c4324ce0 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/common/Util.java @@ -0,0 +1,28 @@ +/* + * Util + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.common; + +public class Util { + public static final int STREAM_HEADER_SIZE = 12; + public static final long BACKWARD_SIZE_MAX = 1L << 34; + public static final int BLOCK_HEADER_SIZE_MAX = 1024; + public static final long VLI_MAX = Long.MAX_VALUE; + public static final int VLI_SIZE_MAX = 9; + + public static int getVLISize(long num) { + int size = 0; + do { + ++size; + num >>= 7; + } while (num != 0); + + return size; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/delta/DeltaCoder.java b/third_party/src/main/java/org/tukaani/xz/delta/DeltaCoder.java new file mode 100644 index 00000000..e3b300b0 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/delta/DeltaCoder.java @@ -0,0 +1,27 @@ +/* + * DeltaCoder + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.delta; + +abstract class DeltaCoder { + static final int DISTANCE_MIN = 1; + static final int DISTANCE_MAX = 256; + static final int DISTANCE_MASK = DISTANCE_MAX - 1; + + final int distance; + final byte[] history = new byte[DISTANCE_MAX]; + int pos = 0; + + public DeltaCoder(int distance) { + if (distance < DISTANCE_MIN || distance > DISTANCE_MAX) + throw new IllegalArgumentException(); + + this.distance = distance; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/delta/DeltaDecoder.java b/third_party/src/main/java/org/tukaani/xz/delta/DeltaDecoder.java new file mode 100644 index 00000000..154cbf34 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/delta/DeltaDecoder.java @@ -0,0 +1,24 @@ +/* + * DeltaDecoder + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.delta; + +public class DeltaDecoder extends DeltaCoder { + public DeltaDecoder(int distance) { + super(distance); + } + + public void decode(byte[] buf, int off, int len) { + int end = off + len; + for (int i = off; i < end; ++i) { + buf[i] += history[(distance + pos) & DISTANCE_MASK]; + history[pos-- & DISTANCE_MASK] = buf[i]; + } + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/index/IndexBase.java b/third_party/src/main/java/org/tukaani/xz/index/IndexBase.java new file mode 100644 index 00000000..e08f17ce --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/index/IndexBase.java @@ -0,0 +1,56 @@ +/* + * IndexBase + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.index; + +import org.tukaani.xz.common.Util; +import org.tukaani.xz.XZIOException; + +abstract class IndexBase { + private final XZIOException invalidIndexException; + long blocksSum = 0; + long uncompressedSum = 0; + long indexListSize = 0; + long recordCount = 0; + + IndexBase(XZIOException invalidIndexException) { + this.invalidIndexException = invalidIndexException; + } + + private long getUnpaddedIndexSize() { + // Index Indicator + Number of Records + List of Records + CRC32 + return 1 + Util.getVLISize(recordCount) + indexListSize + 4; + } + + public long getIndexSize() { + return (getUnpaddedIndexSize() + 3) & ~3; + } + + long getStreamSize() { + return Util.STREAM_HEADER_SIZE + blocksSum + getIndexSize() + + Util.STREAM_HEADER_SIZE; + } + + int getIndexPaddingSize() { + return (int)((4 - getUnpaddedIndexSize()) & 3); + } + + void add(long unpaddedSize, long uncompressedSize) throws XZIOException { + blocksSum += (unpaddedSize + 3) & ~3; + uncompressedSum += uncompressedSize; + indexListSize += Util.getVLISize(unpaddedSize) + + Util.getVLISize(uncompressedSize); + ++recordCount; + + if (blocksSum < 0 || uncompressedSum < 0 + || getIndexSize() > Util.BACKWARD_SIZE_MAX + || getStreamSize() < 0) + throw invalidIndexException; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/index/IndexEncoder.java b/third_party/src/main/java/org/tukaani/xz/index/IndexEncoder.java new file mode 100644 index 00000000..d2453abb --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/index/IndexEncoder.java @@ -0,0 +1,59 @@ +/* + * IndexEncoder + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.index; + +import java.io.OutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.zip.CheckedOutputStream; +import org.tukaani.xz.common.EncoderUtil; +import org.tukaani.xz.XZIOException; + +public class IndexEncoder extends IndexBase { + private ArrayList records = new ArrayList(); + + public IndexEncoder() { + super(new XZIOException("XZ Stream or its Index has grown too big")); + } + + public void add(long unpaddedSize, long uncompressedSize) + throws XZIOException { + super.add(unpaddedSize, uncompressedSize); + records.add(new IndexRecord(unpaddedSize, uncompressedSize)); + } + + public void encode(OutputStream out) throws IOException { + java.util.zip.CRC32 crc32 = new java.util.zip.CRC32(); + CheckedOutputStream outChecked = new CheckedOutputStream(out, crc32); + + // Index Indicator + outChecked.write(0x00); + + // Number of Records + EncoderUtil.encodeVLI(outChecked, recordCount); + + // List of Records + for (Iterator i = records.iterator(); i.hasNext(); ) { + IndexRecord record = (IndexRecord)i.next(); + EncoderUtil.encodeVLI(outChecked, record.unpadded); + EncoderUtil.encodeVLI(outChecked, record.uncompressed); + } + + // Index Padding + for (int i = getIndexPaddingSize(); i > 0; --i) + outChecked.write(0x00); + + // CRC32 + long value = crc32.getValue(); + for (int i = 0; i < 4; ++i) + out.write((byte)(value >>> (i * 8))); + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/index/IndexHash.java b/third_party/src/main/java/org/tukaani/xz/index/IndexHash.java new file mode 100644 index 00000000..ab168c69 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/index/IndexHash.java @@ -0,0 +1,94 @@ +/* + * IndexHash + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.index; + +import java.io.InputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.zip.CheckedInputStream; +import org.tukaani.xz.common.DecoderUtil; +import org.tukaani.xz.XZIOException; +import org.tukaani.xz.CorruptedInputException; + +public class IndexHash extends IndexBase { + private org.tukaani.xz.check.Check hash; + + public IndexHash() { + super(new CorruptedInputException()); + + try { + hash = new org.tukaani.xz.check.SHA256(); + } catch (java.security.NoSuchAlgorithmException e) { + hash = new org.tukaani.xz.check.CRC32(); + } + } + + public void add(long unpaddedSize, long uncompressedSize) + throws XZIOException { + super.add(unpaddedSize, uncompressedSize); + + ByteBuffer buf = ByteBuffer.allocate(2 * 8); + buf.putLong(unpaddedSize); + buf.putLong(uncompressedSize); + hash.update(buf.array()); + } + + public void validate(InputStream in) throws IOException { + // Index Indicator (0x00) has already been read by BlockInputStream + // so add 0x00 to the CRC32 here. + java.util.zip.CRC32 crc32 = new java.util.zip.CRC32(); + crc32.update('\0'); + CheckedInputStream inChecked = new CheckedInputStream(in, crc32); + + // Get and validate the Number of Records field. + long storedRecordCount = DecoderUtil.decodeVLI(inChecked); + if (storedRecordCount != recordCount) + throw new CorruptedInputException("XZ Index is corrupt"); + + // Decode and hash the Index field and compare it to + // the hash value calculated from the decoded Blocks. + IndexHash stored = new IndexHash(); + for (long i = 0; i < recordCount; ++i) { + long unpaddedSize = DecoderUtil.decodeVLI(inChecked); + long uncompressedSize = DecoderUtil.decodeVLI(inChecked); + + try { + stored.add(unpaddedSize, uncompressedSize); + } catch (XZIOException e) { + throw new CorruptedInputException("XZ Index is corrupt"); + } + + if (stored.blocksSum > blocksSum + || stored.uncompressedSum > uncompressedSum + || stored.indexListSize > indexListSize) + throw new CorruptedInputException("XZ Index is corrupt"); + } + + if (stored.blocksSum != blocksSum + || stored.uncompressedSum != uncompressedSum + || stored.indexListSize != indexListSize + || !Arrays.equals(stored.hash.finish(), hash.finish())) + throw new CorruptedInputException("XZ Index is corrupt"); + + // Index Padding + DataInputStream inData = new DataInputStream(inChecked); + for (int i = getIndexPaddingSize(); i > 0; --i) + if (inData.readUnsignedByte() != 0x00) + throw new CorruptedInputException("XZ Index is corrupt"); + + // CRC32 + long value = crc32.getValue(); + for (int i = 0; i < 4; ++i) + if (((value >>> (i * 8)) & 0xFF) != inData.readUnsignedByte()) + throw new CorruptedInputException("XZ Index is corrupt"); + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/index/IndexRecord.java b/third_party/src/main/java/org/tukaani/xz/index/IndexRecord.java new file mode 100644 index 00000000..97629cc3 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/index/IndexRecord.java @@ -0,0 +1,20 @@ +/* + * IndexRecord + * + * Author: Lasse Collin + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.index; + +public class IndexRecord { + public final long unpadded; + public final long uncompressed; + + IndexRecord(long unpadded, long uncompressed) { + this.unpadded = unpadded; + this.uncompressed = uncompressed; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/lz/LZDecoder.java b/third_party/src/main/java/org/tukaani/xz/lz/LZDecoder.java new file mode 100644 index 00000000..ec9af08b --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/lz/LZDecoder.java @@ -0,0 +1,126 @@ +/* + * LZDecoder + * + * Authors: Lasse Collin + * Igor Pavlov + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.lz; + +import java.io.DataInputStream; +import java.io.IOException; +import org.tukaani.xz.CorruptedInputException; + +public final class LZDecoder { + private byte[] buf; + private int start = 0; + private int pos = 0; + private int full = 0; + private int limit = 0; + private int pendingLen = 0; + private int pendingDist = 0; + + public LZDecoder(int dictSize, byte[] presetDict) { + buf = new byte[dictSize]; + + if (presetDict != null) { + pos = Math.min(presetDict.length, dictSize); + full = pos; + start = pos; + System.arraycopy(presetDict, presetDict.length - pos, buf, 0, pos); + } + } + + public void reset() { + start = 0; + pos = 0; + full = 0; + limit = 0; + buf[buf.length - 1] = 0x00; + } + + public void setLimit(int outMax) { + if (buf.length - pos <= outMax) + limit = buf.length; + else + limit = pos + outMax; + } + + public boolean hasSpace() { + return pos < limit; + } + + public boolean hasPending() { + return pendingLen > 0; + } + + public int getPos() { + return pos; + } + + public int getByte(int dist) { + int offset = pos - dist - 1; + if (dist >= pos) + offset += buf.length; + + return buf[offset] & 0xFF; + } + + public void putByte(byte b) { + buf[pos++] = b; + + if (full < pos) + full = pos; + } + + public void repeat(int dist, int len) throws IOException { + if (dist < 0 || dist >= full) + throw new CorruptedInputException(); + + int left = Math.min(limit - pos, len); + pendingLen = len - left; + pendingDist = dist; + + int back = pos - dist - 1; + if (dist >= pos) + back += buf.length; + + do { + buf[pos++] = buf[back++]; + if (back == buf.length) + back = 0; + } while (--left > 0); + + if (full < pos) + full = pos; + } + + public void repeatPending() throws IOException { + if (pendingLen > 0) + repeat(pendingDist, pendingLen); + } + + public void copyUncompressed(DataInputStream inData, int len) + throws IOException { + int copySize = Math.min(buf.length - pos, len); + inData.readFully(buf, pos, copySize); + pos += copySize; + + if (full < pos) + full = pos; + } + + public int flush(byte[] out, int outOff) { + int copySize = pos - start; + if (pos == buf.length) + pos = 0; + + System.arraycopy(buf, start, out, outOff, copySize); + start = pos; + + return copySize; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/lzma/LZMACoder.java b/third_party/src/main/java/org/tukaani/xz/lzma/LZMACoder.java new file mode 100644 index 00000000..674680e0 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/lzma/LZMACoder.java @@ -0,0 +1,139 @@ +/* + * LZMACoder + * + * Authors: Lasse Collin + * Igor Pavlov + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.lzma; + +import org.tukaani.xz.rangecoder.RangeCoder; + +abstract class LZMACoder { + static final int POS_STATES_MAX = 1 << 4; + + static final int MATCH_LEN_MIN = 2; + static final int MATCH_LEN_MAX = MATCH_LEN_MIN + LengthCoder.LOW_SYMBOLS + + LengthCoder.MID_SYMBOLS + + LengthCoder.HIGH_SYMBOLS - 1; + + static final int DIST_STATES = 4; + static final int DIST_SLOTS = 1 << 6; + static final int DIST_MODEL_START = 4; + static final int DIST_MODEL_END = 14; + + static final int ALIGN_BITS = 4; + static final int ALIGN_SIZE = 1 << ALIGN_BITS; + static final int ALIGN_MASK = ALIGN_SIZE - 1; + + static final int REPS = 4; + + final int posMask; + + final int[] rep = new int[4]; + final State state = new State(); + + final short[][] isMatch = new short[State.STATES][POS_STATES_MAX]; + final short[] isRep = new short[State.STATES]; + final short[] isRep0 = new short[State.STATES]; + final short[] isRep1 = new short[State.STATES]; + final short[] isRep2 = new short[State.STATES]; + final short[][] isRep0Long = new short[State.STATES][POS_STATES_MAX]; + final short[][] distSlots = new short[DIST_STATES][DIST_SLOTS]; + final short[][] distSpecial = { new short[2], new short[2], + new short[4], new short[4], + new short[8], new short[8], + new short[16], new short[16], + new short[32], new short[32] }; + final short[] distAlign = new short[ALIGN_SIZE]; + + static final int getDistState(int len) { + return len < DIST_STATES + MATCH_LEN_MIN + ? len - MATCH_LEN_MIN + : DIST_STATES - 1; + } + + LZMACoder(int pb) { + posMask = (1 << pb) - 1; + } + + void reset() { + rep[0] = 0; + rep[1] = 0; + rep[2] = 0; + rep[3] = 0; + state.reset(); + + for (int i = 0; i < isMatch.length; ++i) + RangeCoder.initProbs(isMatch[i]); + + RangeCoder.initProbs(isRep); + RangeCoder.initProbs(isRep0); + RangeCoder.initProbs(isRep1); + RangeCoder.initProbs(isRep2); + + for (int i = 0; i < isRep0Long.length; ++i) + RangeCoder.initProbs(isRep0Long[i]); + + for (int i = 0; i < distSlots.length; ++i) + RangeCoder.initProbs(distSlots[i]); + + for (int i = 0; i < distSpecial.length; ++i) + RangeCoder.initProbs(distSpecial[i]); + + RangeCoder.initProbs(distAlign); + } + + + abstract class LiteralCoder { + private final int lc; + private final int literalPosMask; + + LiteralCoder(int lc, int lp) { + this.lc = lc; + this.literalPosMask = (1 << lp) - 1; + } + + final int getSubcoderIndex(int prevByte, int pos) { + int low = prevByte >> (8 - lc); + int high = (pos & literalPosMask) << lc; + return low + high; + } + + + abstract class LiteralSubcoder { + final short[] probs = new short[0x300]; + + void reset() { + RangeCoder.initProbs(probs); + } + } + } + + + abstract class LengthCoder { + static final int LOW_SYMBOLS = 1 << 3; + static final int MID_SYMBOLS = 1 << 3; + static final int HIGH_SYMBOLS = 1 << 8; + + final short[] choice = new short[2]; + final short[][] low = new short[POS_STATES_MAX][LOW_SYMBOLS]; + final short[][] mid = new short[POS_STATES_MAX][MID_SYMBOLS]; + final short[] high = new short[HIGH_SYMBOLS]; + + void reset() { + RangeCoder.initProbs(choice); + + for (int i = 0; i < low.length; ++i) + RangeCoder.initProbs(low[i]); + + for (int i = 0; i < low.length; ++i) + RangeCoder.initProbs(mid[i]); + + RangeCoder.initProbs(high); + } + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/lzma/LZMADecoder.java b/third_party/src/main/java/org/tukaani/xz/lzma/LZMADecoder.java new file mode 100644 index 00000000..68337277 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/lzma/LZMADecoder.java @@ -0,0 +1,189 @@ +/* + * LZMADecoder + * + * Authors: Lasse Collin + * Igor Pavlov + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.lzma; + +import java.io.IOException; +import org.tukaani.xz.lz.LZDecoder; +import org.tukaani.xz.rangecoder.RangeDecoder; +import org.tukaani.xz.CorruptedInputException; + +public final class LZMADecoder extends LZMACoder { + private final LZDecoder lz; + private final RangeDecoder rc; + private final LiteralDecoder literalDecoder; + private final LengthDecoder matchLenDecoder = new LengthDecoder(); + private final LengthDecoder repLenDecoder = new LengthDecoder(); + + public LZMADecoder(LZDecoder lz, RangeDecoder rc, int lc, int lp, int pb) { + super(pb); + this.lz = lz; + this.rc = rc; + this.literalDecoder = new LiteralDecoder(lc, lp); + reset(); + } + + public void reset() { + super.reset(); + literalDecoder.reset(); + matchLenDecoder.reset(); + repLenDecoder.reset(); + } + + public void decode() throws IOException { + lz.repeatPending(); + + while (lz.hasSpace()) { + int posState = lz.getPos() & posMask; + + if (rc.decodeBit(isMatch[state.get()], posState) == 0) { + literalDecoder.decode(); + } else { + int len = rc.decodeBit(isRep, state.get()) == 0 + ? decodeMatch(posState) + : decodeRepMatch(posState); + lz.repeat(rep[0], len); + } + } + + rc.normalize(); + + if (!rc.isInBufferOK()) + throw new CorruptedInputException(); + } + + private int decodeMatch(int posState) throws IOException { + state.updateMatch(); + + rep[3] = rep[2]; + rep[2] = rep[1]; + rep[1] = rep[0]; + + int len = matchLenDecoder.decode(posState); + int distSlot = rc.decodeBitTree(distSlots[getDistState(len)]); + + if (distSlot < DIST_MODEL_START) { + rep[0] = distSlot; + } else { + int limit = (distSlot >> 1) - 1; + rep[0] = (2 | (distSlot & 1)) << limit; + + if (distSlot < DIST_MODEL_END) { + rep[0] |= rc.decodeReverseBitTree( + distSpecial[distSlot - DIST_MODEL_START]); + } else { + rep[0] |= rc.decodeDirectBits(limit - ALIGN_BITS) + << ALIGN_BITS; + rep[0] |= rc.decodeReverseBitTree(distAlign); + } + } + + return len; + } + + private int decodeRepMatch(int posState) throws IOException { + if (rc.decodeBit(isRep0, state.get()) == 0) { + if (rc.decodeBit(isRep0Long[state.get()], posState) == 0) { + state.updateShortRep(); + return 1; + } + } else { + int tmp; + + if (rc.decodeBit(isRep1, state.get()) == 0) { + tmp = rep[1]; + } else { + if (rc.decodeBit(isRep2, state.get()) == 0) { + tmp = rep[2]; + } else { + tmp = rep[3]; + rep[3] = rep[2]; + } + + rep[2] = rep[1]; + } + + rep[1] = rep[0]; + rep[0] = tmp; + } + + state.updateLongRep(); + + return repLenDecoder.decode(posState); + } + + + private class LiteralDecoder extends LiteralCoder { + LiteralSubdecoder[] subdecoders; + + LiteralDecoder(int lc, int lp) { + super(lc, lp); + + subdecoders = new LiteralSubdecoder[1 << (lc + lp)]; + for (int i = 0; i < subdecoders.length; ++i) + subdecoders[i] = new LiteralSubdecoder(); + } + + void reset() { + for (int i = 0; i < subdecoders.length; ++i) + subdecoders[i].reset(); + } + + void decode() throws IOException { + int i = getSubcoderIndex(lz.getByte(0), lz.getPos()); + subdecoders[i].decode(); + } + + + private class LiteralSubdecoder extends LiteralSubcoder { + void decode() throws IOException { + int symbol = 1; + + if (state.isLiteral()) { + do { + symbol = (symbol << 1) | rc.decodeBit(probs, symbol); + } while (symbol < 0x100); + + } else { + int matchByte = lz.getByte(rep[0]); + int offset = 0x100; + int matchBit; + int bit; + + do { + matchByte <<= 1; + matchBit = matchByte & offset; + bit = rc.decodeBit(probs, offset + matchBit + symbol); + symbol = (symbol << 1) | bit; + offset &= (0 - bit) ^ ~matchBit; + } while (symbol < 0x100); + } + + lz.putByte((byte)symbol); + state.updateLiteral(); + } + } + } + + + private class LengthDecoder extends LengthCoder { + int decode(int posState) throws IOException { + if (rc.decodeBit(choice, 0) == 0) + return rc.decodeBitTree(low[posState]) + MATCH_LEN_MIN; + + if (rc.decodeBit(choice, 1) == 0) + return rc.decodeBitTree(mid[posState]) + + MATCH_LEN_MIN + LOW_SYMBOLS; + + return rc.decodeBitTree(high) + + MATCH_LEN_MIN + LOW_SYMBOLS + MID_SYMBOLS; + } + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/lzma/State.java b/third_party/src/main/java/org/tukaani/xz/lzma/State.java new file mode 100644 index 00000000..43895ab0 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/lzma/State.java @@ -0,0 +1,65 @@ +/* + * State + * + * Authors: Lasse Collin + * Igor Pavlov + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.lzma; + +final class State { + static final int STATES = 12; + + private static final int LIT_STATES = 7; + + private static final int LIT_LIT = 0; + private static final int MATCH_LIT_LIT = 1; + private static final int REP_LIT_LIT = 2; + private static final int SHORTREP_LIT_LIT = 3; + private static final int MATCH_LIT = 4; + private static final int REP_LIT = 5; + private static final int SHORTREP_LIT = 6; + private static final int LIT_MATCH = 7; + private static final int LIT_LONGREP = 8; + private static final int LIT_SHORTREP = 9; + private static final int NONLIT_MATCH = 10; + private static final int NONLIT_REP = 11; + + private int state; + + void reset() { + state = LIT_LIT; + } + + int get() { + return state; + } + + void updateLiteral() { + if (state <= SHORTREP_LIT_LIT) + state = LIT_LIT; + else if (state <= LIT_SHORTREP) + state -= 3; + else + state -= 6; + } + + void updateMatch() { + state = state < LIT_STATES ? LIT_MATCH : NONLIT_MATCH; + } + + void updateLongRep() { + state = state < LIT_STATES ? LIT_LONGREP : NONLIT_REP; + } + + void updateShortRep() { + state = state < LIT_STATES ? LIT_SHORTREP : NONLIT_REP; + } + + boolean isLiteral() { + return state < LIT_STATES; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/package-info.java b/third_party/src/main/java/org/tukaani/xz/package-info.java new file mode 100644 index 00000000..8c6caea2 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/package-info.java @@ -0,0 +1,21 @@ +/** + * XZ data compression support. + *

      + * In the (very) long term, this aims to be a complete implementation of + * XZ data compression in Java. Currently only streamed decompression is + * supported. + *

      + * For the latest source code, see the + * home page of XZ in Java. + * + *

      Decompression notes

      + * + * If you are decompressing complete files and your application knows + * exactly how much uncompressed data there should be, it is still good + * to try reading one more byte by calling read() and checking + * that it returns -1. This way the decompressor will parse the + * file footers and verify the integrity checks, giving the caller more + * confidence that the uncompressed data is valid. (This advice seems to + * apply to java.util.zip.GZIPInputStream too.) + */ +package org.tukaani.xz; diff --git a/third_party/src/main/java/org/tukaani/xz/rangecoder/RangeCoder.java b/third_party/src/main/java/org/tukaani/xz/rangecoder/RangeCoder.java new file mode 100644 index 00000000..3ce4fa77 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/rangecoder/RangeCoder.java @@ -0,0 +1,25 @@ +/* + * RangeCoder + * + * Authors: Lasse Collin + * Igor Pavlov + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.rangecoder; + +public abstract class RangeCoder { + static final int SHIFT_BITS = 8; + static final int TOP_MASK = 0xFF000000; + static final int BIT_MODEL_TOTAL_BITS = 11; + static final int BIT_MODEL_TOTAL = 1 << BIT_MODEL_TOTAL_BITS; + static final short PROB_INIT = (short)(BIT_MODEL_TOTAL / 2); + static final int MOVE_BITS = 5; + + public static final void initProbs(short[] probs) { + for (int i = 0; i < probs.length; ++i) + probs[i] = PROB_INIT; + } +} diff --git a/third_party/src/main/java/org/tukaani/xz/rangecoder/RangeDecoder.java b/third_party/src/main/java/org/tukaani/xz/rangecoder/RangeDecoder.java new file mode 100644 index 00000000..f9ea4e56 --- /dev/null +++ b/third_party/src/main/java/org/tukaani/xz/rangecoder/RangeDecoder.java @@ -0,0 +1,129 @@ +/* + * RangeDecoder + * + * Authors: Lasse Collin + * Igor Pavlov + * + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + +package org.tukaani.xz.rangecoder; + +import java.io.DataInputStream; +import java.io.IOException; +import org.tukaani.xz.CorruptedInputException; + +public final class RangeDecoder extends RangeCoder { + private static final int INIT_SIZE = 5; + + private final byte[] buf; + private int pos = 0; + private int end = 0; + + private int range = 0; + private int code = 0; + + public RangeDecoder(int inputSizeMax) { + buf = new byte[inputSizeMax - INIT_SIZE]; + } + + public void prepareInputBuffer(DataInputStream in, int len) + throws IOException { + if (len < INIT_SIZE) + throw new CorruptedInputException(); + + if (in.readUnsignedByte() != 0x00) + throw new CorruptedInputException(); + + code = in.readInt(); + range = 0xFFFFFFFF; + + pos = 0; + end = len - INIT_SIZE; + in.readFully(buf, 0, end); + } + + public boolean isInBufferOK() { + return pos <= end; + } + + public boolean isFinished() { + return pos == end && code == 0; + } + + public void normalize() throws IOException { + if ((range & TOP_MASK) == 0) { + try { + // If the input is corrupt, this might throw + // ArrayIndexOutOfBoundsException. + code = (code << SHIFT_BITS) | (buf[pos++] & 0xFF); + range <<= SHIFT_BITS; + } catch (ArrayIndexOutOfBoundsException e) { + throw new CorruptedInputException(); + } + } + } + + public int decodeBit(short[] probs, int index) throws IOException { + normalize(); + + int prob = probs[index]; + int bound = (range >>> BIT_MODEL_TOTAL_BITS) * prob; + int bit; + + // Compare code and bound as if they were unsigned 32-bit integers. + if ((code ^ 0x80000000) < (bound ^ 0x80000000)) { + range = bound; + probs[index] = (short)( + prob + ((BIT_MODEL_TOTAL - prob) >>> MOVE_BITS)); + bit = 0; + } else { + range -= bound; + code -= bound; + probs[index] = (short)(prob - (prob >>> MOVE_BITS)); + bit = 1; + } + + return bit; + } + + public int decodeBitTree(short[] probs) throws IOException { + int symbol = 1; + + do { + symbol = (symbol << 1) | decodeBit(probs, symbol); + } while (symbol < probs.length); + + return symbol - probs.length; + } + + public int decodeReverseBitTree(short[] probs) throws IOException { + int symbol = 1; + int i = 0; + int result = 0; + + do { + int bit = decodeBit(probs, symbol); + symbol = (symbol << 1) | bit; + result |= bit << i++; + } while (symbol < probs.length); + + return result; + } + + public int decodeDirectBits(int count) throws IOException { + int result = 0; + + do { + normalize(); + + range >>>= 1; + int t = (code - range) >>> 31; + code -= range & (t - 1); + result = (result << 1) | (1 - t); + } while (--count != 0); + + return result; + } +}

      + This is a game where you explore more or less random and obscure websites around the Internet, based on the + database of the Marginalia Search Engine. +

    }rPooYX?gz0j6cmKu zAGjqb@0CIaQFVs@toYs9dW*@LSMT^zX-)ssk-M(W-@AVO-u$cY5}wBAOV+Ks1fQM8 z(js=E*rCyQqEo*MxL!Z;x9;NMU)*=!FNTY||8^qnH`=vQ4T(m)mm7UkJGkN4Ad(2t zF#uIC`H{BPfM3B6M;fXs@nT9;y+Au*I2U=UImO`C!35(7y`{H9(M-(e=G=pU5c(FN zGMd_x3_X_KWIEJSO5vUDZ3V$C9=xle*VEfv-{&lJY#Ca)D=^e_{lJls+dMTm{pt$! z^s7Jou=k^pm8%+C`YKv^t$UQSw-A_(QQRr-j{z-HM5>?TL+bxT)u)`t2*Bi9X8*I8{=gT2^YW^uRp8&=lY ztln5ST3g*A-j=dN%IPIyuN#jpZEcLX1VeRC^`eb6<|1RhQ)|lCIimH|y@7IXSE#B! zEWSD0f+?guvkvQ}Lp!f>u990E>xZ*SprY^mh~JlI0ymU83+%LLf&%p%8OM&P5IF}2 z1Kd$3%g9!6z^ zY6U+ew;`#8Cr1g${}4hJQKB1t;lbdj8i`j$-7ess3Un%(K*c3~3-%4af`U?0Mxb-c zp}B@k#Pk;QVeth$Qt3-Zd6Mz6jnj#el6*ifw4l{9+s@&?_i9OP)tZ6uqN~R)w_9z& z8n?eQuy2<&PjBdAmZ^#96%_tHy@GsTzaSLWEo)!8v$4foTv+7nYP@Jz$)kd$_|aKb z6r`ovG%>7Y0p~}x{21*Kh(S0K5p+CKS6fmH!9qW!L2J zWYy%OQ_oDlzrs5G{^GvQuF{Cx*;?7q${#D$4eT3YF897IJ1!r-Zs_XIUL@W&%ObqK zXoI7%s%ukpDM?3Kk091Vk7iLpt~nN=THGkghYDl?e|{cT!&T}ipz{M;=$HhX%cai9 zU9`;ZF$*X!#AGXo(&uI+x4^%&?B$<3T~FKxQ$EQBQSL@s!;lA0a zNw_FIe;uB`43U%V++t+OwZ)<PUo6XTb;I%^`$ z*7`NkhGjLyEw$~v&Z(y9msbeWFF*C0%9{Sx+PTB(-?=qHZdM;be%E~U?y8W`@>xK_M zzJ?XgvMOv~yeW3k0mLcNIw;@3I+)Q1j%2|oC(1f}VHH#H*wrY`GO{Gvj4jl^sH+oO zD6U|~uT$}sjO(!(hbFZBAbja4)+-!BDhE8}sNqU2ff+{K^Hw5_+(*tAky#*9<1F3$9w+%}91uR~4C$aNW%Ih>M7pfX6 zzFZs~@9Z9LE^6Ajq;E&t=0!c-{r$1-esT58I-~OY(==wCu>%{Ln>Jk4J9*cNrFZT9 z=v^PY=dQc%II%!oQrhpzWuP;4NQok9bu_lbXF{D+uTd)1+1HB$R2c4PZ)yaIHBv|( zEjoWxi_{_p!8aj&fVQlJVC0sCLs&Gkq%`Ybw&YZr7rb1Vnl#&+zDj2crUi!5JH1Mo ztKrQ2hdESjSsqCB#N4Px)d#M=EOtpPW15;glYmmLNV*b6mW{Tx;w)G^*b^g3s=?}@ zPBr$X0wLyybT~cnKZ1RsY)Be7>hX*t^Z+X91b(Qbl~a)_-qc7jHGZGh!?|UpTt5$` ze8H=!xN#OuUF=I=+Ek-Od3<_lXV%;wUf?3jd+;&r!3fT%H5|~K*xp{I()FO!IRbJ< zwrpNH0+oI}5}-*t)d6iw&Baeq@wI6xK6dK_Vn;y;O5IIn8!jHCutBpWuJaQp_a1b9 zVWBL~i}N%DE}4Y@*G&^(o_4|1D+?q?T=sth^w7mMhZu)=f+FNjcNX$Um!|_yRv!de zeIS!T1CxO|P)V^Edc?QGSWqdro!H@sVV5>KgNVRy%Q`wbVjbOOE)o=y89^jSF(Ved zp(Wcw@u7rmAv$JR!4FQaV5+V0zYAHVxbXq)SKhmKVXeH9^jyjPB<4gopt_E6Iz6@?F#+y@K9fG(h})MNQNF!&T3TS0@9pfWaKnU5I;B}MQQLK<_p!Eo7;R@C=)2!XSHd>$yq43ZJKhk*f>+SEQFFEVSK+o3mO){ zE^bBq?P~Hu2MAB_!!(5XgJA9cMRm0h@?D*z@(fY8S(rbl27*{54zWl`97304I0gi! zoq*&=_^d*J(-cj)I5Yr*Hn?iV$Z%yvu-xmWZY;8Gq@UR~TIkBA8%&CcEQ9HU_>EZ> z_s;3Xoy&llI=GM(PPM2K{Bjg)t;Z2Z=+$^^RWVFV9(~!UoF5oQLa6ify*g zU8s`u?Jrv2Ic{l;~8agwGG# z6A475{Av^odr-}D>IsM*ER?{baDkdE5;eDc%ciw!NHKHPc&VJ(>;~P!h44&Pp5p3+ zmDglTwerOJS(f$2>1AD@)%VSX?9Lg?z_?8KHt3O%2`fqS3t*4>sl*fN7vKz(WCD|A z0zU*Xj>8Ul3`#h2K^Mpf0kyx;(o4zUQP*JiYdB=FWbRsyqr%X+L_=Qg4k^B&dIiG-R zAR7g{I9U zdZ+y*q40l8@jv`P;^7nC1Ia<~h2#?wn&|*-Ht+|atJiRMKP@{7WY<7+S_ju=x&}}# z46XqUxdsp@4HX=}O}hwMQb%etTm-WXorsyLD}4!do3N5O)op9)+h0GcCjDGuIc8Ip zpG~fgfS#zY^S9x+BJ978b}k`s@Rmq*M-o^+RT4%89K!k$@d8&0Ed^<~66Rl99S-_D zB}KAstC>-^mBYFc*nS#G@6A$MAMKik^O-OCR=I%vo+NnRka#g!?)gA->N`{utCSIZ z^xmN!BIFT;J%xjrd>7ni;8_X|(he#lIeb%9$h4(@CRID!m3*dYXntyXWl85PJNG7k zjK2xGT@|_kE#hwbN;5!s_?+UIVs0o20#Ih4?)|bTQoC&a|v9G7Iy}mXWsDXoJE?hq~s3;A)XQ_Mf3%NDCSF_-H z7EP?yY{2~`xxjgGYl5gSwgk=vUcN}$uf)yRuQEzcuru&UrKETzae0*hZJP4zux}HG z8pl;v1_Pe5)cxwniP5Kq=cK;&NVY09kU-wqUlK2x3xhuy$Js?^$~!4=M!%(~L=R0F zbMNAQ7}LpMyAE$9qwh6|Y_lXnj$CO@H<5*t!%YPzL>wq@g{Vyc<#MiCHc+l5pd2|0 z2qmP0h-?O$Ahy6eVuR(A<0FZkQ!3i5N>@&GHf47Sm(HYFWnks}4-;!XgZh+(jwf{x zf%GHc(~^D^2GTne8%Utwdgw?=+*^+m4oO9N8T5vhgvNu26VmH8a|lyfjuI7UJemZ+ z3D?T0Fy%C^X49fF0q+^Lsw{*%`HG+>-cg~K^sFYXhw4*gBH5YPHO_D}1lLbv*Ls9Q zLi>s%*!2FY)c5B&)TTf_b|#E%oQFP^b4`*qDnc@Z&RLYSOxT)#G_eh!gG zLwH5f#voUC+V2rDG2@KjOsdq^X8U;wHs;_}ju1QkQqpE=!cr+!jUobp6 zg^k!(=BWo};LrS##7fRnEzCxor`MHa+`GbClAc&5X&M*B))TVqnMRf~uw)bnAcfID z3Zp@#HxQHz`&+NV(T#+K%?MoAFL%31`EdK)z7$xQR&q{QIa8|?MiN?OCXCD~a;ceF z!0`_<6;I;3H-r};TL>fW<9-%1r6a_$3W~xFIWb{Vrao$t^wA`gYa~qA#2I-Lk~2~i z)6p5{gXbzOkY!#IA{nC>g@dzjVoJuCQ*mW;$$0+FbZxf0HZ9gA6Bp+9CHCZO>TMPh zoW2iVhfYuWE@8g?+^*R66vP)b`cbhz8{hKGMKJF#iF2<|S`bXCqAXal6=m_+bjzE4V7{7jHllkXTa}j258*KOF=4k=)YUNsxrMI| z6&h2qTIknW(yz4&E)I#qZ$ywdx0!m-bIU8k60Sgqtz`bB2FlE}VT7Vo9Xu1EzauJXH>k-6ZVMpUX4HzKm`$@xT4o%c9H=EkH^EIQhRJ)q zGkC%7at#gk^`v6MhV#UR)1>ovX0ljj!-DzWCWO`u=F2STZ^De^xsG$M@p|CtrHJPN z3THGx48s?R;57m~5Tm#o{(Hcxz!(q>jha7z8G8f|lK3BLb?68~jbQ-Rd zm1@vMm7!lxv__z?_hhm%U^pp=(~;;zjZkRji7s=gczfkQV_Cnu*t@=C_f?l&+rBH- zy0d@9hRNY&>&JP0RXOh;?h52v3Pa7$eCvttM=xKy^5Ec>&7;fLZW>#PmT^4SgLh~K zrBmNQ@&D@`qV22QYQCU)xY<4ADf4gXy7b5mhx+#QbzU+uzGd68)tfi)!I}_XKHM3| zx4NpLCx7_OAGcq#as0^eo^2~ut>3nC>>{$9OU}K;elFSa=t<61#zL-As|9hiNG1an zaDdDDXA3k2m2#-A z&Dp-8JGP;%q+{=J|K2X~Ce+{WY3v(7&4h;{TiHt1j!_)DxTRD(ky^aA<#@cs0`-dK>!|GdnN7 zW$bI{8D?wy5AEn^-FkRv-~D6DKlYXHtY3Zi1OBpd&v(|Xxt9a^Ew z)7MJl51rxIm(|XBkN@&>J6W^puWuF@oYMFkPakLBReyNi3>5Y)lAdNqM zI*k|MlWtwpjhk*ISKK()im>eT4l+b7Oow=mjq=3XP5vI6Mgv$tUP%1@-K{ zv-{Zcmrs4f^!FCfE(Y4KNYWm_IC*$EX)Mtm5s-1(TaguE_PzBqvusq_D&F}NE)9b2 zTat92Wh|4%GX5D>vPpSM)!SdhV`KaX=zdv}?sJbfon(QnDz4_Olk)hLp!?w@-RBp=GpCh0!+`0#VAd8g{H4R8EG9={oMe>h3^xyLvEf_3jvZ)$w~ zO?muI(EW)d-RBYd~xepR}HwGap}B1=h)xIeDSPHm!F%z=N!NCN882c&z#Vm%gx_&j$ilOPVvWQ8uWk7&EIp5-~5X`;&)Cz zYxwWn{5|LRoiFYczk0gg_=6ev8|A_>b8-^=4eB*T*s^`%6EB1K))fB!3S_&QD~>tT z3M3xj8Ib%hZ6bESYUY98pi;hIw))tm%+&Vkt8L=zibkdre|_q2;)_fx?*lsDe+)T4 zuX!$K&h~fSX&3*Xcz_j)KRM+Rf5eLAd5G_S3wb}+Jj~2|>`GSADV|}^nDzkFr*p=Y}(|@gV=_HO~#K zzeoH%bM=VNC|_Vh;>X_plK2rs3S*ulr;qc6I0xo6&k?q=SNtWb=oLS&YG)JT&2Rky zI^|lL=cdznd=<`vdChYZ+te@qkhS!S534@Kc8gcPWf!ktyJ?}wA_Bz|V&*R@9B?x56D7(RNA}eb&pZ8 zn!Rx1gn0MXGgs)xu@Jx71!dq8jEC#C&-UiX+3J?y2=eNH^S{S0StHoUo5|5f@U z-vjy;f2p*`&+8tSigxzulTV5Vcb`6PG&H`k!SD$Ek?#RLj6Wpp`}4ZTAyLELed;Ok z{TH9kGfp+XeyQ;;`Xk>1`Wk9& z{-78j0r5Y`WZOw18giS`+aaT6>n)2xnj&UcV*(6$zDQhk-+k`C)xM6fG=va zzpr`gU&I)g|4#74mN-wG&-`1&>Jul}3){gHb-L&3-eSZQnE!t8!!>b!IG_2i5j~&# z9D8Xe_@P1n)rL0?B-3?x7G(6JyMu?aJ^M46^@OYdL z&S(C|Ma@%Bv3K7GK4>=H)eJlWjDY#S2_E=voCnTl{_jqm`P$cb<)z?(7Sr`DuPq@a z!1*6N`y2MGvKujbWicm|Lm^8oQE@5@sU=$8$C?;>R(yd~SBjru>y+JRo;&lL!e6ZL z6E88Y9#Ne*z4P==eBhoRSV?rrh0J^dsba~>a|m^DbKl;=j6Hi zPv3+nvU}z~*NC`grQhs}6Wor0rq77Gv-1(irDI*Di5{JrxnH?C%=m zDRzpf{^3>qL!N61_Q_GC*v)tC`J-YJ`$-dtM$OT8Z+`b?^>)8{yF8a4d*U%^PZY-N zgiB{zI*dh_9hSv%@r6pZPW((IGb!rN{JQweukl`9?1NuR`(V!T*NdO7Ww(k~)H0LO zbNU~y)BljhU%}pzXg}xp4~ciyvPZ-XH2%KR2VAEQV7wdjHcIrKbNmVM>UwPHMfJ?2 z$~(>6r}3^_8E9NC(Rj}B-w?M%*w4kP2*%%Z>K5;*Tcq)Muo;N1bB=#O9Eq^Mi&h$c z?v%xQ%7XDO&~mLr%Q?sYMQm!~l~ZS$m`Q!*yZ8Iwy&p0IJk9@7qMwm;K7$_N+YZ3{%&CoPqYhz7;Bi%w$9)WxZDRZ{#phW~6|wq1axr}h_w!ua+3fL?i!h#p{1OgJ`}w@?$xn+l zc0|029nX|-)1r>&Am@a~B_24xdrpf#cDJ~dJ=l;T38zIOPx4UsrNlSqch6}t$v%-w zVor-ro`c*(VjRiM^SkG?IAveRC0VCMEYJDCpC{w|2``bJGWbZwlAz^|qPYt2B#YH7 zIsD>sHYskOuOe|<#Sdl)UPXOE0uyie&Ti-b66eQ--D8LNjcT@EyrY`GSbSFg1mXVJ zneEOq+ZB(+Kj5I?NzM!AIRDM>9stPb*xh~Gc_dtYm)L`=}8;}(mg!jx0X1+ z&F>zGqv>w(&N^_{#ybA#tV2S1U*fc&_rQ4}Ovd?Ze)mYc$En2e(JWq-agM0+5(kR( z9&Yf{u{b|1=pI68;)v-Iw>HpO(~vxCR5v9K9qArr;GfUO`DcFjNUT-z7!tf$2a@Vs z;$)(=!r3JJJkBo*yGQb<5(cvlE%lX&(~IsAg8uO~Y_d{BTch|+1;rka8Ai2M86p+X z?+Tp&shpC4h22My2vUDas~K4(a@97udK-024J~Ri znvGW-zG=&rn+{)vk4-OMug}+`{F=YTjSv6oQ1y;2ef+cccVByhqT-!5DwH=|)BV6_ zA747wd)A{-7C+K<&nG?>TfM0vUzHcxv^w^&Pu$Z^1~Si8g8vM#`^peOhzIYTMF?6)YWtdBZOR@2qD-oA6^?YCZykB%r~gWY%`PkS)f?!kxde|9Oo zgKZPv{`S8Oe&9A`)jR4c#lqF|+0S58c}}cSX{mFeF#Z?Yj4&@3+ic zDUGN2@If{RIWxdC(D9Uof?qzK;uFSGHL)mkbkd^a3hOI~{M@qsxzyJRJwxl3eS7tJ1c z{Kc0SH}%I3S1JEs*f?*>qrZ7_uy;vgM61w78W;BsKKYwRyXS4B>r@6gqZ;yUfN7%R zD*xw>OBzphTsXFs)i&5nHdE!cRX40#cf+b}l{m3A5aV#OTCZ1Iqe;I%8MV^m=7TMK z;c(l!gCD#wW9*6nS=pzhW&FSu3ue6d{zvQDMhZTELiLn!$KvK&?z{hcozqsgMs*5p ztabIY&hOoS->uDyFQ8*ffDi76Y#m^l>DVg1;@H@X7f}fmD#+T0S+D2sK@6KCt z%?MxdNk=(9a?O(Y?~W7MwyxQwpAV>ir0HDK(RttFPd?l?Yh!1=UZKzL+&HW6;U}NC zx3gnSC&pL>UVRDV`w;y0FviMA_{;ZY$+3OOvJ!rn93zJmPNpv$V^wRj)#A2zTDQ#p z?z!iFclMT64^FMkB+%UD(U?sdTYhaQRGV+pn9Lea*M}YQiiLfb5*INZS~|FMewD29 zY3Tn$9+h)oXRPZyUR3`}TXrwza!)YH1?{9X%ec#jLd#)<+`sg?6pk zqV@Frxl3NPa%e9pCsCqaJMGdXwS3jz&r0&OOD>(ZmZ(XLl<%FoB6Vnq_G`Ln=l2ai z`=@tb9$k9zKnbu<$-u=+M_+#T?PsU;pFfq3y*{f8q+>5l7h#Svsd50^ui9e-DkIS^z?LCaNzE^aOwRQ zUGz#%zei`a>KsLlu~=h~gC2YO4_(VQt^E3o&BRCQNaxKn*44_p<+W!|znOH9dg420 z#@AOi@zsC3P_Elxn7M0cbpNOScz0~|?rCL4g|Tef?$u-O{o~X9qeHu3zQTCNz$2C( zqk+kMN&~UPh{=ZGk~$He;94?41Bq0CS|W5P>OvKiQk`=Nt#U{&naR(W$w1Wy#=8P3K;~7bvs^vJ1{@x=|v7hhKHwQI{ct3BBAXUJe}XYlZ1-O5uE!4-OpXA@}P%40OJp_J0V zFBy+S19QeRIUu@1(xMz25Rw2U=}x7~f&r~OB@q<=aS{>iBPD5BtiNgT%OHcZF9I1< zz5_D2a}LPhOG}#iqn{rH5$vW!uy1je2rlWRL~zQyjRtHtjBPvU;3dcCU{l#Yb8OO> zvSY(B#&$`T5T3W162jHzWeB18ms1eJVN%+{7mT#6`{?}_XDqm4fG<-$S0)>{Vr<3> z9~@lQHeC4mIi?F1H{Y5igfUd^iDn7mEzOH}7&GI$6twX6W3;fj{Hu-6B-Ok-Zt>*s zWG6Zq+97+)9Y|aqG7I8l%h#-QK1QEp1)74DMJNN`hqWG@((V?XS z7tX8V6PkaklvT~&Ik@zY_!seAki?2Zmt_dz3nvl8=Ou!u!=AkX^zgs3^e}~-oeAJZ z4)l;ZG*h{N0Xc+=xfEIg@j;Rps52H4umW2aWdgY+RO6C>F?ZCeFCO%NorV;OFHuqm z@;6BeK^8qd9i*a*Ppz1`SA14{Rs1wV43pY#RAq>v_^J4s`0U=Hl~spK`l;vlPkZ+5 zlStv8o*nKxf0~ZM>K&kkhhP)P@;qO4jlc3>4;@GSj!{MNx2Gh8JtP6bw{FJG;!nh1 zh;MC{2wzIKy;`D==ZJ5KPm23)o_==if8RJk2;V!75Wbrwga(YY@yLEw23<)W;^Pz# zmiRRcTTw_#H~^gq_|j%WhIIj;Xf&8?$ia=XSgJ$@9Ll9)$W>{z0CLGk_2;dfkx0x~ zdtN=S;Z0?FPh}ub>Cu;&c+D=EIqbLVbasE(EQ|IuL?R76NA?%(xc@^^v~M3NI`HG2 zMS@mgNLk817dMNai-*O{pO>4fje_=JF0uZ~hYsFw!$%M8UZ3D;$`{bw#G9i5r%vY#M9qAxKOK#x`;ROw+w%*gM(KaDNT{!+VJ8*Eemi;hG=(tN6^%9uuGW>w}pwOga1! zTMivh0CKG2!Lnd>{=Z-h(h5wD0!u>|!DM6YjTdg7UtK+a^M#FqR;~@?sl8Op_Nwy& zINc++#6m8;-W7^jy$?w(`KBn6Ye?;f->bomy5bU3*=yp%;xthd$MOE~Yh|XABK<2|+qFOa zl(hc-Mbh@kPp@s``u`yA7q5KpJ#n|V{}0kwhG93I2c1<2*_#y)R)mkO3*Ae{_Ezp# zq?I|ga>(B>u{O==%XThpXjr=Qvb3NVbm5@dRuZ+_qa`+VFsu{w*9g{vsMlcdMhmP$ z;f$`LqOKW-nT7{MGE5#O7U=XICd1+b4T46YFRL&oJ`!&bOT`cHDiyE)C}FNBGYFb- zuKTv9QEzj>`|p$aMD_G--Q37QV2HC09~Re$PacrQH4FCIjnI>Ypl47#SeYGHG^zrO zxY$XM$(+&!lFK;?G!jTjS(GzBvD2Hc+_kEyY1OVPn-v&RB&4yG#vP7$sZA4#U`RIz z$nxp6YBW}FAYu}VM|+BkdqzLMwD}2QAZL-+NFiRY!IjVOM6;j~jFnZ^lz3D;UsQ>| z7hlBtHt}f6QdMabG(^X>?)l#Dh(P+Df1dOb;dkHL)5^^`%3$mDi+{tbPyB&oKV+Ht zpBvaKpreaG-=KJ~DhfJA@fpI&r15=)0j^AK-n0U>ONUl$+MH4t75Z3MV=v2hI`hly zns7|7Fy17X5wc^`YHhwy#4MD|?Jq6upZmF?_O{oEn=B*yhz75plBLA++HJKMq^Y{r zRx5reE*O7Dd`$e=_`3+MJy2(>O_?!BKi7HP18?BQrEk53TU>8Ea9t<2Nc>3rrdTY# zj#rVmSA;c^WfmX0j=dE+!hp0lD+|1OI!5`iH4}2i3k=8s8|Ew>jVHn`t+rPbxLgGl zc1<*{4_2SMb@gyr+3@PE=T-;x@#tq-dtNwZR+KInDlZ>eP^vJ;!g+SB@KmGtve-KQ zxOlgC_xNK7{eQX9-dJaGHU8jzQbH~uuaG>vUd2(#`}Z`s%=Hb2%KlEzKKj}1z1#}W z;qyeT_$$1$;<@5$E4bdbb8;E2P(UvGyxfv9 zD6~S+&L1BTU);A(eDTBkcU(iM53$^RH?h2YTP^6)?uS0Q;f8|`UAbQB_fpnANrxu- zP4k&R*MVLmXo;m`@UbS^ZB{NnuHV_Q%yek|K9x5a3MIWNpI-}FG)b6RCTLHLw?s1Z zC~{<}TN8=ts)&<3LIQX_L|jRIG=lm$0tgf+>3z>VC;nLc{1JK*(T=w3rBzzp;M1F5hj*nR3bcnRfg~&Q$7ffFiDnL z<@tk@?hNK1qeX{5qG&o^V5lS2Y~m;TK*Y(}?n^E>NVq-xOba{BiNsH-8}cBCt#|Xi)7W4VoB>v=-ULh@N6KPBE4_ zZfz)_njkx>K!|o|k}S1P(4B%~wCM0TZha9YIAjXB2Z4Vq-ltH4Qw;JcfINx66jy-| ztrmaz;rAW|%^^Xu@xAxR1`_;3Kcz^Yi2wfL@5M(y{pqzC-XIMc)ICXqN+XWkbveao z=vJe9D|a-LTW+qPQ=CY66gpv&Fm+DQox)?Z=r9AMWsnFC(Ajguj@NT!sYGz}3XK2) z1r$6580`Swe;_{aG&gbp%9y=}50h_@x`QK>B8i`gx4!?rc#HVi(^)Y zTrMDL)da-YY?)I!sO%4Pvq?~-gP)&Xp^Jpo_7X~XO6=-zLfE^#sXLM<~fr zafzyZ8xS9pd%k zJFngLS8gG+rEAEGa7_r27szU2S|kx9@U%C)^%gFF2LvgzFPlMsw&&0vGf(j}a2zn) z#FzvL=QHF7n$kP~*<0&5o5|ma zap+&bq7bGGevk_O0p$Oa-*3nWAS|Y7#&LzK9P0uJ9FEq0{GKnUlhS%JiuzS=$8uCg_f-= zXSLgl6#j~~nQK~F*34|H@GFY!?Xy;H{r&p3=fQxb$nbsgg$E%3{NuNy59y;A0lV}j9&?6_C3hrAN_5+O^z8P*&qxiR zbw$-#6MqdB>+n*=QpnM-;p92-zy|qm$sSx%^BLM~1b+G&bUL|hx;RX2PLo_? z_hnm`^gGLy;hO#>+q%2AE$OcbE6bhzOSWG2n~O(~`*;X&bocB0dE!grPsIlzWPEUz zVvYDd4j#l8FP+D~PVUAfD1VDKDZaP``V`uxRe^nD&W??XC95VH|6$+G6+?AVWrb^K z#r8dYeS5aA7;;r8qjf_ocJ>j~CbaY;@n-Q(@#h!w2grYtTS1XrvoD_w%g-{L{O1Aw zBJm0Qd^0Y4X_K^HO99WX<-#-7!Lh(hGH4QIC=*9@7q7K$nE>czX$sIm1qJ9Wq@F^w zc;F<677tK}rhBFcFnur=rm1b<_y$Zia_}@jO_~7JkX?Rt5>$gZ{@V+9ejG5(Hm=A4 zYSK=j8YJacr+{jVsT&a)A6X#7R6NHkCP=3OKAB%7i(i3)x5m%~8k(Gx96(6F@d?*9X|H&4umBc3m7=h49N|8?DU+ z?hS2i8*+e~bOXAfCiszf6`&iugA^&DLeV#z{a84!$$=N4+z{&{08l6B-*L zW1u>X7x4$gYKX|!i|+!^!H8Uslj;Nf#gGza1nk| z%mR#;9L%&xZ_CP&?-sdJ1ynLZT_T2`fe)VAF?nV{q0sBJsAHOq%%PcYc|_*W=p2#^ zSV_PdV5g6N&dS9qvPWEh?C(kL4zZhDaOCMb?<70ml#<&I1-#viOm#YGH0V*SSdgQZ z3Q~?v^a{3Q$nl5`LWC=Nfusd8nN}lH$P~2Pr9^XJFLcwShzav8iehmfX z6{*F$`ule+PECB)4DFsdbN5i@{oJzQs;ZH)vXQE);j-G!n%d6J+L}(H8@Oa)Dz)&E zfr%SuUO9E@l`}`_ZKKuGN=l|xS4}G|omN%b)m2;9)rB$Tti2K|pPvR)(fF&$smA0U zmmR`tF&e0Q0$pPvCSaKfe0c)K%mmkPPVrWX73vx zq4W5^YLgeRq7XG=c%Rr;NJ+Q_&2Hj2gqF=mquAi%BWUg>(txH>oruk8Q^z!?8-eCz z3M9D~2ugF&!P0!I)JD(3Xuk|aP3SJi$HXp|$z(d2&S-%;f)95*}o z`UAJ#dO%#C*;zXVSFIkTyZeN;k-uC@w*0r6%d;gmXe>xCphwcoh>((?*S+c#_zkWm-FjsZtY4-BF?;e+8 z2accbx^y*dmGn|m0zAA~*$zxEM$Z7jAZNR522I7hfTm&|%uY1QaAjIHW2_M&hKA96 z$T-N#$3G{V(CS@e&t2@BN9O$L?z`E`k37v)(Ux;@hyeJ_qic=TrIpo?6dtL&<$zse zDCT7R3goxMZUipN8Me>ku$c@RFmo~iV^^XGVrH!a##ANh%+SJjfYnSv%*^rRWbE-f zU3qyfr+?yea$+9Ke*a1K6Fq1?`U!cB?UFK%$AD$Z(#6<|$QLhVPe#8mImn_6t02vi zOvb2Ha41awMWP{!UC?ihE7FTj! z30IavpL(X_h@2^wJl$Bn7(4;8^(Ppa^+f{gL@lI=9;27EC_tx*es(#bT`jwmNuiE|O z62)EDkjum^7wcT`07H-MSpLpABd?kLL2#QtxXS=XF`k@SCEk zusG!IK;fQCfGH3Ug;ZG7(`FXJCzvth$0E!SVLWb!&8SCU1;TAeo`5`9k)Tx983ZOJ z^$S}E(h(&!0>Da+`X3Eb+;L}7e!)PjKGYp=?2^vngYo)s&pYJ_Ys6zzlkbWvm0Fxc z-A1)oMjlg2_FnJNuaOG!Dk!#<(n>X`vj)(OzM^L=%`%M53_OdZT{0OzgW-AEe6SO; z0TeO|8PT|>Fh%`u2Z?4 z{>b&I1v^y)F_23JuK66eo`l~?aHVUo^5_%fD(()HH}Ixim|ME^7zWMC zfa!+^JTMeZlN3qBFrOhVL0V;8&+v1D=U1_RI|9289lLl9Q_pS2IvLYCoYR~f%sJ39 z(o_ua{zWk{@fz7%bj`{A;x*(ir@5BUDCfUST+A|B;Eqq(C!ni{Gjr0jDF3BY2yz9- z8VS!uBQQt3eDp-YMI43s2s!-vc6nC_-iga)9;7vEN{_c{Rp`x1EZ8g0$ zjlrgXzfRLo&I*ny$F6l1RTZJ>-bmfb&S{$(+7`|2nIG`~x7&s(V93z}M?c{MtOZ`x zjp@25raVSVYO=k4m(^_INjJrTSO5%!?j@S`o~AdUdx?<^BojsXk)YF0`M+480;+fT zDurUy&JN1~hfM`DyddTIN@aJxT+nf$fQ99{H?7*xWo>9{w8~5^0|UO0GoMFJFG1zD zjch7f@%TKWMHJ!hx9!dVIA9wb zCtw>1gJ7$|X@1YblKUA$pi zqc#L#D_!P|M!YljUVZ!0yhUe~H+L&Ia|FdTD_Q;Z{=B?|SFL$j>(j8}V+G~bWQE6R zR2vFHaxPeyhso%X^J=xf#HZubYC~gJMQgyNT}9`GJ6aAn`K|0;N&4_Hed%rkifd?5 zI)!38f-F@Gicts*Po)qr-PbLfLj8=_|-4@%uJyyhP?P zS$y$eah30cKHHgv>G_O`SMn;9P=b6Q7i2rKJ=YRmg`R_YC>0o$5~9?x&TBHA*Jx<> zHFk|1)Z7fV0}y@OK=daJM6lAr<-nK#%ZU;AE{|M(dE~^Nf1iGNxn$dsF-1oYaNlGf zU<#N{riYnMylG>fh85V}XoQm~i-?NhvYBGLbwtByD6&KHePIU!g}hb)6rz@^DcJ$z zjO-%<(&Gw9xG&SvA{1JhQC*89qblemW-%(29F)POPah^b z#lzD7#GWCUrmL%KWLj5GS5Nnpj<$4TC>+5nR1nH!hL-3Mb$3i98Fojgs16|Er!ND3pl9D4Ot|w~V*$^(NDDUiY#G^qiVS@(yKx=EgTB9{74ADxj zjooB&b~W`+UpS+_IqWd2ZLW$`LpoNrR-9f*?h@0%mV$uGP(+HXdDZd4ghwZjRm6P$ zumwrm8XF&U>QpL2mHSRzS>1}^1!pJ15qHGh+1fqJ%`O_RX8%*%dy&!_4R`~f+?0Q# zd*UK#Ps~Eyw&mn^nbfG(`5IQiHWIaB9^D-3=|m&Qdb1@NCVx~;{HUcrrYFBY?xu+X z$%96s=k#<>LGV)SV00SYvAv|0DUh6BX24EDoh+nGRj8IR2J8a40lPphXyh|sOVdat zcQj*K^hPsLAY$qlJy(I8w+c1(D19=60uhWd_rZdeJ`%BY8o3tOMbL~f*(cLI+w({&C1r*^itwlt>;f~fpVx3Nh| zaDK+y7>?N3qtyRZimae4Vj`8WBStB+o$Zs-24xG(Uv+1bF^8_aBor-7Bx|Qs1&Xz- z#!6heL?9SSmL!@Bn!}N}kuw`F`${|gs^($`N_06~B_&nW2Dy1#O+3%;_1c}OvUsUg zFPPc?IBDM>J<3!b{gm6!?qi}dnv?{@MasW}I!oa4HnCU3n=H*NPGhI>^4cgX@P!4e zQb?D?a9+eX&+4BXIu^gvr`%H1STeib+jIpx`|oMR7P$l;Ezb(uZmvrbJr$hRbr3VC$3&T#A0n|*qh{y<}) z$NW2bB)$;Tk)D$Y23br22U|9B^iy^fa_*Kg^V4%^z6GiuR4U{Or5pu-WeWK$6)jbb z+J?upSm0h!L@j6YkEye=X?`pcLgk6 zA|t8dEW_+hC_YH}!kzh!g5~Y)D;u~qC1obR-&9tzhF`a=Z1H;kH|Bd;LDAJ&hO(vxwiijjQ94>hOT_KcC|S7{vX_n)_|P#1R%cjY(qqOX zk!6{5$TDL_2&l|ZLp@>ITkE?Ux|500lH#Jmf@qk|i_1ZoK*)4}35a5Kq|Th383^ih zGB7EW-=`8Y-84aYIo(Ca9fLni94D$#tWGZ#j5b)j(j%r@l>YF$0iQ55WH)Qg7dvm! zdo_lguDkByT>TXlJq3-cE6ZwJmddhdgHBhb@~K!x?DXboyk2ddhx{aF%hRy#(D~}1 zne~LukL~d%YZf=pI=3;uYIdq@Bw1(@|7`RDb_`Rp z>kP}ZYL?f?;8D**%K$~5qzB;^V)JoR-RYiFJPiEDj{nKi0NGnMtys2T{=B)g*7aOB!Fd@MykbwY>IcuIuAmN zRfa=I6ux0>g0oko;J}x{Jp!cxkiwIdTq+m}k+#c}g3oJ5MHRit zj>bRBt9B~X7K_fHvzX21RAEuEL9HmOPW0s)3)@nT;^3X->0+x=*-MNC(M3_Y&?^{( za4P73LeOws)y=PmSl;7`SqoEc4fc&a5Q{bj?L{`9CSE}9@%VK<8E-GPxbhKTXgB&| z(S*%j;V`z>R(HfuccOg$K)~ed9&7dKg#YmQl%$G~|1gD2MlRprQrxF$ZAyxdAWE8v zF&}dmvez^DsGzzveGV#yspURG09=e2U}XlD(E)krm>mitP#9BV2Uv)SQ5#HZu!%%# zm|;{wrwb)Bv?EYBWzL9pOiw@shDK-2oDM#}gEB`A^>sC+#f1g=@leog1Rb_njRq@4 z&2*WP$qj)*Bq~w~?!q5N z%={k9US8pJ+~9I4tkFv^mhIRTu-X0NcR5uhrM^&MI$hYEN|b>0bUM|}f^eX)ps}eS zRO2rUt8{YP5vMut2ohq}?a%WnoKA%|@7<8iDZWDDPHX5;o|y>IYG^s5zHlNxRc%r! z_ryxA&WI=C3nk*g5)`J)&~6KJMD}&?HWAS7+2qZhdvyS~1<=H4c-eXrcxqm=&O=;~ zpk3?Ts0*V~uXET~Y7v=bH?y2k%Ss9)Fg8H`5c~v2;Svk%c8YcsF7=LoYC{x*EIO_sVY@V#gayX2q!g~0#K-0jYcmv zG(kyVx}zzZ5wHWDgg_PW1yBpVl8&oV5$XHm=?Vz15K0Ad@1NZJctBN()2qchqE<(U zjIth?ITl^ekni+*o%s!e<#5RtzwVN2ef~nh<$A>J6bk%#TEVR#i#Vf3d?ao*$M;1Y zCUZeS)TAaGHao46zeFw0)giy~T66sCQR6MjNdEVOVZHda+oN?8t$*oBjBh9PVFncO`qZJ9BBWO&+C=oN~MMprl- zH)yded?+SN=mUV&K@f6CMT$Gd|AV==T%>BPZFFyVayfdL)&?|KlHpO=yUM*4HPw$J9E%}Q>0{OY8$Dn z$oKhVdb28Ey7$L5%Zj}53Cv$&9m^7-LT8qx-Go@<4Qw705J;Gki()=}) zmqNHBnaMsb)pG7mchXwU)Tn~$OPsVca?CDuGN84RR|uGEic#q2M3O_Bm0>(CtKa6w zGc1;f(WrzuI%1vN>luEC?r3geYJrc*OmSbi+hbQ5zO`5#*i}|hQC6I&h{xmZ!lOYw zTi_9|^7=LG^$C#BL}4Ke{wz~^^b>9?yBn1z;?yU?p@H!doy}MPB#SW$l5k)eX~lYu z8H2h4Whb#+p-gwjJ#mv!iL|`LMeQ#<)UH{w0I3==nc({=DVIZilU-wpe;^kvxnX45 zH4A6#o@-<)8a)AfWl6sB$2ZU3)qUMJrtj>yUekBgqQ!f9Q%gI;oipnzrmgFCJ1+gs zg0jWEXRl5zXZi7xMQ=2vFSsp#BVGtH+UWUoGdoA`AF!9)Ut8JN&cp)=;`DHGL zQKgW}Sk%SBN*z1HdN~EmN`?YZb;^w6AFrg3*KkTaUy0{iExdKcL@Ri@4emK-{CEoJ z|Jos>=jP0ib;mz@iYTV9-L`Gp1=}t-@7%3hHmpB;-P*HOtys2n$>N0z=FbHKGb4lj z8|&+8YpTmjiweQHfP3)<40##mB~bxPQObs}r6`k<0SM@FpsUG#Lm8YD7#T^1N~Pe( z52nRRnHNhQo5-S?EHV)&4~oPDClU?%(f6BWtw$Vsh0Rj!8U@3_s~t|>AlS5pm4ZR31%LS z&t(g5Uhj2-LsBY~<~qlq207#8PN!h8t-0N%B}R8pe}g%GRoJxEX~JEBPXi9;$KU%N z;6={ZP(%9fkZ~Iz-_6tXr{kZ2qhnQ#vckU=zk?pl%k(o- znd!`)^cBNI4`ZJK22U$7%gwtW>B@I820g1cfHbOgDxF%jgE6U0mnfk?*R!*179*#J z)={rmCZHS^wd+#hhtcX(+8GS4*3DvcIu*8=N~ao{Hf>;F+VpAD2c`}{eg!AlJ*ATt z#iS)~ON)_u#cY;Piorf944n>pM81juMAgK=66EQ-RC!8%5-->c8N5LLc4k7*#Tl)z zi(-S*z`y3p#3163RKb6iU><(*pjcaBZ&p(O4$yPiE~=-2rZS_pqBV7>d!bYp2T8|i zU3F+{6G^IgNg#dxzaRL`Z)Wy>Yv4~W95WiJ?gK(`c=Dm5xP^C$y@i6L%8lLdI{qRn9zhXQ<~@?ji1DXAA^%oBEfAblddcPRixD`3Lp@PLL_tcnnVpc-$`d9Khi~&dW>+CEKe)-CG4*6{s-_Ed6$d zgY3o9qOhk*IPdrS^Zof&{G&<-11-!dA;?rR1|*5<3~(8;TVc<@X8_-&pogH}!0eHX zv1OvOc*&At@*aH`w=P)%3b{mlb${LbKQ5|kPyW*M%gXllN_-^AOF#GhyrKec<(oFI z*kQ5kSV6t2j_pS!q|5qr4Wva#ldv(-jma^WV878sjQBt%MuC^(gd2cof?TClLBB3n z%9Uml99*49!9{m359XKpR7~t*@!-9`yf?Fzn=0`S>*m9JUXS^tp7M!w8Std^Tr>xw z0Y}Z8LTuEj<2W}C+K@R(6^ZZ{Lo>ep2k+f~@Yy5m+{*FWEAb8k1YNT&c>4|7K=EOS zTNu}*SBOTflPr!%a|!Ve2i6Er3TJAoUlx-fcVeI zXV6bMGoK;=IWqtb+!BJ)%IS4_-7bs6;((GGOo%}Tr=~y%{e**g3a)SHcK{tv2%MQZ zTPMt%Jrytu0j}h-y238;*HiNAF1r**B#EQTZ~M;88^`b1_??@7dXf0~g%=U|D*J^O zIaaY(XFdzXKZ$R#br_zP_zoNp?>j)}ypS1W3S<|tv*C|RA*CF0z!b; z%(t2EGY>M4GtV%uGVd^dW5y9FgWv{IjOej;L{iKpE666YlUzxzCwG$j5IOlP@_X_} za*!N?Hj0S^;53XJ4T%7B5^xA57zhajp`61vP%6XPh2xO)4*M>?5ps^rEa+Q@}a_E4Z)k>OEa1GQTPhG5!i__tGxFb^PMqQl)AOZon ztHT8n*guw~1l*$W{o-#)1sfbkAUgtm-&hR-09-E-10-SS$|6g#cj8M>2=vRxEMqIk z%{Zb|Fr99ID<4$0GB{{5awyl2PwxqB2&k??0m&mv4yhA1XcUs6L3e9Mlz6~ODKAG! zB5M)196562gUZSaD=Uc%B#}V>%CL{ZK4_-Z0?c}il1Ds8H>EM4cvNOPF0rQBWWuni zQ+WVXB19dSV%+2rxlP>ivyRrE@bhW|_Ym6;+oZ!F+g5wkRjKihSzYy2S5f&~gEkLf z?tQRM7J(k19(CN0Txl>)0`GHdNuJMP2U+821P^sfMmLVX0arUfClOm(<3@G&9x!eJiXtbSTv1sZlE zAF*=U@!8}lan;2v$v^y~AAxofW--&q7L)6-8)E4&<-r(4NJxwcJ~*1uPGU^(JN3$? z8!`f)c^XYbKB*B?HS0-?7FrQs#j{dOF{{F}cm_W69AT!=KAmHd$`?k=2@a0-{(o!x z^9OBviCp|9SxtQybK@87;7WSaQ>ZTzWNp9&4~JY{SfL_vB}8vItUM<`iqoM4qn77& zgq$IxUSU_-aRW`u#~IZaH>K1x2+5qIz^8J?r2jho{fTH^W5u~>6{ zys9dWKN_S&gAEa(^ir%j7Ei}w={OF`Qt^0-{$UPt$6#hWOoSOs_eJtt7P`lK>0nSH z1K@>jA#`g6^gkFCD}^C~uVN@y1<+##&~Mltd|n#=p;gPwyjdEDDmxC6F*Q&bLejT! zlCI8Kok^dxsj>d4bK5ccrRfBebMPHr=qu0We;5-jN#GR%^j4P9=^FeAFq$N?V_ zYk(Ld2scBN8`?WZid&^^Vlmg;d{`83U`NDD(Jc0?1#v>}K~B}tabm5A4q96{FW_hw zRV!UWVS-}-Sm@<^su<=_Z5RKbpY{}*X$&F20q}*A+g8i1uOJ(*_*VTZM285%hs5rb zIE6esh^NKT&f$r6bTqj#8bdph<_e8nLLUuWhz|tvNr0Hs4iOMJh$uR&Hrh+FhtUu% za!DIP*dW?S(bfhrB!CEQYdhMa7`{ARB8^4rKkznnKOdK;>Qi*j(>+pwxZ{^_Bw+3k^Py9*2uY+Niol#m77b!T-6;XZ zC%`NNk6pT(vT!Qoz$u@A14qI<36i9%^$Gs*mYwWN6JYqdxQ={Xe20!R7w+gtbu@1^ zdcgzgq{g!-LEg%rWC~qYWW7s`%h}I2q)suP?dcY+5>nA@z2O;hUg_4?|LOWo%{`Z zSwUkUX>2iu3=tg|J9l!iywcoG?p+D#fCECTQo;&+BSy*04XG5l@mKKLp)f+9`>_Ng zYD!;mBi2U_Gu5#bqlBBrz|dK9QesWOQCI~8X5+zZ^*ispvv==aaV5AW9ge5clM5zc zkUsz0S-Ce8ElX()qGM%v z*0r&{ePdVGrna_CT{%SYX;!Xp-`LT)p}l=WXUE3&%3Naj5M~y-sY(1v^$9KMCJEY9 zawR82oG-cx*V0Ldqd`nqK&ypvBAI#BYZYdtISYO29O#SXI5kZAjzGzofyjXqB8|z1 zfXyNHN1x`#CDZt1iu;O>?q+uaK6pvb6rjM@OXfMSCCrBg! zNeS5DKO(_8jp!#Ul|PM~3?p|P)#BJoA~SAW(Wqoa46CI0T)d-r~ueRD-> z{HD~3RmvGRtXXrz4CN}ZCAi%*A4mkRI(wn8&+CCNy{e40sIPkty)MP2Fd&OdPib-ynX zk90nJ53!uI@Ds3f8G7lUe*82W0#Xmq74F3fB1RNUa>ft_3z8%f;A280pbVzhO=IM7 z4U%jC+fXW>LcX%39Cj3JMQK~=SaVL~ImOt&)tI9$gWl`%`=T*-ykMc<=kxjzy8(V+ zkyGI;i_85sN2oNvpf+q{-S$w>>54?rN-lrzW`3A4yVo-+PN`I_2k`Bx!Oqh8yS zxZRM$mk=vrW>PR6N`DCfN?_p_ld|@qs>Z508%l0{1fQd4mmK30#8m2%OHv!8H_QcM ztzoa(&y+wz+049{*2qB%d}b5erPyrfMFn6Rkx23#0CDU(Im88;ZUPbFh>8Ybf-IyE z1jP(|D1`ZNgA=HarV2Jacs(6|CkQ6!7)zhPa0;F~)vf9OL3=RsIT{ISkQUCB>#eGV zwxVje9!@7-jgdEc3Vk6DaF)WJY>HJj`;gP1yxMzjq#jAtDm!O{l4Pw26?S=(o705{ z#$@iMRLEvftAasCaon8mEFCCqSf1`br?sqA{9~-K#obXobEvk(y+@*oK0Rw zd_WWP>QN^O#}SV+any0Z=*^gHU8nE5V!+$Gd}OIIK|VTL9g%s@cJD2oW>E9ZGR zkT1yTd_*1bbiJqzHh>kDp&;8ATzIBW#ER$bZ>GA28+Kg?+=`D4` zNC)7l2|7yh^y=91ajXELws%;_Zpv`)Qt)V2_JT_|kQv}W26e?T5o|$*-=mTa_&tdP zNc7i6bvIRZEdPNy2J2)t6d9q!i0Y#DHoPk&PfUg6;6S0I@N~C<#RbBf zg%3DIzd z)-NElvVp*Fe_GPUpJ>agOcG5F&p+OKZ!Ze`ibStg9#~ zq3EaqQ>P*<+>VZ$X&tUa1FdF0!u++G$(29FLlE_fcyj4}Ec)s0PdA+6)-!J( zD<2&loj!g2x{2uM>9eN8hjw^se{W}ds305WZ~02#`qlOdMY^%c{SuixI)hz7Zu!!U zgWROIaIARCQxlu#Cuf8X9AAhmu#a+fl4)W~0r4xC3Pvl-XP*boNCIb2-m@JR@;)*< zvt0^X;bzc;Ed$=0Eh2)Vkm3=c4gvh~-uAZUG<*XZ>T9Y~JToQByqx|P*~z!0Pk;5v z4NNdo2_PP9BgG{4QmR>n1_?7|G*VS2r!<2)Cg5Ez5dirl&7h>_w6{_<9PY(O4&C^l zG=^>J>hA97=R0D(zr*?=PNorhUmwoA; zQf9Z1_)g)z0`|(npA?LL8*l6$oQ^kr%*j6fVl6G^MpTD4YF+)u3o z=D^ls|xJf|021voME8#?~0K?emU zn1=cUN`zDN5nPy*9+6s%QurHWuN_}e#|FmVV6Prun_|Pb5B$zs-0#^xVDA~g>%p>U z(}ItJ)~(mBquH6dts1C_m?=Q~{CjmOBdE8zu$V8Z8#9!AjEx;;6)aPlizq6yg zCEZYqoCp<&k1mXb{IF}ez)qSCs0ptSz;8#6V?KicAN33**#5YRdOCq2fzS;fbU*`y zMr!zmY$|Sp`9d&N6N{^?0-Q(fsgykyv%~A0Kmf&AjQz{b&`z&+M`&mM zxl>Rramu;zoxvSm&yLWJ{H*}mZ5Q2Ovwp2^tY&`TYgOsw*8&S_#%>DCuOYJkz9Gny z>IGtl)pld3n&rFer!;qVwRG2aS9c26T_<#0)Q`M=!Hyz@~vl7**M6Yk| z1B#l$fe;(+E36S7_`zsFjc{Xn-7H(vjY18nuSiu_jvC!`A3_)Q3HwLL4OG)uNVRwY z8|r{aTsa83Yy~8ErGR{sEHv)N@ErnnfbaZ%!uSjQg>h&;y>5ryYEdI1hnQ88lP#6= zsPADWi7A1fW`fEPkVA!7NRrKAqrSZB{CUabyz{#{GoRue>+Za5owDzoj?OK8DGMkA;=y%VHSe4K@AB|PO;dgz{4ynp>KE_$K!|-VB!Vwf;_J!W{qVAlgVXv{9s~| z>L4o-LC-)f)hCXmvt~(eVPWr*n%bp(g@t`fN#NZNPZ~p0QQwl9nkDoC>9dG@M?8ys zgW?RV%g4YO9%!uF(#;`%9vl{YAd=KJ2puG0cL2EI!UvNsF2_)yWS4TXVI+hJ1}KcH zm2j2t5KktE3}cWIB1{-^li8)AejP~2N^a{sZ%*ZC%h~O1Yg<~@wso8fr^31C+NV6T ziuhJ)D(9Tr(YCI&bzNKAx|SJDjg?DVbTEvG30om77?VG zOja87gz0Qg_cZs^)l@bmo61WOgpFhn4x8R*@F59=j_3^7cBzaBfqJ}B3j#dnCiOk^ z9EF1D76gh5#q0oQdR?xCA&_HjxG&Zm?QV;wQFe^%z;Tq?-*Mti>k<@E^ZHUcWf%R82@LQoOX=Ed(X$h(FSCZk$d`b074NqzYRfq9U(nX^%IA{k< zwFx7;1vd_%nT|IQmQ2q_4&rYiRC8s6V0olxIDeGxkIdYI(`#g^LtM zql{)hOoBWqg;935_y(yGZxU~WwV)TSnQs#>yP&)riT7_Li}A~^iHCkW(bkh_>nf?O za5{jtpi+k=45I|x%%Fp&#tDWUFj@>WIio$Gge@O$qeP2GEg_4=r%7OZBRdP(*8fC2 zYEp%-ZWMoAPL>ic{SFOLd^*t%wFpBKBco|+k-r2+eR$u>x+SQEu4mcFEo(H?e2JXB zQ24keIzUd*h1T&;s9F|P4#so3F(`hHk_xOSX8JKPx3nM7(?h>a5OzVG3&4EBwHr?O zF!>W0jTRk8rU0 zTzrP&SnQz1m|G6(46n4b2IpuTZaD@AE9I;?z14RuT# z)5Y|He;Q_Hl2_8jr9=gtHIIbvEBDLmP)R6G`Tw4B-!zQ(Pnzhan0Eb z!j6CC7$J7_kBqdV?q3K&0wXg=X3m(7IQ5}SglYGb_OAA>&JLPmprO8+28bmJDhkSJ zikeV#q!~YOI0Szt{YCKctgogP>oLbIxc^oS3(TzcuE zi$_MzuF1!5m*v;=D>UMw{IYh1=0Arw=1c!b{=luVx|-@+r91QK`yxfl<>bDl&0`mb z8Y>1yD#@-`!=OU5xFLpTudl*8`7jm}A0PPf!2JVH#bWR>ncUAVaHvI)eH)QGQQFJQ z9A*K2T`s#M9a0iG+lEc+CU#j7QK||(tTs?U1jY+jpgLmEE3#UZ%NV=NDzmR=tRb0o z9is|K7Az)D5Evxj&=I{}M>F0)-78;~?{&s(h*|JBES${FK?n#jK}{$~EnJKypwuj9 z0*qe~@FU=lQRHc{&+`}`$@3wR9bAwEvOMfUe4xRgpCKLS4GRh)ad*@#H|Pz6St0z= ze|3jQ9uoH37@B0)% zpRPPbpF-5SnzOh64<{k#4^X=vlEcBT$7n{WIkQJ+W$^IO@DNZjv_aidx|%whI$B$r(=Z7)Am~4ZWG@he!81pZ-4RGw z@?Nk+%MFFlVF%5Y&aQ>_4vS+#;^#@DGBtk3ur7eh6Kuc5$(o(Pxc zqGb+009hvqy*2ws@ZG`udZMz|9@pq!v4F*{z+P{2q+ZK}~2UR1_O(MUxB!S3`I=pHO~ z$pTBihOUBM1bT{8=~yeXkRXFS*T!Z=lw2gjDO+zHELyduT*aBpa0XD8ZI$W61AZCn zaLAj#)!6clMxHpGg!gpiiEke5`W7lEuv4ea;&n=*TDyG3Iu(KFVNkQGnpMf<%9IL{ zSlS@dz{EIjc-lf4ix_(yE1NfcWFAihmF(|c()^v#{E9WJiWrORdogU*PkmftsHzghz5u4QK?s#W;Tthu#B>y`o|JgUk zE$r1j&}T~bl6%=V$zfb^ruUM=llL+g!;|RQGq^XBZTDH+ivAw`2=~=|h5Ksgeazyc z`^X3E%MkB$z8_ZS|+LgJ;~W4I2+>x?i+3nc`si_@gNgY%C2>hK$t!S=BW(T9wd82#|)0c5l) z76s9A?9DF8-V9b!@<@mxV(ILQ*v5) z`LvE5Lf_`D&dt5T)~!O{Ih|dbdlhx$M~a3yRjD}*sCqDj!&KFr20A|K+qVz7LSBZ{ z9fZCOvVAxNZrC`{Hw^fa9NjfwfTc6$%GAv{)>XLCLPvYL?%tE_uJ}{-|55iI0B)3L z{_wmrl19>~jLN9@zFMucyIQTbTf5#}@4Bxm?q2r=4A?Xq447iTfI|Q~v`|AyXdxj9 zAtaZ=1=0(UOD?%fyYG^_kmNh=$faB?{=a8Nve$+rko)ew@3U8$(P;F(@4V%CpZa^g zQh)w!+PtUP=0*Mei~d5ZPj7Rv)_S7N7|(_3AW=gxN;ouq{F;phhu&x=8jNN)d)wjA zH0`ctAMb8-E*m@Ejp0=LdsaX5-`!(`a(&)KXfHK-;l=3cMPl8f_BC~IqpzvkwZ3+v zuU|x8gXM+xqPNy>F8=IDt^N}0t+C$vORv;_^~(5Vyz9u3@ub=c zZH%cYFV}zlGMZvu*WbVH_-V$G_4{Ydxf5-1z`YG7EE+olcSuBNE$T%(pkGq` zb#Z{huIlP&vjI<3w#lSla-uCExLB!)xzw>loCCT8876kCouLgK_07x7`7;0AgUA2# zjQ;*J5SrIPO;MYJPN#AC3dSW&dI^sWYuf985ktqq&@}6yBZPhvqob##052GxY3{iM zy{n%l&-STQ(uGidEoq8_;@U-M-Jr}PazNW&BM(TH-CRqIrdD>e`qm8P$HJ$47-xGG z-`ti|r^m*2)b}^f(Z1#yjgx&PK?{0t>$f%AS^rjlKbvZPn%!oRi`ByFYJ3~g*GL(r2A{wW36owUYWz?8`)573$i-{e zLa<-F9VyZZ!2_w!K(kdPGW>V3Wze3X_8)r^b5hTxYYDY2wAsLhhy&j+BI3pq8xK>% zVYKFa_mBUC)|?oJMme;9fj(j)lY$Z7s{0odjI)cO_$i7G7^{u<^FRILKhbTfz3?ae z^U#RN6xjklba)s-4{AhIAfg6IMyFI3Z*dmmI7;VoLLE%$^gyTd`zOVyu$MS*HYIRF)^y7JR1Oye2eT4{Yd`{L}1*R{8+b&ofm%grHXqxob_ z*sc@N1ZWAO3I0OGYM{egp7!*L{{#Cnb4VksKge-r%vz{* z;dUzsdSpb<>xF5=Ug(7>a=(s*0i6wXK4PH6S&@pQRM(&09jARB5I%AI^|uVk@wZ$# z+}>U}IZm0Ar&`;`(e@RMwj=&mZM&X7godJ(7{M7#n`Sr>o<-IE|H*b!{O{@WCjHm7 zd%V3JX}(U(uT$Es&ST+&Xm^x&Hdqs}c_Nku0^}iR)4@Qf9vy5Nb3(_ak}m8|G{?f& zE!9b#J#}hV8&h1kfqHN9La+b4$=MU#GJD2%;9qmrXSBIb?fI_oE1L1#JEAk_Cs2X` z;gafi+y7AiJDNv~8)=lG(qAH*AXa2bq<9^&{UA3cCa$_R(?mp2gz&3BuB-}!)$g+W zAO0}A42kF_MHitEun$xUaS*Gz*C7l6W|gAq>>Tzp zm5$LbZWk?kV1sl;n2*P8J{qgu?k5&0&$Is%A@%|Uu zUwEM%5mqnU#u27`>zghh%GbH9?f~+EMVY9(6ntKkbAE(lpR7lpuC#uH@!E+xo=dqk zf|H*I%58WCa}qgV^%cECUlE+h%zPi4X3Kt06bFO(Ua@#BbNV3bEb z?Wm+3X55K;e*`J3i8LC$O9X-e0Xq!ZH+XN6H)lb+6CTl*eIV}_q zPkfPImEh9kr9{sOg_Lmj4fXrgXWe|mhBtnvJ?+XP%tgVi>*i1%9`Wj_DK z%q|BFd>{&ZoL(c+UlE_q9s;ygTdi_L&e17Q<~qAQBAB zaM2Qr7^9JyG*iv}IKbGS!s_Oxe(?Qoe&dBNeBu+2KX%`}*Ij$*CFh>Ab<65ilM{(J z<9_m^@Bi};{`otveBTZ&i-@v&+Oc?d+Y8E>sC##o}O4axpLlES7$yKZ%?#` zLnmP|up7o$tP^S_#35!?*B_|WpccTA#nM%USP^~$h9>GR#11VK-lh5xkgGbBY#>Gr zmjguoYi>j2QDlH;LM5ZhD7cTlp>td(TUYaXcGh(?;>10qZEfCv;s69mj&(x$K72rpn}{i}c<^!mD3&>11V7 z`tX#~6>PSY+RM9RiN2IC=csLVI5yW3rBq*{{+Mia8_$#Fy+)^1-fgA&OeX7BEJn3* zRDh$|X!(kj8i@wZNA=W%=F9pt+G4euMC;S^ptBwgAA;(mq3!H;^$AeM*Q}z6nopS3 z4%NTv2mE)S1IWSxI10qhwcHLw_+8H3z`c)qkb8>z6!#+cGWSF7cGX24-Ytb|4{_S(@=NBjRO{iD<^dY+h!!*z7Y3vn1TS)oHQLa3JMQ z-sPN$s+;&49N<!e>A8 z+_TSod*zHk=9=j^(92PkPST-Pq8=Xi>D+#}ab%t9$7U&X`_|KLu~0K-H#;)Bz0_{z z@YsFk8{O_BW{=&&t_smQ4MCUC6EaI)yAQs$BUCxBw&OpvCy=je9P8gu8%fO_LCfjk zm>x|;Q{?)-w>0lUiSoG1_UjtQue{|B$i0nqMt+R6&W5=~h*H?j?cpxt4s*x22e>D> z=eRF&Kd=2?1566;+q*)rNE4mV=A6BQm+i=Q3duA=ODB?M;b0_;9m|+vLCF}L!MZ`7 z2EAlavvQe{l?&@9fF09hh8J6$>FkJk#X|Bi*=m=us;o(cx4Epg8P1(xF5c~$iD_#I z2dpI^C1Wi;|EUi@b>j_JT&}I9&wut)Uwr;C^|i^lGeu64VwH6Q)|==!jGu&CXF!uCc>kG1Au{52SWZU8Mf#Rj@~wllFMffZx4ky zPRo@!I&T!F&AB7PH3}FV8tm!s0V;i0wF`F2_HsM2l7vbjM3SNPLBGS{cSNAZf}vSW z%FAa}Xi(r?fgkgh6~MA)EBFS^+g=4%)7~UJu(cQPyRx_?sUC4Y-pOpO_-BJ(`^)9# zd3g(Sw4~E54px7?c?Q<^b@mJ4HU7uKkA<(=@3&8d!xtI8sQ++mQ9NHgbA3#Rtv|Dx zpM5&9d2YBZJa_ZNiIZ|HRtN@j@GrFngB$N*NsnhmEC!wc`EIy1>`_(rGCYI2Ag4Lf zFkWos=Ruy(v|HEziP_iHh~9rPO5 zzWB`W8$1f*G_PwxWy4SPx`t!GR&uG{+S@n|wJzA!+q;df(QyHSHPwB9{Mp+u>mjj| zrbc%N5uxCb4v~8BE!xKtVnn;Uiv6YjL<||MOiJYRRbQe-~tGxliz2zq+8(9K95}6ThdSjIL)CrJiz<^p?W!cA%WO?&b5U4!sf^y-tI^x1z4 z4|d9f{zZ@XzQ$v|1F!eLJ>>~$e(Zbm4QP(wP#%VtWaB(@85P{Wc2EteqG-+!^^KjkPnq*=JxV0 z@m@r@t*R~WPK0FG8+!}T#0n#@yuc2FQGjBMGCLr=91I6|cc1`MLM#sHWZ}#=jo=EA zeE0|Z;pLu-1OCqr>4O_%V-1iTBqrBnN7%hoKX;mXkOS2OhgAc)e|X@H-7tdDHxNoy zZxGC3@T&%P;Ls;4{v9_zv1$HVNw;{OPG3%WGNQ!u4msu;pOdQ$A`VQa%_ZvS|G z9J(-_ISzV+RRx0cH`tR1Bi9SsPDngiSino%p4vI3Vrw1}NgHFv7O1h)jMK$2b+eGL z$x9oZX4C_g=O9QDbV2+dkRD`V)NJFaO(&ux#qd>Fs1${8ElRu2p z;mRZ}WE3Iyb-2!;XJ}D-2v38&l_=H?$|U5>J2eiD7jq*mxshCUG@l*GKGEHM-_1OK zbN%J+?x)i`Qta&X_Eh~@ys_Q%aY24=E;pC%%#Qri&1@_1!@Ij*y}3S)J4Wb+_u}m^ zK489(AO2Aun8N6bQFg3m^TBo5!OVc^FsbQKDF%HhJdEIQjK}7YO!S;3SRZrQsm*esfm73MR{(d*58ZNW*u#9WGOamZADkW@ohJcXbgv}ie=bWZkB^s^Ni{61&hn@M0k=8+Pyf>XLN61|u*_$}V zST&mJWG+vA3}W*{9-cFA*Ef}JtBW!YsbGRqGv zYms9eftF#poEU5^bYB^tU(T`ZhMrupFIbFrZd=hMw6_bD<=Z-=#b9qKR~74b4-Zs; zZf=6#z#Fi}-UD8C3naVixZAkL8oad z7y%=0-neXOts6O)S2D>WE-dPJku6ZvoxljAhfy{X`vKo(4*s}$$tLM-aPyRrqEoP& zpua@rn55R?4u9g|M&xs-+kxN`;Ogr^O5UsJAB)<*Ic^)?6bD+*|ukT)7mo@Ob+yQ zv_~TjA2D;l67eFx4bg7(Et~1~BB2{t5>j<|JMo5Dr&52>^9T}J_knlgg4j@gQ~ zgVB&7mq9Z_D6(FVjuO~LXs*TYWfXeqYwIXOD zbTV#3mWn$Y+*3PtKG7tvTL9NT=Jo1KiTMeT?hv*kQv?>7_S? zJJ){7?Y?6B&I8!L-4<^&Z@23OlNgE&ZhvsmwnAnyvG9!2(m6V#UaBD6O41v3=DDfV z$PTN+VYSM#wQ-zpk!1^X3ZJyt?H0*q^S2u$$=If2`zO{8=mbeJxGjNb|4MTdVK7p! znYTGb-aH3U*@0X&;j_5)CP}9sTr(lN!}E*f72CJ3C>Q64-RF3FBa>58laYR}RZ`k* zMw`iLOFMMomj2;t%HXyN6V+nXyGjIxvfkwLndm6We&6>T7K?*k+Z-cWi;t98|!1aO8*3dK|v^bSiI0 zh;_5E>c~YBBS;fS-RFo*osLzA?RoS}q(E)N;hGMH>t3?v|Y@CjdK@TvRd@`}#R z73D5^?@AA(QUmD}y|V|5I^#bYy+%d;2cvI^!65yE zFRTdbaamups{WMVvpDu~@DoNZSo0YnfbxI>A%`Z_SAkvRR!u_yN%6wxwVGx_OY7hM z*?DhntN$Ca6nyc;J@q%YzeYEMZR5X#ax7%U2mTiF+EFj7{E!)Cj99=3h zb~Q~Hn#hlUI@sT#=z4wU9DXBJwEZ<;5z>milQ~uiY!{ws{C@2(@}HSG?{yYsL}-r+;e+oM?LaCP4pzN%cO_WiBKrgd(ql;7xn^E zB!LUKj4vII{IW}yF1=(~-VutI%kf0HeEi?=mcR$*6VknLbzY>BT^{dbx?^2m-@1rI@#WlNW=JgdVh`8Mr3$5sQpEd zZTvyVsA>#S1+I?e5~v43NtYy7_2g_!fFEE=_&G*TL$bd1bf(mvPIOSGX??}-#@tE(lc$f{T-%^9bP?u_n~B)_4I=cLP|!d|1Q;9eQz3y6 zGAF#$00RwhYxo=@3p>PuX~J2ZYA>adBomkV+WYdkWHD6?1zd z+*9l#Exnav3o<}&o;Q$$!~P*AK^OxgOn5c)h3mu@GJT<5JN4#fmMro{uvr+?yB4qm zf<>HhL+UoeiBb{_GqTkqNKRf6IETbRMdpBJ*yJD%LZ?%`1SrPRq@!3ts(OkOU4l#8 zd$LTVtwzesW|44}o`D9=$;r+cs|T666r@4@^98H!694@Mf;Aqz^Nz#UZ{NyuPd)j> z2Oqfajz{l&^p=~izy0v-2QDFl&erR;U$<$)%4PE>C{&@pucMewg#&ggH^PtjNj`6g zrPU!FPym3vtI4Xk3icqARlqnmuaOZHtWFi=i69lQ7$JulM9MEoA4s0nh!p)Y352k$ z9!{Phcf^Q$Jun4zXR}2e5XuBmbkqIpFYJ2|1rwrD{?>ja0Hca-?yduuRVtSq*fm!N zBTGQpANK2f7@*n0;%iIz4*#xK(OsLN{D11nBIFqXXgdyZypdV2zmchVKSeeEVRwb z=O+q@YAjYwBzj`8p0&YnB)Eqy^?CGW{hnB73b~4Xsm|CQeDwJI{(U&Ewnlv4^JE>V zd_LvKdcNn2SXblnKEHqLAmb0X{jWIVHYE^HYzfCJe)j>Mf5?54!|Qe2*9nWj;2|$mW_+y`7Y%`V>XvI=4!avUM2OSYPcu&B+1X4B= z$ouuQ9r6T%C7}~DDECqM4`#lJ`_S^viZE3u9OU;)#G z`Zw#}rasYW@|(~C`E3OKvza@$cJ^vG@IjKZA$oiM0B@KNDa3FH_9nrix11E~Y~(f# zPacjPQv!wbZ&QL^TxSLN0(Mcx<^hE^&Eeds3)lFx4WuxBidqad=OQR zL`BuXI_-QXGGVJDV23c{1S|Nl6Q?_@b{Cd5(!aXpcPupWWxvbWnlW3ld8eYxK7Y>Q zcILAdbEXw288YVd0`S^b^1lY&MmKtWHF7sy&Ao>^tG27w$LqlmUb6q(ZClqYUNmYF z=40A|O9Nr%AS-hE8Q@lNCNnJRoMeSvP=d+@9!sJQKH(zIt6DvNn&U-&>d-Yk)f*3A z^PWRTh6bu5JtMhPGTj2aP#b*2o2*rvUjX0tG~OH_UdLK^wmB zRlMj1h<**tC76BhiHj%nnYc=>^4hJlud*brR!BXV=1TxrQ#3_ee9lg5PPIK9cKMrP^Z>c)huD6l<@WsX0SX0JuR z`qe1^{!iCq?8o)x?7hd1)z4Guz{#>T8;uHgz|;a7zis>qG{gPo@A!}Mw?k))IFT$u zFgnACDuiH1VhA@$kX8Z~IqpQ31~`#vQXhjxAD^Z&?p!*TPPbB=0hl-%-l~duOFJrj z5ol+nZBk`-=>!nDIFK+XHqVk8Lq}o+oVU}_M7Y3$UFEJoB(7KmdnQzAZ9Qi!+8u9I z0%?&qrv0AQM0aF#cWbr}uB;YMI+z?CWs44epwroU|Gva{f4bG>PFa{0sZmlvTY1@d zEE&(Z-I-V_HomNE3nkOB1hXVDl!F^+e`(|9@t5%n;T^M;`(w@9t^&p&+ywALD~MrT zIEAEX$eL^d)6@&*A4s;$a1dEWftAf2G{Y_co$%!tN(cnNM>pWIfdD1}R!p#-;xu6u zH`A8RAz2Gi50JV5CI|)LyDjFw(FE{SNb)|tnN0Y--NiyK19TM!@Xyz>wZvVg8|EfW zt4N%Ws&1r(OIdwR2qw5doRrEOd6D&)29y*)L_V}3r&W&cDH}Jw(`3Ue?2&E$uGb7!AK2vO6mes*bqe~=8{g>$dPM@FmbzU+c(2D?5%*qZsL<-+Ng7g6opvvSg>G_Xsu_VW6Z)hluRTD zf*sR^=1j7?3kd`@qh}n3UoD$$Hj@>|CvixqYGSmi8^o-tV$ZPgn$r`$!p@}KAh4HN zva3B@TN-mJd7nENZBwk}9%pP$pAYT{R@YHq%4+dcyE8fq!oP-fx{HO>f-a+5u;;?z zus&tey>^K?netf;EZ{+wbc<*z_`yxp@3LjhEgc1$*8vfDM2YL-gTW3@D*uQ*5(tSQ zSF|J<^|6lj1UtY20Tb^QvMGODu{$bdv%zML6%g4cI9so}bFsl;m)w34u6v3<5Mh>F zNl}8#WO9W?ho6^x9!Jas9oo-4J&9!pq5#c&+77`0k0LXlXwBtJ zWsg~na@wks^im`mbYLsMd>+Ak{^e=&xwDoofLCZkMD)#vubrPi# zik`RlLH5PQjE2iQ7;LaS_&t$<1}G1){mAiK=$mn+O+w{VWV zWCy#JGdm@KL7O4*!_!;V8!?9+Jf^hlVisOjr!+r}5#;SWrZhlp-fc~G2d4D9u4+xK zcu<|wlamLJ9z1&F#(fvlte)9LvwCvXcXB7nkrt4*Q{DHT*y zo%=ARQk*^fc8la~IBRAb_>@6sOX)F5b9TXP>{^f#F4pOW8HPd!)v3o~O?!{Jtqjw= zZ%)kF(`E4#qCvMWuQ+2%YvJ}v;%99-353{S^`#Qh@#_V_UoeSV*bW=?Urvv$(9vSf zZgl5Uo{r!kruStGim{m9D3zkFkQnwy*a4$M47*Gw7F3i{&H{gKKXPbTM88|II}D5O zys8y5-03XD%r=9$TyfZyxIdK@+`K6eNQAwu9#hQW@kvNWD?|@0OZ0f0_Fyh&72(8& zmg@ZB5&9U7cUnZOgzFu_XarMQXHCmSYs~J7VqP0nI{hr>^$7e~ui&odj&cui&u}mD zB~7XX2y6Vc^CY@b?ysel>6+^bpcm5kY ze)BGFM-@$I*K5a7U18JU)f7NMXpeU0JFM0! zS*&ZQatl>1x!GT+6?H;`u?7dbTBDu7C;qpe5nN5r!w`cyP{DwxqgQHVKHZxlZK6kI1fLMv@x z){xy{^Tc%)vo9mNO!kOhe%PE!`GI0owMT8%t{zurQCx489t!1jzwTTXw{(;NUgmcg zy(z^D0x@sWB_KA>9yde+*`R1BFOOezjv*e{7>e5_!b{-t;IAXiLH%`OsO`75LR(Ze znA*ED%Ds-RfXO8Xx|05t)}qhtV0L&4n>u@2_I4C~Mr?3AZwNY&Svl>p*$h$H9x$tW zT*l;#h}anOUX!cyf%X1QmMN4(c#9WHR!vy6Ky1>RwSTG_R3O7a^S@I=?SJ|iKu_9U{M*lb`lC-j{-FmS zxa-ba-+R;HYp=Qdz{MAyHM3*erVVS(ShA=#)Y_7WA?1}*f!tK>P}gtOt4c_!IRdU4 znk3XXc)XfY1uy}i4_ZJEXn>;H$g*jT48Wxnw1l_<@EIbsB1DIXkkCQ9Q~efV6;T?f zt3j>OY0wvZgieO|4+oJWMbz{(l`geJv@5G#2D&u;P}S9dOIIr+h0fY{w?+f3_P@PI zGk>1QNy(JoYOH@5M9xx~P2_aHP^ny(U8QoK^gE?;>}9JN0Y-02<@}XdRL(9Dl~ZZR zIC$;F5Nw5l%^&sa)(1#m@!DujE3ns-5hMvfP%yVE+cn$%VPB17A zPC#tpPT+d_+%_LEC>FC7jER|3bY^7+{&hN+SK~{N#ubEKbqEQ}=vG9Ui5IdrBG>}g~G2pGE)~j0X z8$>DsxtpjFQ54mir;1Oic35R_P=98zPL~TkBw6+GMHyF5#bS%vS=DIur;^gcvOi)6 z$B^-vExNeJ=CFsXB+Rr59tY14c;d^;fDy?CB8Ipfpv{5{VB~hoq_?fiMVsapCm4}RD^cC5jAI9>` zT>%RMJ&ipI3zk82WqF@aNa_6vOvzHwY?Q>3RJna3mx}qu+orB^`xmV%By(o_w`C{z zlBmRg!fTXbQN?bwAf9m8EW6s1cC#c~^X~=w9Su62q7*h+5-LrZd2x|Y zrp&!!5G=-itcI(uJiixso(7A`gxHO+E8D@E*mZ943wT%$;~XXIyoUjVA*Y#(-N3By z^c1YtCVTQ%6{SC|sM=pu%4rR1(k5*57L&yQ6@w1khK{%m&SEfw+L$<}(c~og$u1f} zrbJ{!6d@)->7jR!wgF5Av<&dKcFEAAxj3kN;P-451c*0Qs{l7StI#AuU{?ORYETK) zp#L*95VP{v75)yNftVGDJvu~Le9<> zDIQ$sFW3Jb+=@k%+DcYQ`g0;h{WVpZ@M)5Tjg=cRg_*<&k}G~nB84c=7AYiKvgDS{ zD=tZx8}gPSeTxphh0N}RR=~xafJ}f7kdq-RuL$AkIjEkyd;8Y4XXG=vq$8VdE2w%naINZO z#2Za3%vyC$K*Q!H$-RA`g>O*ow0%DLUAmyi8mQNdP*e4W= zG3E+8+Saw<&k=IR1ickmMa=g5-Ck4L7WGDtcp{#dHEr_jkexP@i8o0$Crj!)5vRqb z>nWEOw6!fLbyamXi!}>3?y7CCbhQ<9Mq!}8S1^b?Wl}eaMx99J3Ixg``~WT=KwtsI zmk?`UB-b%={($Qk4;NIE>Vi6VgmP$1&Kp@Y7xGz0drNM1ptl92?=J-z2_$ks^wPtl zJX?jis_LPSM4}pYEuxa_+Hozedb}B^JapEwVsY77LxbAU@p(4fzXe{l8$Rc-PWO-0 z&)Xc2O^4^kv$dV>2#j3ODB_$?hGkPRh2pZa2OGa196Wnjk+qEN&ps-}ozA%QXm)@7 zYag`RKj^SKzY=w8WoT=-4gFb1e{O`=z-8P`+#TEnwLLqhd2|so7!M*$^15p-x?p6u zRLmt{qqZXae&Nz3@Xr(H6CO8%_8y9TF#RHgdxIGOvxq*Jfr|~0$JXG$_-O(o4&UK=sH2NQYnLdxgo<@Gv~Xv&VY>fkHf zg+QWKTu$`gt(XIIQprKT$>VAZbpM{Qi5^*KsjvW0<|5@BFB!W>*%zF_AaEgq{B35# z;D&CCN3snrFDW^f!4p8y^f#6d`;F;R%jp*1 zysE0TDl=U8qvCb@JP0MSIW9C<#o9Jc&}&~%)FB*45X`RPq-^y|-szfX9S^zGRv_vX zK=+8p5M=%f+`Zf*z`z-eo_bu*w+>g1RbHC>P69a5!PUdGZmW4K|!UowGwuVH7 zT;VWSO`1o$e;18T=?}2hkkw`LA{l!>5awOB&Tda(UZ-?&XgrY*NVRNw=?RBzZH^+%EwpEs<;0!joy zAjW`8=j}*V%dO>VqQm_NA%0R8Oa;N6`jQ$TyHP=;$w=@#U~ZnGaB=@ z&$DHLMkM*}N_Dpb?z+8_+G0!^6M{wfHSE~3+Z_M4*{%6qiaNa|py!=-*{Ju(PO~WjlA*|6&<$5K z?gG4l*({1Shh?Q7Tz#Y?5K9P#P*CqRL|lF-uU!$?frBA~kcb63A||8LztZBc8exru zsng<$M`59}cxA<4Q6eU@Q}*bMvfat+0~WnbwDMjX3qnS+nawuI%zs@c#KxnEi3I+J zM=4U3%^gjQyF)`ErR+CII*6xbB{bAJZ!|udU_$+04OSBwIZQAGM*+6kGbcJ6!>A5M z55QVNNN@4v zkU%Sz*4L?P4l`N7>VRMlSTjslZPl>II=!eDT35A%*R*7M$7*v%`_e6EgmY_J5Hkbs zPJbbhY^$`*8=mYyUWmm8iDzw_tCfWdE7i?y@PaTbHk(+-{NZ^>`qthTFSHd49ZRUe zlxOz^HU59JHXJmq05pdEt*`uXt>l`Dxkqkof8zV>f(B8m3P>0z>eKoAtxILmfl_{Xq7op`xLqcU;3e`{B}`4h{Wu=}#I5e)a3i zf3o7=`}+QU#ZQ*M-lvTj@rkeU_ageJg_~Qe#SxpzO!A#@f?g)M*7K^pSTHKj*E~(;ef;rGkYO2`UU`SMU10(0EdJ*)N6_GPH!*V=Ub51TgjZ|dvYsA96}kSFjbz?eVE4T8_z!0qSO)y}v$iiq0f^9HNsd@>dZ zx+DX$0rhamE=u4R3@T||On{frc2CB&3O;1UTloz{qmAnfyM;pym7Wys#R}e>*bwvcs zU4y7{RM_5#of4@Cl%#UL8pqar$GzCXv1PKmJlP3X-s>Lda}*bqV&w=6x>NlLgSo3> zHYEB}?jVbFMN5l{j=ll6H}9IrZcMLQ)X_Q=;r)rn|Ml^NpAXMz?O3!b-S5u{mVlxJ zEZVz}^Si=EV>sH16f#iTD0XFX>p*Gk0x!!>Wyf}mI@f(|f1z;UH#Rs&cZ}f@^DbCh z8rV9C8=>aW0T8c4i1eK?P~Eg3z&bY#zPNn(i-VgwSzy7Y>cAO&5kZVV$>w%DW!dR= z!yFi~TP$`=KEfP-gTICU20RN0!3S&bntkW&J`+0HW%DP8270@@3i*~895j&OE}*$> zG9dq|2ogKCAqHVD+pANh4dR!GEE8owj}bFf&5B)`Q%7@bK(MpKZm6slF-{mS;#%+< zh@<$m0uL%;H1GwqGJ=wAc-5&T5}%^by)GJRjU@p`L-(s=&(_NxZy+uqRM6)1#;Wmi zg1MGpt<0j~>gGXzD6A8D$Hw|}I%mr1Kro@MZ+xsz&{;z^|KR3oILgX3`oTHzYRpST zn4~yTS*DGmT;NfDD^-BrNkTS=*UO4C{?NS?9{DvGbBX=cH^M;?l@KSt1l=;2WrP6FEpFbsxr7 zM*+vD;+>o>8G{4H6v|{BLcoWZ#C(#f{v$>-}|{`Y&51%tjdc@M=NQ2T$R z>IMp24>wBaA_*1k7VHAT*9Ewqsk9zZcLqQ*068A!Ym6qPpQQLP@I-yR#kSf|?`Yp> zS7%#Ku_v2LnJD1{C|NTbDEV@DRkH;xD}eZ@Yt5#E*g!kC#^}!y_dDrHnm{S^Bwr5K zy86ysRw_)a&a*^!t~OE29dQJM4o4`Yy@EkDsba2eytJt!*b?v06qoHD8rZS0XyBbA zrCd*Iwr${F{4STz=W_Y$-)x@ISPJk(=7bpbWQuu_xI(Q3zyC91ox3UeC=RC@Ej5If$-zu0{3JG$@KJ}O^#TKRUayH>`=icA35SmA$!9KnET z--(i&aLA~G0aIfZwk#zFr~*rtfX(TJN|m+E%2=%aWA+G(?c2w$*~bp;+xMsIrOI@u ztWm!Tp8ZbiB`n)NQ7?ZB9v0Q=-}0<3;wX&FAej&s0ZcV8)lhsu&4C9#tyg8)Xp~(t zxGUlrjgnC!J`11(!B(o5HL7nOo88@f`s01gvwh5h#xy(Pt^H5Y)dbwy7+El$#BAww z8ndO_gwE0Hv_VGws=s|>kQt78yqb0MFV@$h?_=x{_1%JAIDT%U|KF}JF!ClOK1cK= z!Mfx5v6UecrvT^%6`h~og#HouC8u$iRBf~B=22_H?CNKqqX+5-s1qJgTk|K{LM=eN z0Wid=O#<5V?%I+*r8{QVKl_~bYn^gb>lnk^%n>g|gh5AU1^^83UWoNo!=)*0EG<18 zu=YSnvG~Yab5{QlW<|Wwj%ItBM|R1{!I|Bn+2?2!HB6npuz9rr1NmkCE;W985hA$P zV99Ui&H@g_#oV#ltrzS$_nfn4cJ0W5ctwG7>-D&tZjrxmIV7(I3>o4#ZUA!M>Xj?z z_x5yGIy)$0opCVZAT$>X0GSz>c*ueq0SBZ%2r#v4@MusCuC__M2|@HK&YJeLi1o+Q z?%g|qknp`1@4a~2)~TfvizXInf#D%nDC7#-fwT=PPZkU}l2_BQt4hA!*gMqyqq`Gs zvmP(F8P$Cjfpl3gImGB>>5$NrEE*lo;y&7Kv7=U9RHjEQ%Amb#*VW5jFMa7{YxR@8 zWou06@q~2?L~)_1)Gx%XUBThB-qL2#r-p-7TT~coGff$dOLZY-K#18Yy%YG-L#l>g zI{mDeT4dGrrHniD);R``bF^)jp{H%GtF$N~?i8-*X&ZGaw-tH}it|=uVj(~vro=)Y zO6d%aWYjt)d%I9YzY-Ks#}w)q!nfVM^Rxzx1S@FXKpTF>JZgnORNRMBy~o;mQD6Nx z+jY{S_@?b*a-lUp+J>5bYd}T1{-n5FZyBt%jXDTg3RH9Ln``;ku}2iSA#QtZOV-Fk zW@Af0id{Qh2da@!6ZZIHRF1^OGE8L znKW{0hcv;?<+NHL_ZC^vrm?iD$OK?w)jtV#|1yGlNU|dnAju6$=*JY5UwSLv!^7oO zU4{9@&zdR#kf~MXu+Mb<4VzP`#bT9;>2qz9t(_~&Kx9Y`W^zNx)SO4kE6SxMUHbQ( zbLQ~XwX3hZ;^4E-9=zg8oDQFP&if3N#YNoTnVFMJ4Q7B3kQ&Np{j&7UUkMKMivsBp zSyUoE8@M|z?6;G;hhJ!sM1zi-??g6L9h<~vND>$SmZV=^PblDlNu?#H2SAs%(p`0A zvK45qp`I2U9{6}B6Rt$o0X?YKVOY8y|DOKHN1xidef!p@KKhZ50JEozf3EhFo&C_B zY9AbIPs;U#oNOOtzt`7`dOdqtPwmwKGf@z}%^yKtrZVtlmIEo#oP}cEiU@B?0@n%O zSVPlZ4wra@Y=d((A=U`7M_9PIO*Pw;C& zgf-<|{CrI;6)8efP=B7Cl|YrT67VTU$o6S^9Nu8$RSwuKEG%ZQ#wd!cy?3 zG)96=rHOaXz(nPA*(%4R^1p zbSx=G6?Z<=v#Gyty03S0pQM0;8`wEKdfsTHwGin|#CnhaN81E!5N*ZDeCuR)CRmCL zmh!9Dt=~}WJ)^9`L3usxb4tC_{rKzK(8Ke*x8UhmTpBuiykqP1y5;Himh^?C1ucjv zFHE%N7ZlM~xi>fKM$uO_=smX9Hs@1NAq{u%yoKOm<_z!>r(1}H2SgL7IykdrzLv63 z!CnlgX`psdM8nCc75&)BEH= zbu^)O_2qgN=NDeRytC)bxo6GsR|>ld`F>WeRpvUvS}5o>styPd~#h+ zY4vC>H@3P|UH8Kdl0R~9uGL+G@gCsH{32>qn1C9tc9u7SS zpwzFS5yzvzy?_RsgPS0-e@}y^nYgBJPUgf#^$~O;VvU`62 zfcm~{xi8}D&K1V`LbsUzdAJ8!jMa}Uo$Atok$XxZ?a{04S7)>Ewi&i1+bS)1}3uL)hF?W3dztAg(=KqCT)DW3>p z`%v5tpndWSTavs$(dZD~K<7Z+G-V2;yLWU{c6Cg?XMD*`TZ56d`T6$Amh_V07IwR?XG3@Y_JQ^db5p}nTgsh^b;|ua zJ9wkfZ0azZ98$jLtljAaxz5RKaba6-f@oJKw_bReuYk^9N4^P(I78~I9oxqQ*~~9Q zwhm-0L5eCpM|2+Z*N*w?LRg#4!UHPYOvxvJioOO1StVPtGs{V2yj+AUqc0e8 zD2Uk3d7Po(>~mj0HWiI>AQ#IPd`_RogSSF<_IZrLME)NrgFWU5w8!G@9r1Wad%Ss! zwFhLSqczz4+&l*J9rd5T%O4Oug8qcxgS8N*>|7*C>q+Sa>l0GBz^y^InvO`iXu^(z ztcxLdT|$7vt6@Gtq#pV{iO`=)IRhpaj<>|&5?rT;-&5(*)&CYwpc;~`9@94PW_Jy}EeBYgS+}?2~ttSg~ldtnn@{a@MVLqaZr(q4a6?XY2 zxW9o-{#EXK?A01VP_aXHnt7{Z2Qyo*zjh1e+$&$;O!|mdcZ23?8jbTfhs7#5_M!_&m&OUZ zAPU$J@Ts%R*qIe5p0d@9U@|#`R4p>xy8WEf<%ff%3$g7kc!rob#chInx100A2?5C+ zJbJxn4Gtd4is;c3y^f%|UFc$tsnx+od+>j-KK03fpNI$hpREbIMc_tK|6`H>Gl{-K8+yz7qJ?mc?%O*dS1aR0@?$OqQrys?Z@Dy2zH z6sC2pN=HgknZtOCf_hmcnD}vn+h$F|soEbzwG1#kq9;2`(FyUIxE{#YM4RD3fp^tyQ{#o) zUO11__Z~<(_@dki0EenUgpM2W)rs!zxZ)kowDkshJH3p}tXsc}F?YE)*wdC7_PP^Y z-|-X{ltwq}AX=~N>s{%qf5ll0N~3K>f9kvCoHOMw7RK(<$^FTA!84Gu$g!?uu8U2i z`jpm0Vn7y{yW~f3Ow{=|zD&mFPp7|PwryUxXrs;KT|1|?7R47)?|!#Cj-GmG7w09&ufoH?H;Gs_erPU=Q5hC zUc23EHD%1VJ|7^9K`U~g6<^?=<<`Llbt`v2_fhV7P>EN#A91g7 zzvcdmd1`MSDCY!TzIa0=NK(Xh z?*QVX;AM&|C^JaM1keSfX_<-n!h#c6GjQ*OlG-eZz@CvnFC>*-AjuzqNs;{4X0d}_ z*x)G&dSSD1(~u->+|;jr`I8@i=hZKN=`)}H7=Y;il}L(N*yXBC2~Ih=YOC*fiOpJ<5Qa|}BW4>WX2T_V zXWheSMP95fKeHfVa{H6;B1#%pd0fGi-sFiFZNrgu&u1J27I zHUyo%c-)t?KXR!vOcz~I{VnV4>i51Fe?%&I8;hG=`S3H+0d|Kmh$X2{Sl(H7Cndx6CaW z8tUmO788ldiFt&RH!?IlG+dk0GuSiO-&X`!Y`HVhmS`)q=36M+1^$KH0ot>?iWi9j ztCea8O~Y(fb=@HMjk2Qg<=CMhO=>Sa7Fjh;D@mV)2}l(2QxLPbE1r$Be=D@seXRw( zQ5@P=x}2BI-&l-)-*#0;1wW9$zX*J8E=bn@HA%dd5naRa zpW$ERdZB;V$(@e`47YIia1U@_t$nFA&g(r7+<)JJOD`NB>F}H3xqi*%m+7qbZV&Vm z>lh;Q0q_pwcLB_f;6*wfj~_959zD{{IT7)TFrJ-YjASIBrO17o-GDG!JN#zsRuDQ% z0EP2@^^CTD)+kf)9L`#!H-}WT0b2&fI|S!f9DZ;1^&bXg>!bqY;L4omvVQ3n~OR(MGS9 zJ)2!PLCa3?BuI(^4w>eMlAL{~0D{Cn4^GuCT-mqM>s{4%B`-To{QH%-tr98kTIX(! zM)U6J&7I+jHKBaMW?8#E8Oof0aoUsKyxwAuhk|3_T}Fd`%fRwzblJcby(sPqkA*_< zQT&@bHu_}TFg`dmZivN|@lt7?axu6g{JGqLfZG`f2Z2(%O|WEAQp{$HNvVuQ(235- zGRE5n3jI5Wlt{FtB^q_l-QH8^Lu|*gh!dzz-_{Ros>buSRN7idl&1%D^;Vs1v&hzj z)0wc!7MrXKD*##q^rCW>B!d!29*n!)Vm;g!>+X&{7l7d8^LqUCCtNNk(E#|v@jLl% zV%^`wy%*V_9tLXor?{`zzS5rKZHBw%_aGd>V7EOr)GrwA`v6+8!p!STbD`iF1y0n2 zz+3djg&$|e8q*=N-6|}2>_Z=%*}0>HC2J-qKAYhXW*}lOjCR>>ln-*U$u1M5Jv_Wk zI)H{F5f%f^1#QuA?Gs2PX|d`pyPOOT>XO+i5!eK0UX2)TA{u6H$&*jqa?6n;AN%MN zpL+6B58QXlv0IM8^ZUI=Fhj4u_R!UrU3%7;>(?w>>W3hJT%$mGBU4uuOema%R9;c# zYQZ#O_R#iQ#YEEnDqch!ddcc@^5rI868tB~is%)pG*9V=SyFH+@rH#yD|S}_ut0yf zO`W?QQVeMDR5*hlI0MzmW|MKzMYWyW>kZkCYF6nMffNKq8F@5UaRVQ!uW1dY!jR<+ zW`oJ;`9HM12Vf*;c{e`q%+AjC-h1EGuB2_+U3IP6Nhj$}<#f{Nnohdzt3LaR+i_?6 z&c?>ZHnwr01Oo{PBm^)uCX~<#?W6rENq_`HAYi@(5|VIx|KBsKcEw-}-<_nH-JO}8 zdCT)Y<@fv^5Up9glGX)!Y)?sVfYVB$7}A^y`72Q8+8hd@66aaa$CxuEC@dKmMGf=3 zJy;Cd;usTAH*;AN9&56*VT)R;*My^bg#uOlEHRJ9h}1&C?-$HQtIb&oHs@#-lbSs!^Fi8_DPmcCDw)V7920 zdgXo1gJU|I#*4NHDW#ADdJ7=@LXSeN%8QnmHP7y3o`z^^NT(zSoP%eX;lV^;c(~2j zF#wWL9wn@d7K_Oq_i3%DG-g+bg4^jDsHm;AgVAWf?Pd!$L8B1$YMs?=a`+#%dHZx< zM_V|R_~YP3jHgbRdjHHn2R~&y{C*>-FF3``V5nNv41^P7E{;bAo}|~%+E!0joUuY8 z0#|r~!r9=NI00{qT7}x4aHD|B9-M6lpfZ}wIulkoGki*BalZw=PJ>Z0U?DVu_+DeY zcEozCvsYbMI`_x9GN?s1Z5kgR9UU6#>`0|H&1{<4JUu=&J~cTpx^Z;lhOwcMp^^2& zsN%4$b6u)E)$Tx&UEJ?=QTb(=xWfsNDlevac@iLEhShY<@RW*R5J}1trJmADL)^MX zwG(JL#jiCP28&13$=*{04$vNDO|`jZ7P0e!s(7P2*wfV56LcN*3qGsF^KS+xxnTA2 z_xPK$e*8Yk4%R-+)D8C9m(b4sU;Eh8oxX&|rPh^v<@x!FuLX=R`nacaUZ3};ySmc; zyuS82N1XYJ$V1Y4jqXMRi#yoOnNTR54uvwc3f9}FqtV)37%EUUNpdXvHwqXFySZbq zT<%9c%Ii=m;Td*owVd=prD^c+D*d*(-Y$hs_1Xu)%f6w+8;s9jIex?IU-$G=x7~X4 zOnnEL@ck!ZowL@ zb1;ilCp#~U1}jyVG;n5<0b~}W9U(0gS@@(KE&D7suU=cNSDpJ~U0l9r#RaKaD&#}G7K#II+u)m4i4`zFF!U=5- z{18+Z0a6vJ!L^=-<<)zwgF?=jkc}ee72XB0?sZ7WE&&Ks*#FE7KIoZLjI}R|%lJCG_kF=0`A!1u{FVvcroC67ku`?FV zI6B#5W}P$R&lO_r%YK5=7uwNjdlAUwM6oJ0xFfScOa7L3tN^9 z|6pqN5SjdB`u`QoEHeC^7S}UDz0aq|uARkA%{?pvYyX?D_JfVyU#dTMmm1%-~okFtsHYQtqJ~UcFtpVLk&QYP6H?&AiPx&kpiM@f{tN%8^gn6$f^Ij%ZK$J$g0D>3ge-JU0e5W+rM$F zy|pP54%#e|h-!3Oe6ZWs6!m!7mO6OKt{myI4**qJUwk;O5wpLq#SJRfrO~_RCD__i z`c+$-Ef01zS?bL6qJ#C}muxwIW_cvDzklgQ^H!ss>}T0%&N^54tqc{4^6=YfnTH;3v8w1&EJO@tJac{LCpTz-<{gO|btNPOENF?2qh8^4hsB9!2 ziQ56>G@D%ptm4)eGl8+H*Nx<5dzWE>e%}>_t{@w~x)sxw?Sp0OX8g4#t)aJ+Sy;%F z&MjN9QE2eXHf)6(haZq+!d-|+-N!w^vei<+#_Nm^yy`v}j2F(HiW!hBeE%!KcX#|i zcV`MQbjx~__b4&ccN@FoG;ce0khf6VI?%RTs0opKtU2c}fWktpISzIg@X|0+noLH> z0Td2E!A+xXKkmY6Q(JGqy_hXFGwEr-O(ULy+#Lz_4RUu-Hf~;l(f|^N4gjKP?WF-7 zI{Qbu_FT1X$M%T{#@%`Dwp&P;>|WTuf5-lbZ4=vw*bQpgrm4vdqeFvz)%I3OPW8A{ zB3EG*7sN|($vmdKh!-)A$v`Hvch#nj*UeutuCFkQ8TmnA^}_pMU+bH*;>xrz*4_1l zep!8m=qpJJXG!|_k{w($iEf9lv}y>KJR!R(Vnz zQ|E$Gs(n?qc88aY-Piet;_;zCXk9F}F1TjD4PxQ`#Lwfh`K#r%HsfYPNAs1|Z*zxk z*5pF2^@_2)&aOt0a_Hc&daYr2r?}ah3;-Ts3)(KkO1Q0WQ7+d`e9OodFh4Ctl zRWa5^Zz(i4At!gFPQN@vEMpgdUejqVAbzFQUBJSr1mQh0#ep0-PR1^9P$*0%^X^jM zN>ZsM2oF(I&4P@XM1_h(#qe*t5>j%`zSK+2RYSMlx_8g5_uhK%o#*%5wD+c6J83*l zPYn+R!Vzc6NLBw@fKDXV4S7hB35%U;Oyq8!Rw@|8vVpv8CBv2Ml;j^60f-90%*Hq^ zl&!!XBk2R~HgeU}xp)Xg38joYI%Gza1Pn*ALI7H$TnP3xJC7=ofvisDR;nycQKhtb zB&{2{`JRf_pc2%{oI}!k)P_^OV4Du$&{l`YS7KqGGbLFvu?8}l6Cs5;=Pvd&)LzRD zTI{XnufE_;DZd7!v_c71(0taqL8*6YdVqhj!X0-l61ShYVyC9SbkPa)&;WO| zdMIe*B_JU&Oq9eofRU!7>JLn|NfF@}N!DS-VVVPwX@J_u3l5edrFuV>B2FGnw4|xH z@!sBaL+?N@9IOO<=>W<$*WA#OZh<9eF`!aXTwW+-SICk(^$G)U$)^fSv7VWXfdTKF z5Qbey+_40-hF_Jvj|{#De3S~lW!2zoA?&;(`qm{w?*^u0KKt7oo;)nR*Rr@@PGbq{yVn?a^WA#CS>rP3%53Y3UH2+$3jFd9MA;b_bqZig{zI_v|H6ggd7J{~ z7#StI=BJ0&bu>3c!*Frw*@ztSoukDXW))R5O;JTtgoX*GMsNml0s+yn2F{Tb0sElj z3m7CAv&8&@$)QZHyjx!TcD1m=0brCK8kd*!5@i*WG!eLrx3q`c?gn&WzD##}Ua+Rpegj%c@O_83a*g$`8aBAEXuf}~{)xl`8r-d2(O(2g$=<<(A84u z2!zXxmMcHV23_TjF8TmVZ0hnk{`*2da&#KFLN#ka4ssj=5ttgNA2utLZfaBk!EIy$ zLcsCxUi18^WGaa&j;Om8N0CDkAaEK`S_?S;#H-)2#!Dhgv-gAkz?%=nyVl2yE?w>E zD_%CRy|t(LkKT1@A-}1`k_tsT8r?vK?>ki6mYZJl#@`$oI*fTl7_a~2-Tae4)uL8_ z17QlF(x}X-*X0xyOv8-;8?>BTt7Yp2e2(l9Y&Zs-JYJ1ETn>x;2oPHE`yrYZHP?|z zW(3eEkG4q3z7_V=Q{0PS0CIs%&XL!tGrURdsjh_l*ez##1#tZv={C_6X4P)Q^^U^ z7>pdv1)R$+0``x>8cMYaxLV0XzAD9j15+B|l10*ynF+{}n8QXujWi48%AjQKo#)Tq ze(L6Zdv+@b_ zRKT0Eh^UZb2==l(dx}L!4;&=b--Q1jAIV;P3ckqdhTSfELy=J^hcK5M|KwFgny0)l zM57=B!$GpEmCajpycJ1#f@sp1-H4Yvxx zCQ-0xwN_qdp;Pogh|Jgwl1Qg$VGFr5bc!e$Y&b=w$0-r{N(yQDQXm{%x|UHVm~^0~ zL0Q73OU*4`N-17(t+}OZ!9ff58l6ed8JBOkR$IPf>U$mX$iBk=FaL%7_v>@bl!wc<&& z7k3(+>$Y#*I&L(?t#*&9Io1AXW9^fHc-he0@MuSd{o97Ew;|quW%km4KmJR*E^KU= z9-BKYs#M*9^rP0QF}lT4FgCE?sLo}x2g<5%cpd@2Nn1+VSL z`KLl1!Cm;GAAQl2@)okE&*a(bJf4upIPSX7?5;iU2;1D_PRqS+!`;O%qwY)Dll{_u zzT$TC#k)&iRq0iZ+T=0yxJI+t4LLYwunu;s13DLb-h5P}9u~Ko%x9j-g)MTZAXvsx)ixVTH^kyv?w8-i~L*n*(V0>+Ohpke^j$jJG$TA6AO3PdlQ~pl07O1f_&nh8$Q=H=IU#> z#ha=s#fayVv#l-8puU0K{@!G?Wqo#LrVv4a>bDQeIDO9vY%b~^KyhUW-dQH~Z**V=gT}IVGx#6^bV8bQRYBFq!fTyyIR7&zR zdpQjmEmV^RmnL9MdK1}}sMe`&dl`*78OjcLlO_#_p&DbM{s_;qB*(69<6YeO>RGvs zwZas&F__aZ92IKK)*_a-Y5@bZhpi4y)k;&N3<)7~G-|32rH1DZ>l!B^aRhD@a`|07 zO1nAMgBwSj9@@dIL4vaxW}pg}j_z zNC%q?9-ZG}bNMqdZ^Z5PnNzN0uF)BIIlVDvGetEfv(aoz*^GW?)M$={lV*^zxFbnO z{VQd}I@Cyet{R7w0AGzHj282`tlxraKRpE~8yNIpNm&*Xbpy7H92}GcI-$d&0YW_4 z~Yum4xJ6pI5 z)jR9Gq!#jiIK6Tf*ou>5SwW>8twJZfYU&iheKINwcZe!A%^y_Z)~Yr83E>8M`rk2s zrl%&xH*OfA`Lm^K%m0h{gPDW2k1kahP`;dI&tTrTwSKc5fJDG1R1fVEjizwE^<{Lx znB5T5m>f2nO>aJ%El7!mbTi4YtLMu!bg3(8X&5`fy`mT;VbrU{GlsFQ-2``iA-}x`;{}Yt|d$sZ`TK zz-0qq0}hd^*Oeh|Pjz8gg7xS90zmGvo}+?zqHz7P0K*Cmk)>J{Sxd|Ji>%s1okM0@ zz2!Aj9_|m3G^Du#V+8!i%V}Rp;g6qscOak@UPe<+R=4M~k!a5l(wt!ET`B*2tKGye zlC7(W7LA?>)L#6`$VF=D^#*9YS(Ja^iu!We;$^e|o|||(za%7}8*AmP+6?$P2D$O- z*dYA4?SvmpC0$OXXcUwtcwk%62EtfHwwIJhFjopdDG5xt0Ksbz*jvL9U+Br60|GRq=YKW!mMSl~YED7I65ANI_&cpY)IJD5Mqe*U1@bCcf_ zu{$@Tt$_K0@8Q4ER#sGAPs8U(IgneqBh?!yP%%Ex)>>EbP%<@gmuV@ zL^{3-Bo8Vo$c#$ap!xvg0O2_HOJIeA8)%NU{4(7sk)2LYtsm;|!ywFo8!})IXc4YK zMLZb@P5!b~){`U|g76BX(c(oib(8Ww%Ex1F;Ufw0i6x`3SX?m+aUATzWkUq^A$xq} z{9>iDcz$H${6eL&aQ zZZ(Un=@*v026KnW;xa`o@hx)^r&)5d5xiq`S?m)KZmKnHpF}ow7e^KEcYz!A?&|p? zu=W>c5ha=!ul6)I86{0CLRmBL?retUf!0?F8bOIVXBtV+NM~X4qZAJ6nxRl56Oii# zEEHU?q5?Hn<=(&souNg~ag)>2)Eze*nBFx#KevSz{;rO4%Ub@~diwlTog!E8k@LRb z{|xZgodCtv2>@^EijD7-b?~J*aK!{*kFP(!yHeSGe*ID(jr5(ID;DQY_APAzwPFg* zo^Aa`L+v+rmpo|(-pEhec5m7?Clw;yU87DP3hd3(zV(9fG|Z!@{frY^I7 zpY`+6Epu@TLQiG~dw7q|GHJ88jOKP(#{QCj1im{xJfwMUYjqYOQ--<11A>Q4JONA` z=q=Qj2Ui-v48Tdl@q)bfI*kk#I1awH19EhDj-tcJP*RGM$wX5okx%BEqZHemACEl!^tXCFI|l~$wKfeWlNr>}PIacW zPXl3-NVc>bb#^y@?0<{B>ps!Y-nF-*bxVhS&_2|h9mxb6&1_3cJW(tr;w|K-oM1Y^ z!S2RWyQ+4)hIAyaDdGx{jb@YVNk;%ca1e(N7*xm`yn2qrMXMq5!@|#s4g9R&j>5}v z1>Bp!yP8cRu^%B)glpI*mdR*9$y4X!q1xIP1qZLe_e)%)8pL*mgXW4mK{gXPZi&T& z{@3q^?&%_^N^sWN>&SdX0F{8G@4BQqAO~G0(jkg1y5@CZdg!HaoXX9vA$a!E*DXIAJ5J0m z_{O!5hwokL4fMt38U8eK7G22u-c{Y9!$YwQG%z>pbf+HNe;D=n#tRy%AWJn`2p2)) z9=VKUkthVzdsYI?rUXNlBnb=fvIt0-Z%D=?;egMB3%epMk%$>nIs?W^I}K_F`GU>$ zp-;YLGZ9+QyG1kQ&&dAfiPGSB%hqj0Q`l??n~Gbvwj@U*^|xU1NlS11h0xI!Fk4Wm z@(}?*1hc7jjC}%cFay27?XEswsant@Y{k0_V4LDGmw5y`@ETge3U`TuQy})ios#cZ zCbn`$U*w)Il&*J!*W3^FuLwTmIR6@GFB`aNZVRWcYPP`JF*aNdDhe5EG69Ht8EYce zML1TG{{x?B1;NxLeImbwcO3A>lCC0Q$;2T_Xe|&PNt{TBDnU`mlukz(noOmfg7+uf@M$#siM%b0LW_+dX#DqDlr~%5eDA%M zoXx7R+r^{GdXEnrv-fPen7oJi7jSJ;RIgV`n=s!Nux5>4yw%>o ztJOZ0bP9kNpNg)%FyGub997b#b5^TJmsVJ9SzLOfz<$j(CvjzSXt|WE z3*lu>5gMXU{vMa)iVj>r42?Yy*rsy_<*=*RRPiK-PbCX(B?G;tEuP%`TG0jaXjp*Jo zJG;d#YE(!b6b4Ysaf&yB)StDeJCi_#<0sLfDIv91Z{VkHY|E#ZMG3m$?=4gQ z&{!;7{yG0&h*R_}p{KzC21GI74-`3SEwvPK&5aq9X?G&E$e@*c+=xqwn%pFVPI=t4 zG{ozp4(&#vQw%KcQn6n2m}bt z%pPbnxFoIK6RCBZ_WaWr^Q_xrTr}AXC=6<-glsI!jP^r^Y(_rI>~QBBbjbQ@)Z*LP z61^s?F6Te;quZN)_Oqtje{{s#thX5Z5@oFL+D7)9v09h$?|&Gs{dnDO<6exRrpw=8 zZw06MBi!qe2U&E&zsY>andTpWjBYI|xl&~%94-Pw-oGlHr7zqma7dXK&a*~hPc^z|3- zK6&%Pyf2V0J0s=Bsu9!4Di^4Q+5+WX4kRwQy6rgv-`1%RG1;ORBS5@GBY8;<4g$x! zG_zzF6XsA-o?ps3s-*rxF13*?!P|$RB02uBi+Lv3WTx+{zmlhN{RZSWmXT68^3O%B znCWaBN}BCRMGV``sOe}kX~QAZb}|SKcTS}?tB|A+0h6mzFeiP{R(M~nreF&ox6JNy zy5mTYwg~n(Q6A-ko|x6-D+F_%Xx{Au&wsF-=?gbEgg+*Mq*teA53+F3mvSm1sPIG+ z*&0*^1+CFxml&0GL15x02`Ft;K-AdGHlIZ`Eg4bmLINiOQgugFNU2d-Y#y&>gVES4 znE?kujg3i_NvkrbY+kF^rrM&i7&Hdlv(w|kQ{1L-B3JO33YiU}ROgiKzI<8O!GDj7 zK&MSn{M^sP5uzaF)lj`;E(u`R>v%f00O*Ot5K~6PKNiBujj*{1v{fP(VG#|iI~E1! zIa(i6+ZtZorS2#qjW~>C7f~oOp(tqkUa4)K$fPG5wU5*$m-d@;$a!LDe+fU7frr#P`Su}g9nc4WVUVZX@>C*# z76^vc0*v`P6{d8|+v12tP1>}^6iGN*ywS8tP?W}^sfvlUjb_$2gO+Ke!UxA=qvhHc z8oRq2n|pfLpD0QrF>gxeNO$D2osBkK${QO&Zo07{mfD>Aq-INdBWRe`H@0uld~)Y_ ztifoJ&uh-~;Hd;IhyME#bj%&FiErWNsxytTFklR&qQ`|4p!GvaP^BrhZJEGwPTD5) zF%HgeAU{)HqQ|ke zJj*wGREm^Vy-}-h37R3shBSf;jCvYPQlaY6C`&enO1A;Q7T|j%b&pz^)Tk%aqDxSZ zFgBtVP|IdQtw}2Jkg~D{PWu^!gyJ#+ll*{H+lcXNd8$hG6 zUabrPQL70l)$4VXGdQ4@nj}@bN*vW{M@3b;q+EM!7%psWSU%Ql&<)~PrL=mi>I7$5h-oP~KyFqog!|m+(#s@LB@Q0-z_>P(%n7s0pJ`(+2`W zxkk+JNyPa#_VK)AJ?o2(b&U=LXpktQVyt29sb+CBEd zP{Hakipb%0gs`}vj6=|2(JMrQ+lr4IZhP-kv#r6dYz!-`zGTwpPbU4oc--fS$30fJ zTW4g`s&dS1QcLlQS8%(c4pUI=l02~Um`8GJA{KklJGx_YUw6A+uYT?WZ+Yeo$M>!G2pXOK;~#zY-4DL%zCEMm zLbFb(BjP2cNF@uv0VW-Uxlfr-91KD9piYTmoCf5OqxZB*0pu=Pg+e=rjaIS1DYS~o z*F191#a;9DtmBIh-}CVKSDZdMe`wdCty?C?qp@@>nli&JUBR-xq&GP0epUG|soZt1 zE4oVV+a#?*ada&D760K^ghGK?i~m@ID6<&5a(}Zj(AP9!C4xCt?590E%Nu`x8h405Nl9fuZyl^l0TKQC5ymP3hXQ;io zcaLeHQ0O;jr^C(d!#!2J6`szS`suyJzHB>-`FviGBB6Q@^Wd)^z&6IdJKUbk3d-E3 zpg&T3G~cZD#o$5pshfWjn9eDMY^rTI!M-XAxn%oL)DxVp%`F}7iw+}UF6LD?ncCG-qBpD-dMDoSr{JzP7;uRgZry}^BdTl#f}z(3X0m}mYr%pw{tmaT zb!$t@T$`=7*>25O0)gh4fWyY#oU89Tt&aEl1Fd-+YY%L$xmvciS=}CM+guAf;HUe= z?{04O}KC4)5EgFoOTD5h~C@W?XMVwt?{?9ITK#ptwGWGI4@nHfW)o zgIi1|`&{O*VcdcqXgfem8F%XB@uPbdcWmFfWqN9K{a}A@PiwgaPPJ$l%970lI2w0` zoguW{iuge3AB-mqSMuf|1rvizTGox7MH!1Kk8Q-iRt9_hC`c$d(hxv~^YTX|tSB#F z1=9@`psw@MmnfBQ4>biH03d0N2|sVt0;;Z4HO7M}mP~fGuG=o;gn)N@W7ZcC^1}9Y zt(`Ezk|D%F3^)=Mc6|w>hPhqNKvSr+QB@E-`t5G3N2Lzg?Gd%gZFAfEJH)(d<7_DC z_4{I4-d~CsG%7#K3Csc8v58rKU@3T zPkoB@eU`1O-3Y3RH-~GVWy!sv&Efo>&JK;0-T8m-t-aoQ=Y;juObj(xZ!VWg>!)ka zPUM3DBtwwy)yW0H1#l93#OWTc1`uBzkx+7NJ;wBlIDVZXKwVKOm55B+nfpZ@yz8_Z%;M(g~uAQvLK%+?4CW2c`oLSt!G6+|`7r`t*i?zZVDpsJ~ zCF9OK@4EC2T-{pp&|lV6sm!)~pvVrjyuV2I&#>}zvYXyr`ex>m4A@ce&pBH-o#ikh;TF1iKfdJgf#s5S!gYr;MzV}JrQMk_12Mr zYP+aIJxaY6F&~{UW-*ur@RY%jK8H$4z~N$L13eir3BlOHKtU_c*7pQ%7oHC4IyceZ zmxpS!VYF{k|E9J|t~cL{Z^|a);UGEsQLPa53Skn*k*-rGtDq%?_$roO%D{5cLh36W zO?7+@Q+P?mtgm>IBML4ok{vS3L0#>v8_lu~%0GYq!2EniF4s0cKk(p#UGwF>WBti> zfxvLG?`Us%XGbJEp3QE|<clmmXK zS0#U-OnpXe|3={rf*M-LX5tKIORkSbn9NY|TS9Q1l)?zx3J14NZS3jpXiqo9p+G6d znkXg0>#+&^2)NydzB9Y_97yW05xE*yo+e&#TJ`t(7OMJ7UYQ1%Vmu)At&pC`c zC^JfmzX4AIPofAiT@b{BO?Uae$Wmrn{Xm<#gTz?{ZHhfwmi_1FeOn_8|giD zHHqvRj7X@^?1i5F{i?2=G@{aD!8d27Q!Za`B-H7)Xw^alsO)Fr>T1iIBUNw69Jhvg zJa}L0F*w9$9G>ct0Svr&H6^vpmQ?xSPfKlE%BuWO8UruhbEG(`X&GIp%(tlr_jh8b z`CM-j-3BLehc#y3Ah=P(?(Wbt`7U+Cn-peM2>1rP-xsZx+zr#2X9DXR`i}PZVMwNj zbE?v8y9)UsUny;Am#Ff{crG(sP;16ov?$FYkHrDuR$&Hg5u@C8ZXfrlYOELu0&fxk zj;AHZi<-r$f$lu=vrJTnQXES+LNSf8gZ$hOxl$%w*E(tA<0}=y4<%xcs`~4tMoP?fXp|oP`Y;p>ePwJ8>qzp;1T= zH9+`eH{^xkiZJ}IrD64Ek}#W9kT8%>onv0tadf;xK*w8r1F=B6$F*Vhcwjh%4qn$7 zHx2A8Tdd0bNF&4yd#!O`N7-f*^JC4BMU9IlQ`Zh?w4K}0&y?TtL*O5tg%s}LMytc& z5Io}ujj1>m3-~34fx+xzZo`|TZpC|1||?_XYDsn_1OG_tdsLye2M z#mU(XE>C+PHsC`IKc#@O#Ny=M+`c@my6?WWOWy&zkk;DG{r_Kmv8^wza(}M7{5C!# zE|{rznQx3SU}h>t|>zrSN=8G|~vF^gFs zPlqjd*|mRAHPQbXZ^|5Uw)I9M{q2sZxzYQY{t5NKfldgMz8sX1$y{zSpPk5&Jm6-8 zzvesSHhcbk4zL@cOhtXMZ^HwlXa$eL2H;_m0KP$3MJZy8b6TF$QVu3ameG(0AlucN zIj~fz7dTj-%Zq<6oEp4VfiaG(6rLekvw9tzfuYZ76~FyL)eiY4aQ3sk>7HILeNeBe=U%5r7< z4va6g@4#B&#D*Z9D)U{hesyFNz1M1V8(LR;ukXkeA_>?}^o5ykm;LvU&fH9OlNmJ( z)|GRO36LZsSqX9(njujpBL^1TSfC5$6m#;9f?HrJ9-?P5+wAu-uC3DClkJH|eNFx* zuyE>ObbFbX;Ain={}3t6SLxQP^Z&&)Yq^>osXvf2d??=M^Yz9Ck5!=nMX}!>!zimO zHfV~5xHlB?#lM}3`LKQm)Q$q|4>%iiE*%1q%ahrr3!#Yd{X{dTw`h7lMGC298uBY(z8& zBab*#fJ4%NuhFD43iwbnC+}#u1*L(f`YIjmnKa}2d)wD{tS{!%ZJ9P0%25JlIF`~T z)e_gh8qD?PTW+|eAtWby%1Q&RY$&lJ4|94+3`!?Uouiih|biTt+dept#9=&l9+Lzch79aP3YeE~CWHj1daqUmS4bffkDKG7nc1E3Qk-q8eV_UGzx&O$ zpYrD}-7!DGvJ(@vuQ2gD)#1+%;~ESo0gk;Nu^Z$KR})mX7DXXsU4Vizvxq!1BpXst z2IB-|<^XJgn=dNR(v6j%zGg>Z@k3oD%`yOrJ**OJ{$o`nw>x5KD-ccmdM1q^#av}d zCEP01JXX8g3Wh)cq?aly{7cf{pach!MQyg20s%vzjp&MT6NSqc+1C+M3v;L^=Jgn% z<%zhxCB0mPF?NMsPB#jeT0BKsH1glUmL^->t5&&Oe?eA*p>0rzfPKBRv*YyX_S2`^ zwwF+CG&kjp#1|?(2fNwd^z2z@c~#4;x3;{;P=*g+Jd<*#V(H3Wx;Gxa$cyY>;eRk7 zvk#~-vyQ$odIQq)i{Mo>VVQ*-Kzm=>hM zq3j!!H#8(_>tZ`kks7eRaJWX$jG#1rkoby)#Yv018EP0dP=m3$K=VSYLGuMTDr*1U z>C=Z(M_>EK)9FL)M^dNW@c6Ox;nSyahiv$8`e@VX)S-J{c_e+R=~(J;?fosc;u{3c zcKOHr>u|qn#4Uncf3*rI8+36a<2j&wRM5rO*UL!}Bnx*3EYB?6EUTb_Ha<=fxBtKpev8g60P_-G;#a~BHkcrs2lB+peYKfpfBe~|M)n*jQrhB=y7 zt5Rb?AtoXx%|rJRaqqw#VFTO|dKkbXc=Nef#GCe|p)z@xM+q_Fge)cTQ9;|G9YM8` zv|nFI`nSTSTykAw`(uIkyeIG&sscg&p(bj$9FDYx!>u>rW#d>;VxPHtF?+Lcb9V8g zQgO5qvauaCSEH41xDv&&_3=@`_;>~-kMPfg>+3Jb?^|81zKr8Gw_y5DU@6A8-aPFRO{lopahaci|pQ?N;`o%9sKUVn^ zHhA7nZjN8zcS14(fvUx~;Jf(xcVGj*C5~ZK`%u8p0;mBzC`F-SEn4}7FMR6Kk3ao| zwr{a-@zvTyrNaI?P%E%6)}LX38{`l2BXwUZ)EU$gUF&J3ub{e_uz3O2l!T~|w94X< zKS&pB`@*Mr*Qa&|*cWSs0Q>7oMIL{U!F-22SImUsK>jL7RK#-}3af#kM=ldc%_D-Y zN$x(h6hBXL1}<*dsf`(ee?UOMaX`hf!=

    0!~u$&56?W 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%0A=-`Zt{0Iu7=q=n|{Z4ahh>@J0G@3&H2~^{SgE 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 G00004$sLCP literal 0 HcmV?d00001 diff --git a/marginalia_nu/src/main/nlp-models/se-token.bin b/marginalia_nu/src/main/nlp-models/se-token.bin new file mode 100644 index 0000000000000000000000000000000000000000..d66c8709a99c1565e7566d435ca8739f2ac6a979 GIT binary patch literal 219795 zcmV)MK)An9O9KQH00;mG0CiI*Jpcdz0000000000022TJ0BvDzX=Y_}bS`jmZ*XOD zbZKRCP0qV&!!Q^H;9XB4$m}A?7pVaa#druLP9dVZlKiqoqYJup$kW$!N!J7CJ7 zssg@1#Drr?_Kfd4hR5lmFe8*8m9+KNFU|A=zN|X0axhmW`um;rlybpuS3|@*ht{-o z;JO2J(%u@#rTK!CZSa!55p>jksd@pK;f0R11Y>0XN)82*E?X%}N(eef@bmHWpVV)b zvTozGi;^R_h0?{w{zy8NhPEJ4l)h|E&$4~z8)00R4`ip;!|DAmG z+mj#d-hB71{Qa-#Eszc*v<72x^h>XJleha%l%)EPOs3KyR6aODE;_hmIk+*xtt68K&a*BZ}Gvt z?aDQO?@peCH`Cl!C4SEGp+CnDeGoenAK!rGx|6f;<~E+5 z=lbMq_hu_^85%iz)yJW!YPL7Nx+~}kiQd18%HRxnJ#1>)n@yHc!!YjY8r77CMd6H|ft>4DB+7R>s_o442mQrsM zHuTb7eK_8H@;toRb2PEyX1{Te22HUq^?+q~xbjOOU2;kuW_)eiw1+rb@&0`!WAXXG z{^AXV%;G$i8{BYMNxQ#~ZIBx34qpXe4F zFn}2e;SBs9nizuJ+uts@*?jT>3+vsXo`lNB?ber9k3Qbv`*#@cA67bARV&@P2FY+H zaPMJ0IcLM0GEPIJ+emYf6~==(a)XtwnC>RK%};LZpz`$*d+lT;4bmAXPktQf(w8MV zzL{&#$!5YD)K?vjo}cVO$Xf8)J6y@pw{p6|aL>}qauTXv`Ar*qX6(CNEkhaCy*x0- z2|eR^A>sJhmQ88OAWK#sUJJiugCe_=;RFAPKlgKpgC*@N`)<6E8Li~LDs1I5>vjBf zb=}EmM*6Dv(`c)>E1mYIl@N$Nw*klfQ=6_9(!_n_E3pMae(ULO#yw^Bxs4F$txU|B zkR;B^sW8SV<)uP@gN6Tb6x!XELm(?Uxr`&%#rbY!0Z;Pv(DiW)LA=|xzm+%ECm&z} zY+}j@OYaXHyR;=n+K%HB_s>l!kLIt~Ja`2*k3Z(jc71npDZ^E9XW*)fZ|&q`HzoMo zH+M3?@{;Wq3@-M&WDj(#=ZOP1)m2wZq0U~il@D$v<*nb8(s-wqvdi8PN%_UmrcqavJ|fKp^kKU$I zIWPF3mIor!m?^l#&Gt~D)nAX;*L`c*#raE2dadpIy2Y^UcKux#q)Y$7E`42lLqLLG zmnT1wg>qZ#P;r!GyN&&ego7PpXuv3BO;2_(*o(LeV!it*GMGu2SR}b=-42i-A|Wk) zD$xdX?9brG;GPv`;cGVe&y$eZjy#m}8anwgW3+ZtJ6v;n-A0ZYX{d}NkBwEf*+UyR zwxqy6m--8z{LgFo&Y$9UE{w0Xv8(9|bsAaOqlLceuvf@Of|PbM2l<{0r9qXk1^n(sx*t81AXP?z_d}V=wpR&w+(hXb4@c4j^Nd?b`&vGtTf!hMA4E`jmfjL2hg!OJXd6o4 z75482GR#EYZ?Z%r4P3)Ahw##EI-n5pz(1BgHX|Jnx1h zSB=~hMr`KoN`LWNEmaDUNe*0er$$F|bh8gPlx141fykfaDbQR6*^0cNg1}!l_`8vD zUGuTqE2DxqP0vmB(9z*kR~Vpf8i!Fw5NxQr^Czp57f}-H;|$2=0p5kL*3c26guusM zpZs(w*_(KbhTcNrxm~F)H*qD^*emKS{&xDJ71$)m|6tode6~ZoHXyMtN#V!$9l&p> z1Wu5lHxO<*h?ngFe=+hcxKUhPp%vaOXC~or^_GnnI*L>K1(~V{&m6?{SXCm9E^Y-! z+F)+l0pEji)^;+oTkO|zlDF+~jL~Q@VH+a?mtJu87I##((gy4&`o5Vu3CC~)nPn7R z3E%0jgv8>;wY3?HEED(7Sj79erWT;LVG1H8USeXlyB_P*zN?2t`KqOyu*(-{Yom)sT8OclyK{Ey0Iw0|b|G@;-n& zM!ir(Uunq-qJhwviG*l#v$s91IQi&-6yD>tH*uCOMGR}OMFukQ<_Xig6(as-N3SFuq4}8O4Gqxk8SvqS98fn`I=D}nZ6g3SfdTs zP*7?t2<;{3u9ugLao0p6>%x9!JH%9zJZ@V21v%BPWtIb$ziJur5|&8y8~nvWsj$*+ z?$CrAJos~*0qt&}-EHrLbxIenadL%yd)~#*MzQ5c$B*=}l`jwD7L?afyE9!BGQ%JC zfp(j!#9qZM`v%n4Q>5yl;0Yy1HS8ym3z(d0ABx+X(F>=q3jP~?ZEw;5MT&s(zagJ) zv2Di}*kUBv&Y2D#c9Y&)`CZggK(qC4j-JKF``xP^-J~>FiUeF}C%|{=D>M{z(1H<0 z9W)ypu&b)JJv{3C5ESFNN;tZFXd9)KxQ92;Z*=o1r7bRNMtN42{_Qwb>q!(f`S4_; z8n?8kP?xqQUJ7?pL2$rM7n$=VZi$Pj>s#K_fSX6*SHwd*g0W1b6PF6&8!jo)nldfV zzScq5%O0ybI=;llfp%|4Xt`Y!7^MHAti?^T9$*D%FvHEyrD2UoQry;L>|7JatFXE| zw0Hx}X^uQ!xpvnsmD$kwwug?~P|V^JE}+qr)AfEYWP%N*iVC6_D&k+Tqz=ob!J_K! z9&$x8v>WX3QMrz6tv|S(My*7Dd)KI~J$T1Ax`|T4$&cU5s$_I#;aI}_>Yj38i`K;*NMukEZ6c==`FJOLhY8osv{+Cj=qF$~ zOzbUYYyLToF@h7eeplJK_Bt2Wn>xv@fuiI5GrSe7Cb%`H0fU{R)HwV`rnTu+ysw9- zG^~7(p)SVFjx4L@z|1=5*it4K3Z3BT%JZ8VKQ_k2m zrw2CXT{$_)s0B~m0-Un>O5a|lafdOhr?~}>%$WHZJ0$6jq_u?!<}MOifc`3{R$@#m z543=XLFB5q=E)|uuUw|7+;9a2C@Q71U&{gg=l=PiYUG=_|K4i&hu6=*Vot`VL}h2^m)g%nz`KQKTyI zvH`+Fxv?82s)lh$VEv7kc5+G79)1w>&cW(2ufr?O4<(M$Z7meSxZ(%OYY@9iwgK`kD`;k9r#zpLbTTty*HAePtDD@Zn=`1PA zzLyKDu8_dZEt$pcmGGA~%(}GDnE|x21E(Km3Msoc*KIZvJaa=s0xTb(xVS_7HW2hj|aq2!BEj|LN?4{*JQm0DPIJIYSmO!aO@5oQfNT1~a`5>ES`;t`eu7~E*{ z5>yMRRmp3dbH&x6mj>_RoORB52R$e|kk&P}{zld43z6jJ_HeTgFlv_v6R@)@g@}&H z^%grXi-62bgiP7ZLtnxt8Jo~z6Owkoo;uMLC!4tls?V05OlXT_0^)G1ZI%vu=m*ly zw>3NiaX;QF>52LP>Nvn_-1PONw%|t?_Tzh3i8O0yr6SzOy)&gbEUvgt6NA0oOG7bs z4Q<-;xjjgC4`#B5-!7<&&=28g`3};39F2VmL2**MYCpm#bg1Rqfg*7iH;T2Y5i^+8 z+)Ag^t}^h;GsMRzn>-giENUJ%YHdrCiD8@fkg;^`V=IQT@S~NhBQ=!YNt!XyN%4K# zVDH#Wt-b*^O=-M52wT@w(h!N=aOUH2)d)3wnGCCO5O-x2wr{} z-ozj_-fPj)h1hcQQmt)T(V#m*$&&uw&0GRgRCz_@i!&^n2yCR*$xGA04z1{kr^di# zlsVmH-(zjwVSJNIN$tcpYBaDn!DnTu?|}wYnk!}fyb$LiHDwW(gv|Il=p7>r45Y0w&Z3M1*7}HEjeGi4WF>$| zqCoxxw!}zJ4_kwUH8z>d3Y(A&=q|QKufwP)GyH-0q157fD5nuiWW1s2L)&*Plhjjp zjX$xujZ{HI7nv{s->Pv}I=k3{8Q(Atu6Io^ZwRmE$9Cxo*l*NI_uUNEdJ zOf1PI{aj4j*$#($@bX)7qixGCQpBv(x3dM0+QK$0D_4H?;7fA2(5YKKb5t5s76 zMtG{XBjm+TfBgyVUDEagbQVZ{m!d33U2~FkimC0fT)(3EhfevpIqiqaPh)s{uqF2p z%b8nlCtC;=V(0eXn54O;nNr*+vgQ{OkK7QjrUIOnw(W5P59nq<`vN*1Fm?e06F__W zS2zashpmk1j-He_VudUjAvQ5LLM$k3uMu(yao+S!v^>CE7q0OaJeiUgLW4GU_{4OF zzk5<^>_KFa6ImKNXIEsaG%!cTe`!|50f7$}%7|fKlsn)V2;Dwv0FMx0 zB2+C#gkCj45g-ogx136TlM3q<7Kq$2a5mk_66nCbNRFEona6-G4#}k#WK)qJ>$*EY z>e3rq|3W2%u;qkJBGS7jK+RN2jL=?YZw|sW#F%$Q{q>nUnd5pJKry?$KZn8C1-fP| zL}$mdAfDZL@{uqxcNr%W`cAhpT9dY1X**qM141FLDq=7 z=_&SY=_-pHQO;et5Yyk%ml(THsRSZ7_(9?QS;4pFZfI#Qt$mDyfIoIZ1VyAQ1O*H- z%yCHR%C-$w%}M&;M7gk|$v)CtSF7t;+}v>ujD44zydr6R1(x;H!h9gken2nq9Jb}u zflZuImyc}$cWXD0w1Nw+SZr~2<8}|*1jmPQ1J3PgGb%B>hOX0H-BBpvA>1pB%#aV= zj?%Pyk3HbP?4XyMH#oH}Xz&Yy>#)JyKxU^<)E?Sd7Jl~Vbn?H{$NL_VO|L5GS*8dB z!}|WndME-E)<;igsaT{sHg*dJ5~T>VA5k#<9N4WZ^VjQX&}5vCg4^h3u2**HRxhp8 z2+KH_o|{;tlox7%%{ZJsxsi1rw*lYZ^F3*m(QXM6QzZi-7nOL;2it4sz#a{i{$K0= zCG>_vG8di;FIC0j;b@im7)()OPljkzO5ofh5F|WgyB(**E+Qr=GiKQc4JvZTx9r?3 z`^!4+G*KCu5yvEB0Ph^guWxm54rt5RmzmRMJ671A_$z9mk$&OpiCkFezlKwqf>I%L zcHmZ`SFz@%awB05cH2s3$n`gsF>|xGNF=Uo_oD>%>cLXHv1iniyJyJ6HmW-!xr4FCIf#;# z`&mGCOKFdB4eg+gyXUe~%u`}T>Egh(uA&rNg6)AE`&=~Yrk0Ht^E@fRVJf3g{|d7k zOsW_>wD(4&Cp@v2;Z5C_0sDw99XN7fyqswPo5p&us0}D?Pc?8DI}vSSRLXknz&7gg zq4pKxUl)4crMXRnO0a0gI93xF%w+}l}LYE|Kj2sM+)$xF_11c)Kh1$2S-4t?aeq(>l%8v>=Vtklb z^C@`ZP0fO8T{e0e}!}*>;tyH+~ltDqedO@q@JqK4spQU7qacI9-Jxs?#Yi} zJ^WV~;D7mZ>V!>MF`^P8DX#nzx*6$<7UX~V1oYMnBaP$#!p&KYDhL?aMWJFAa(u?6 zsNyn3H}xsUtymp^ID!mSW`TlYqC|+4$|dWp=U^lUHR(R#@CIwYpPAJ>f^dIf;_x^| zzVmWTDNDB`UO`_zV>Hpe8twYl4$zoCLcEYbfai>hNvR9Un?-6Ax<9%O`^;ERHyYBq zuWEa-d!3`avcY`G1bhY!4+_H?#+DBjA# z`9vk?nfN^CgLoLbm)SfLi;y} z6#x+q4J~GWA>}+3yANgJ8+2QD^;L?4*0mk7t7!SJRz3=_Z~wYLAAgBOv75$f-%%1g zl>LgA3T**y8Q`Gf_NvDST`rSFnO{n8)aDdyKx9`sq4wqjJM=| zhx!_t1`WW%O7qCL^At9!;u(^PAW>0|TgS~^hW6L1)U>^Rttxee(>wAS(z=DjMAG^3 z!>~wc=}(TmIP%pN?}R?W{|jjZd{kj*=Q+t+BIHSss%Q(FjjbjUPGmKg3onq2?-#P7 zOj>_IOKk4vl&5SU^uekVrg&7l{!69UUzXA|3Pj350%_!CK!zqYAm8rN+DO)dGwp< zI(wYGy7v3{o1+3x!Ary$zJv|-PyuubAcG@cW!E3IZNqplPESH+5~gW>*S2e}ym>ZMVp=nF*xDFTJ}pJiKvZj@uQfp7xD$)-b5z-_pI< zLyOP+hQgxX=WRRqC%^*MBXmGJSNl0UGt~_u6kKuXY1DJ+PZ+vQCEb*~Eu{THO`(ui z2if3Y?|EBErt^h3>c_}%J0_UphM7BurCxJ#py%aw*~j8TpAppZs!-`u498dz^q7Nr zcuG}`>OtdFc#;ZG&8g8 z#OAFEOR*NO9aq@_S3dbC2sm-S*?{pP_;t#xsKb5(*%llo%oN__6Y9x_#(SA%yVg*U zmu_w7{&h#2VG@O~#&kGJ;n8s-z%dv*pxWzd`Fd!CmiA;BF`s;bzj;!Z1l66n4PmhZ zfU-Y<4)@6`W?(bG#*D1%+kI8fPLfw-Gh8Z~BqQlWF5nz!!V-HfW46C6w?<=gBJGu* zg9tF?PITO$&!T;wuKblNwEH59DbQ zVWWfrQTG=s`-xeDZ*A!xaEM>Q4G;*G{k%KGEgQ!CKyv}MzbM*91>skpz=`6$ zk+zyC8l%$f`+1*ju(x|C4w1J@udVHfBHa)CZ)-t>9X1&xWaRIidD{Ws{}kMQ#Oipb zucp(+^Sk9$O-(fcD^C(VML9Xg&Q^`0=%*I8wuf_TE_-Z0jM#$-rDUX4uc27k4-BFr zD@dG_5*EG}pl=vE!KUFfbkmBJfVVO2ct{m z^cBPqOJ=lctWEysIa32 z|zmCH*ji+7z!EKqtxdlX`yHH3jphX(lRuQ0!nQjNqu$}S8`MxB|tH+-99fA4^`SyoW?l^@i!OVhdN z^=G_=Y_>Lc&RbkvLuDBZH)j2FEN2)OOuCb34f-C!!bOkWTY`n{g|A_0$wnBT|BYRS z@X;_S8CMWh`06uz4@dmUej8ny2ucP&6Xx?? zOQb^MOq6&;)rQ25P=B@SLq22fx5&HSAQ3tWe-IcMXodZ$nhZ@yv??sux_GA?+*GIDFzz zFHx@sq-5`eiJdXjPCk*nsURn%Q&(cWdhn}nsc6^)V648P#YTd}Wdf4lAoB$2{eIBf zL9hcbfUhR*ZwyWSYHuN=CIiSA+>Ck;1Zm9mvTwAMsf8&&4jQ#3#D`hhMW$Xt!yrOV z2m@u&6fP*A{GH)p*oZ&P)dIA%T{dBdXrtdV5c8{=4c-ODIx_qml=BCjh6z^AKkRuU zt+GNRFhc%e%%7|68WgalzvD4O=kZW&6GznpM_^V}tAI!Cutp~?1k>J7{QQEdh3q&b2JB0C zyO~HWKUH)u@vU!UpJhxR6nWnlwzXzW5g5eo%?N;S48Q*YWJL4CG_K*I5#%@(_dt4I zmfD~p-L+cIakx`q19(Gm0d1mIh!rER-|lE^#J)c;j&;{Z*$;+{@Fq9e;6r2#@W)Xg zt!Q`Z!McZcyCN3%rO$8`GL845y2(H$t#)ZjEQFq z45XxP!CxIWj}h)SO-7OQS#Dw0jM@3XEwgbxA9w*g=<}XG!pWn}FWv|tX965<06qTd zlUJPS;c(nCOxougl?-Z%kF<=y2%`4xU^f&w->;G2o?sLnV}T$f`3p$VX56{bWicRT z{d2^!e}0fVm~;lV7N=Wq9Hzo7ELFY~zED<5@ISE|PVkp}VV>2uF#aZQnA+8HpcJ_s zgZKsf8I=G6JcmGVf+v&}Cp#D2igsq5otj7rJ^_5K~#kQ8?bLLsyzSr;SboNG81M@f<&-^H^tAEbL z<8sK>MLjL~Y`eG^@bv%2-=8_}KeT3!6Z4T=`zQSUzlAk7Njt{Lf0dKpl=FU46TF=K z6`L)4pZxqa{{B)ia|W|C$;r>MPOsSM-3}syXxo@9j)y2ZK(Oil#(GObk(<3uy-|9l=rf&T^a$KQoIam#1s>(~93Y zpR%N?(M7;zuZl)~V{vqKWp8dJq;VV1EVGv47*z1HS1IJ!U=cY)|6>tmg4zsGKaK!ZWJuzh)bi)b(lI;3%|{$XE2f)=j?k%v04x6F)=92L1|XE`!z z*SCC%jbt01GL1oYMsZlI{6xbOg?**;D+fQq21b)F^e~BS5p!GZ9vg8exZnIDQ;Xns zq!r;gJtrQ5D-V^P@`GS^34hg(Znt*I+q$KWg5AxUixsiC>w5y9QMd0O?WxKy$3%HWNgXc{4D>gRcx(Mub6gC}FKMFG&pF zm7Of8kkBzi7a~|ZqntF#MtwR?@(z!fUs*AAht`~Plfh;<7pC+goh%wVW&0*H1D$c` z7lOG~H?aOh^4`!+h-IeQRIj_VxlbD!CbBDq0Fh-rv4fkfZO{ny36>)V%$h&*$bE!1 z(go4Qd&Dwd@7;0Epk#V3OR^VJ*oQ!>kJJ$S5$z&I14g-WN9r|7=zJ%XfdEE zt(Us81v*0oL}Hx;50(<4d*R6{dpH6=`KOiU65eWRZ5o#(`Z1aUOXzMWND%GZMqXU% zj8B9b&Mb3eW=wEWJSu-3oh#kEN~@K*_&v2JahE*U!(bYeab1_4K40kCEd#NE1z<3} z5i(_Rlx9B)OH2XQ0vKKvmyZ#co>qs!K;vbDV5i9} zZh=uy`LC63aTC}~)lM}MkDlf{pGjnCzYD~0X~t>sqtI7{Srx-si73-vW!xIYJ+O>l zKZNM@B}FQ|kq${qxslBTx)vh1VbC47J4Wvh_nPDZ#Uyijn87h_I!XoMjk!TZ%+eH! zGZ&9+OOWW9KoAs_yu~J=DAJx5`p(9 zdfhf8x^d4Kfh-9KMi;Aevhr8mrjxbb3EP5!p;8gfq7nq`MA=4Pxa<;CM1xqeEc|VL9$nps^ya&It@H-2=b9LO9mD0KPmoW8Ou0~q6 zQ7@w(7TLB&q4XjXf0vwXwftEqEP#d5W@#AOJXox1Q6W<2RN7i}>K2;*-yAYA`JvFH3gxInQNw7R;fVUh*CSv3`N;nH* z%CR8?!^OaB@=LRle#jI+#*!xRXu{o4q}F|bMRGK;Lsnh6tSQkm>JZimjn#I$y{xA^ zy^2*3y4u!_${CdsO57?9%22VPO*s->>P}*MWO3OXS;QZoh1fr#FoLQu3B80xL6m92 z0BqQ3phqaEvo5a=!lKy2)h9z6ridO#TOgEquOquUF*{%y(i4~L{_p2 zUnQ6IsWE9t5_EVFc%Id!xoM)Vhn`5 zXrU^fIiQiJhxUdr;$dENJxJwEYOOyr6BS|4eZ!aASMx9 z7Uh;ijyiLG4o)O*Y7`$KK%vwFNE>@Z#$GOpqj=7N8K2rvYRXW}JR(u3YD0^JHX0R1 z$PVOD6l_|W&@yRL?_6n796%153Q~iK>Q}KBSz7aa$0W0o z30>=vdmYt3;io;_p!!h+P%YWLWO^V+a+#4aGbIbf0xneNKd5xpv1q_0f(0CY&xfo` zj(%X|sH5L8`k^+cp#+{08aFw?LbU}TVH2Iu4mSI$gUL=mU@T8JMn#sk+%5?XxPM=9 zk-=S73LLU-D;>y!xrL1ik%L6e;D*4T#MWE4Biw+H?yJ7s?) z&okGpnVri+Y^UTeJUfDI6W)TCGu>yU>Rg!S;?CG9_R zM`Y{&V3|)vS7r;)vhySrARmMZwMj$La@Y+zDR^Ypi-pmfCThqS4N|DV*6HMhHhW4W zYT8b5L@&Jf(AZJ5XI*xTm0E0ZkY%S_!earZmS%lN$W#K*tVc=0mc^IzKr71#*}R4u zaqWtOG>w&ZAf?`xnolct0NmMLtKHhJv>Aak21Jyjmd?i~uu{whn90cqLO!~8wY(mT z=W!RO(}hx&l+In*0apCfa@&sogM?Bwv*h6+%1yQ>(gc%%4*{P#qz6hr3Dy3ZX*V~p z8o^aHsYP2&dhW6%de1oNtn5@sY-CN4<=nhBL@2@E)6fQC*DGpeTE zI-tlElO#BprxepL+oQmJUQl~OQ|eq>sG}N#+Cy7uj%{g@z3%*_y1gHJjECzHX=l3)X~)zsIMj=?xGBrRK1 zzGy-9na3USr&C1l@2^b>#M&Cs(iL& zvK2$iT`Cfht}uFPCMW{YxisucjY=AG@0c3YS^y=R!Fxb7AGC^+XVGS(Wlr}vlK?M_ zYps=_<}^?%BCDV>VT&--&*>;ZksqWRSsLO9MhkO_M5L&Fnh1#uX(7%K`F=ntTh?$w zr6WiUpa44R&6V|hmlc?dyoCafIsodM@!&5bg4lHz2!P5|wH4EA2Msze332uU63-$R z_*89CgkBmE*e6!7R#lP(mMrut^IScb3W%Bhj#27?J-v`R32&BMe+#Q+-yT-_Kk*$} zDrNN#GymIh)17Foz3N!4MeF)*)n*{Es80aeb!A&B6~Fq2R(qqbt6Bdfl(1St!aP?I zW!r)+3ma!-oCMz30VX65Y9>`$ahf% zU~y)MVT?ZLZ&jR;%cFLtNY`i#mv+Hj&SstVG7uHa=g#r!A&bGZ*rri%)g~#cJR~PuIAa?L>wqt1uNA}~Wy&6`=| zKVT9#G8yR15R^+kH)!C8SQ7-@|B3QT1fA|t)*J3O)#4ukIVPPccm_ocUVi#-s1?nS zgEi7JJWHJS1D5HOj%T@@zCZ)AtYZ9n?x>r!Ab-g zmr?ZXGQBC`5+0N?2n;z6;gv2B4O!OEKT|ZGWmF?V^qC`7YQN{BTRs{?o%x)aKjp>_j}Rh{vqQ$})Yw)We=EbA}IYgG&?+8Q@;i11&X zJxyq-6lP)JJ~FAwE7e~ScZ^_hj0j<}TT4yZ4!M6$3#NqqcGi686m5Y>GT{s#>~bYL z%w-~Px`-p)p~M|@H~3%o37gK&(;6S=OE}&_MApc8$3ouErQIC-3KS1<+l0(St+ob? zS8Jqcn-{^jtV$GTUn@wpU`=k~t(4RhXj1Dgpw&Uo&{(LE2y9y>O(~+~$?4`($q-{? zc)W$V;{y?Q>}a)Qi-ER5jX2cukQ-vRr4***iem%^kflAvKw{f_Mv=)H$A431DHbR$ z0Fso+20KrCk40f!1oV^!>G*$$@>@l2q+j#EqJzDfV;?t$7*D{F|?2mj*5b4I>BvK z`sLGG7221K#kU z*N7NydX}Ymi3^8_X_ZB*A84^<$-C?}lgF=SQDx8E{qKLPL3ajLdIu}iDHnqR z6b^&NPvXcDq_il|nX-{~Rz#%jP(xk6GR!B)_cspetS@;o8eN`9UYIPT-Il#{2xJ5*>R;=uimNh?R5@Mw0`~;fsmB(}g%3YJF^_*!_f~ zeW4zh79B@g&U4sGGh-DQ^*a;jq?cVTRT|nT4I9cX@6^P2et#hv+ecW zwH<8ipdk#$E^c`sJw097f%dVqAj>6a)Q;iTof^?cltqRUa>`Vm^JE!=$AzFP;4NJ1 zrxrV?$Q~X-b8Vn3*Y=3L^LfaLnFjj?+*>2R2CFC0G#b203Am#7%mXgZ7%eDs0FeBn zeCvX_}U# zljE}(DOGB!5lV8?z(%`3*O(^sZ}w^oXW@;iR`C59*Jd=C*qY!Fb+?rI`P5zh{7A@z zp-|hxq^Em2gEbb_sHRB91Wp4_V;Vd}FRsm3nYrpYz+yD+N>fR!M4_)^YL+S_q=dS} z!)_D>;Oc<2W`q)2(Wj>2|2V;P^GGU- zS!xv9LAcgO;0#Nm%5L4z2(i#9NFfbo!VKY##2m>esY^)A!M(q;pR z%%V-jOo~j3VUQ_>Xn1Lw=kcZcJ z*$2zgeHwiuGbCEtL8(fdv4!Ou)70LKR{wB_(uRzVn8PjRg8lDGdrP7IO2su5IgmmU zJY!pu10z;-I1aG$e;IXx8%tOHn-}eUqIGPt`{z5~QQy*}Pp6@mKonlA)+!AC1gp}b z@HWI4L(cH~!Q8xpg--Vb%U?r>g`)$B+2SYfbq&yOuS)VG7-SJ$>gZz=5bHg6F5QmZ zHj9Os)e{8C85-@gyi^;7p*Zy5!jkyH8I2O<$M4(SphD!5@l&(&KpH@2XI%wUfOdjf zs$^2+wm7UOvzv8BHi?L#$ z)!~)2tt$zmf6vr;8c?9mf~o3>LHk7$)}UrUT_o@Lp2=4A00on^$n;F3x6p{pm5dTf z{_%k|R~6MqS-iPTC{ScR{NCYDUC*IBw(0HLvmKzFvw#BGnXMgk9wB7))7}4#3~X?C z@#CFo&D;t)kXIC|HCjOxXNVm7=s|m2>TlcPDWy`8n1wN%1Fe9#7E0b5!_s5JY+PAp zkltstGD&uzm%p6UV@2X*h<-i5TA79H}dVaagQZD2yW_ zF49*3bXIT2yn(y?N6#Cufh4QAzIWDUF}#{t<&RmWl{3FPV6(Bc3wK+eD4Gl41orp2!rr|k2_cTeVk zJd`0FP6&cHD$yoN<%94NivApBf=I_z}^IQ^eBe!e43iJy&!m zvCBNmG$y3g1)EMp2i>zoAGsaHX&E0+o5qNx0BMTlfj%`1{}$laS2-}kW4dTyZ%n~= zvc9s^K^mvkwDjB%en>P3anLwe0j!Zx#(yitYfKS%6at4KJD}|Yc8Ve$f?@?t8{Q9k z&u%a^FKNaNy+D!3*a`cy|Kz@Uosu5N}Y{9(7P<9Q@z?s1Rz-@j|y0gxabPOHksf8gJ zZ!x%17@ox9LgS+v32mMZL>aQUt?a=DRoxvvsjB_MHR3uMyCL(i+XDvUJ&@bDh-zVC zhhbzWj;MmctStH5KEwpk3hI!SFuV)Izuq&N!|r01j(XBPT%QS(VhyuwQcLfn9!QHN zmS$AP3;jgUXuL%gTk(mN8cByRi^l{KHtdj~G1<4{R3R@kD;P7Rftu1G0*KQZ%7_IJ zCC3-DfFMc@vI_gQb3{0p{g~SIG{=D=9338{yekS&R9)K}dtk+NK6OahKiZk(GkF>N z#zBiJ6?Z6#iK^XE7s7;9>Q@KT(UG-DuFLZ+ok8Sj*U|znt4B8c0FF-~`6?dS#)065y)W>|UZ9 zYT@g8&W4AX+xCMJ7!w=v^W~E1FxTzhMaLCUY=5Z5$M8^i5(}1j3w*gOWKwvq-YN_ecsxp$4@)be_a5VbfD5@9t7 zPxzW*I-#Z3@(MMg7IG1@QCvtSVjymAv8PN2Z2D?!c?5rovt7zTC@O*?o)4gT(w!57 zpR$^+`jy5galoz>mZld_KCm~zz=>W%KuWRcLTw$lO?$92Xpta5_`%G;WQZ?mTnYQA z6>T6v(N#*n1v)wh9T6Jk%s^Bl`MJg9(Xg`?#c}y14yrlC!r~W($syf=E`hNCx0+R0 zsYWyQ(#wNuVCh!p=w9n2#UYGK<->;>0-gz_-2)+8)!h=7Ylp{%$|b1|O}jDjo>*aw zan-^=i63Dvz$%;-n${#v2_3xA3*b@|Z24^sTn9p?LNzYHtQPGf4d4 z1t7zff~XP1jtOv^7%+mip-4Rd*RUYuGexE2R4WkKx0-H^{p=d%n_XX07cDC{v6Y&3 zvXk6)7$M?w2&!inF)sN6F`SQ3$G~8ie4AG8YddtWyaFI;!L2T`qYL~|6{>&uC zgaN5eEZONG)&cGBF#~EGu!2=E~Evm3nDFTb(EHv_Y9Nx2q8;ekECP=`7 zO8{1EuTy&uv@@ANb|)0pzof2c36qy}5o^Y27RsuiuhNh2Oc%eqXYnys1srLeZg~S-wBH*F z#ba4PDmC_{{zQ1#pNZ&qZy^b0oB$C0bMvBEfTHmAiT8p=IQ0ZM?|4zKRd9Du%f#X}Pf+Aur#;mfbc>bje z+e})Y(RMmLvqK-@@XFgv8GBv25eee9AErT|puR4rRDiuE)||=vYCr?1)H#Zkau>4& zv7cRqRqx(FSKjQ7XTuvnIc})V>O#NFZ5}~YKP}lHZ&gMPrb@k4&FavrmXBAdPleqQ z>>$l%y|Qzt*Vd8ftTVJ#Xml&~z}9YG6Xhgm+KZ$)+oi@1Hg=$!KSy`gs4CW>bL>>S ze!9b7@S7;z$9@p2j9>& z%yv_w#*@Q(B4#o7mOnyHrmRJYjI8^RB#zPtybKvYmnHxUkLsy8z1*n4XUaZP2EpYL zkxl$q#}5Av*$GKMVn9Bi5AeS~rginW9hD&C_Ip326PbY^JOlJUnYmBtPZm7U>G zO4z6BgftZ7zY3N41OpK_V=z!z9p=}p;#12TW(}y7^qQ!3yq1z<{D-?E7eMsyl$qWM8mhW$K+v4Qv9 z_&X#j8(zV@Oq0E5Vh3O<+owEZkNtF{g3CPPhc(C`0!*;e98rpuh?Rt0X5vzKEWB~%IU(mo z8Kze2hR^jA02ncY5;k$Q!KiV@G=05oqK&?++8O~oiaRi9^w%Zyo46<4@5 zMBU)zLLR!m{b1@7*}u=9y45(O+BPNnZQe_!?2UHPRWb} zQTUD-+9xWK7p(SUE8TzshlWUzWh0)d&zoGbZ|AlHb6dFHCc{ToFti^(bT>;3#YNUQ z5N@R=L!$9~d_)im5Dk-S#G9*IGOaGrP4HFFoNJ5 zm=Awd-LgjS!x+^8T>VI}2Kycr)IdR<>_c>>%FfG+DBXEf%C!sOPEbV~GK{$eJ48DUdP-ighy( zW`QOI5;9ZFCOr3gS3pWDjKY)GIX1r;u@E7zB<9J(v_5NS-?^QdJr)-2kq{J5aM2O z*`E;Rd8D(38YLs6L?M3hKy+dAVECxTRE`-ad``9b&^(gk)9tu~U;*)x`QdT0b**a( zVx(OLMsMX@+#(T;aK;s>*_ZcL7z#qD<6J$>fn#LU7okK}yU6NaaT{hxKMM-&-u&sF zu(99mcEJM^GtQkvV|txgo1nsM{U?prMVwyUXUqhIiS{@ifJodMUPRWD*POTO@=bf&`CKaNB zEz;#zGRnbZRsnaa-7uql>hBiKcU5>3kvuDPPuscAc2*!YRyM><4c z`*Bw3VTkGkVtvm{xT@{4JM#k7cX-EO%KX#4CbDEAnDU)!N-s@q2gHw$bUH>1*LY;# zn=~{^>qF`H-f_5EkH|t1hK%p3L8w+-@p@7#7PwZOksYx;z)hG}Vrr$_I=X-XZ=4A08N9KuzOklVGi(GWtb|r`(3G7aqr@ku0@q!AOQwO zs~9*YJe~=|>_vF9EXr3Xfn1@&BO}TUm-3839`DH9M-*+k$Pla<0RXw+BCDqisbP-4 zknpUvtu8W@P3`?k|ECaRMtX(hLI8&C8wRo`u$53FG9W3UZK?6ekeQORIqu!qDG?0Z zGFwg}C-*hPJ<&bI#dB_;3H0)-fm+~-MKOGFoD_y3)?}WGYy%cz{hE`WLU#-^1!|fi z9!)JfUl$&>q9L&7fWdK;(4;;IRdrd&g5=QgB(W@J5gRjlBB)}@zeCz#XL|i!L5Tct z@wH*Oz&dP_p#=KiJ{X4xy|HR6#* zA5dU#_du!+*Ed2F2eM({(y_so#4@X?f{xy_q}&Meu%811@`w6Hjd_gl4|aNu&|=Xj zdI4H6vLL3XYF!jkI~`HNmS>kIyUfJa*3vsaNZeMp(Nh)zyfRo>%OL=4B1WL@+;#wq z6SKn+FOJKVQkWLtv2@5YPZEZWF|&43lRGsFEe*pSV`eFEV32o%m=*0!SUJCpX#D4wqxznZ;C6977oDrh@7XM z1-*zJU6sqGu(QBe2oDM6%uT`i)iQq^*dq52qHu-fGodlrHiQoknZzq`-G*It)liw; z6VH_CVe+CMi{0r#n6GKt5+!z#Wy3?>nU@)5S~TcwFY5dgHBid9nyCc5NTv^Zqvi0S zuY~z!40LJzhX$=_r<4Q5&^ojBuvP`pQG>(5%H1ktHEOS7v7w|ei@2e3=tsw|R@rMz zvGu}YU9nGjX#&h@MKC48J`NZ#n;m=)+r;J<|IdH#N2b7jL;SU#5-T7PEZBVU3pQjS zyD7_H4g2ku(Tw+h)e3!2H765vi*&KE?`%A^gbEKsIwWUdP&T%G2^L~@Q0C)!nvx5s zZcn3)*-HOsIFi_IP|8z?-G5v$hDr^xX~$F8rP5_R22=2mApL_DxK!NFo!B)xwhZiE z_t-a%X!Bd?@f%lhB4q{g03W0F7vhyOAzvEkjTl+k(cOrX7%uJkA? zsqZ_b29Fa>@Qlkn)6}x7!GyNAjW!d!@sB2r0&w;U?v%Z zA=RQ6HNr8BmnEjR*MD_Xi9k4b!y>(tg@k3d+9SwSiy>xd!qujqOebcM%F4+ zm%{x*I~Ms`aX5GiH$~CCDx35w#N05TX1?ce$Tlw1# zu=Qt|9gt=uz)j*`Q*{VYs6=^qo{A2T#3fD+;Af9enKRd0?i&4YW79kb0tmIo7VJbnzU?i&V@#H?rqPn3^%50BTvA>&pnhxEV3N&4CBJU zF4_F>A9A%HLce4jMe{`FYmRG~6$l{H)e4)}W}usyH?QTQc&5o#L#cP6pl2pr^sJ7Z zO1J?6jH;)M-;qZRK%~2=1`6b1TPxvID^;d$>&Z?jXDh>E9$8FiG%(AnhSrLiF>l70 zl!$b=UaPt(idlTQz>Q)O)*c3>XW{7Z*dhjFK4Jb!!O|g9;|*jKU5#W}%2C;rj}OkU zc<5md)RI&k5URc*bA~yWi%?6g;p~i$ zG^}QZndRnGlOG{E56<&27#){`!WgntxSgH-2vTK^Rys8A`83yk4F^J=t1aYmpls#5);$>JGx+ZzY zEvoNYZh)Vr+N(a?M88_+C=;}pJwNQs)R_G!v|wdqMu=_F48BwNR#YwUh}j|0cd&R@ zu6zVj-$9BPJuw6w*$|9WTux;lZuhLyEGAQ8_uPq2MIeBR$hZy_JaWI4*z)E10|*7i zs~Z3_*9QVr1htJE&F>B(5);YJR6?CzFGi@gJLWVSb^1&*rPJ9vSSdkdBL^V3vSO!j zafms1Jls;f0lb+AfFzHO((cjL*ONgJ+e|`$kI05^rV&0+tlD@g72i%5mhO#NCkg!) zcbT!)$4mM8q12#4mHDirj?Lm=HW=b^4k{6R_6dQJWp)u`54fDXVhHj)B;V`k3QLbb zf6;t0>^iHX)8yjR$B1bGWJ$N9o3#7NMl58GX4M}v^m4#mQ6Xc<=rCxc(;&{uQx?g} zj*9H@ZEb;p3F!^u{WpT|r^Xvrx6?R>#{|{O3@*V*WKrFRvR#$>Go-`GN}U<**{Q|7 zDzj&~m`n&`)u&xagG-Q#=9v$H#S0#x=Dc-%(9M6Gh)E!#<2~cMDMkj}W@*Wbq6q8% z)tRlJ?$VyM1;|NW8WeX7{11_~lNCx{qEqq?IGbN~EZV(zh5om3$&~AfX_WJPWy?zZ z9&+OKaluZy2orV{dFK4VEXjne<)ng53`VXVFmJDW&3EL6p~IdM!;BRT3beadb^zkF2|;X-WUFP_ryHvo z#&EqK=e0}S4rxteD&Ii^0;O&R@wY0=Ls|5Qc{6)WAQ5FTN&D0_zd%Refz})fl_}0s zngUkNy#k0nX4Q3*OZQu7M^VthVhkAT55t@3Sb0PEl?9qgR=%xnCf`iQ+TaTdu1xvc zP}aS)m;5r>9iy?CjZ;xU^Q(njg?;urPv5`TB}@=xenmxdFlaV^j)%3$jOCvy%Dn$t1EuhKHPTrG7vEUj3O zC%}XJzIFA36v~A<%}h?n^Rf?W;7HADCM_fUc9OwW+($DzAp**Dv&cFCeR)n(N09Og z9~_rggRr7<`FZM|Iixk?PEzm_2s6t}U3lTZ*6KkbFV0@m@yY;Kh{mSwlf-W0*7^70%`5ryiTwFRHeKsh&iqpTe1^5aNbgA0{~^0! zJpFIj$?q-Zc(wdlxRgI@sQ?1xL-%gXP5!paeGAvHNlnS9kDDvM`QW`kklP>R2wkY< zPo*B)s6fVFH}Z#DY*g^$@kag}sCz^{fqB>%Mbf_J1&hoLQS|8eKrT-TU zkLfERqT^kfWXo_0xAdJUX#7CYXv{yk%C~OaHb$6L6l05Qm{B9~tMkc^WC0se5~~Mm z@I(NZM)oZ92VGyu?~-Cj@UC|m+zQKDoSIUL$!iZlBtp#(x}Qge9#}~O>>juKjvfgd zt5z3J!^-jU5&a}wOP^;G@VVU|H@p6mJ^9Nd1D_Mv+P&_mk=OV)yLUj8g zGtghd1jdnYkuB=ma}!|zk8ZXM{=h1P{-<(~P>5h9Tx8x7Vh>`Ne)?Wvk`c@fGIE~W z#&kU~1N1y0Rw6*cb0SZEwD~l0B>DSm2B>MtZ$vIfC0nlil0;)MjL53YZ9sxdBk`-a z`G4c@EByWEWr6s0+aDJY)AS#koRAI^GQnn~jp0r)@%@nXCR1T=NTJx{EoXjxyQgq4 zFl13d;|ej{Hvw3zfXH?7EpKATGSHoY4#g)~@oKA=`$(rl}ic^J3mRd_6>X(qz#C(8-rumu~{+OB%CRe~baj2L`}HI9%pLND4P z#)GC|y&*1+*g#R>_&eBvO8e76*2jauSdqVhqj`@oU`f@4sM+sbaG@1aWdIwbr5mGWth^;TFo+lw-CC z!2H9at;m!|x+o%Oo)KH?J_9>SsUOfOC4BB)F&!T>6%C9D!HhgXSjiyxb+SWwaLWZV zJ0{7QW}+WLNWt`Id?ZN7V^S6@Ydc1dz}c>b=b-hX5DI%nJRuRW{BYJMy&c%CZ?*2d zjxtQd6(Jk{=4C|Ak!JuN36(0#6yPgX%hM}sb>*_KX8S)S@H<7FiV zyMoz1@^(K5uC*-9j>U8C;{bK_D~UJ;?MyPU!RocSpiR7iyh1RX)?3Rfjt4fcWOxiG z@Xv5D$4?{v8x9U>S*pUTm+7~{w(Cc<#lw`J_7Py3H|z*j;YdWvVO+crFMM%J^VdDkX@4lt4%FV`d&| z#SfX1+JG05Th2hh=_Wj7HdbU&+38hkOd=Wj5`CNyCfJYN;$-0&2VNh*C5nP|+YBO2 z4NevjcU#jmu-0o#B=wcD;}8UHP88g zG?Q9hkuU46u6qFfd0HGcuym|7f1Cv}dLTPoX<v8UK>)fcb> z5klNvcca$xR*~VF^;p4zdNh0`dvr*1J_AQJ zK^G%iLrgUD1YWk8xmx7Kex*=76+xYGH0iZT zMZV!TNUo)cl#j3c%&arpb1NtZ`^|;*?l38@E)CT|lcHKX3Hfy9)SS7DrrqxgU+Gh= z!lp&8vX$^1E?lD807bG8YZOfEI)t;I4vCs3L%$MWj)My%M2_HhST;{#!<$w1tbZ{a zJ8yS{u(mWX|8m2ycEkZLUR(Gr8>vn85cy-eHI8Oc7;UGO1j&>|K9qJGFC&MDdu35nBH<(F_9qltOB&JFFWZFAqs+M_R_3B0%l&3`0Vv8z_2~ zLu67R5z6d-%T8;9T%%rqk-#vE124he*sg`F%6d{0lgeD0BSsSvGvc?S=_{5eBMd|B zqQR>~!F_G0Cyph&{2jE{N**=HP|BR;Dd&0Un~n~$>7XXfHAF~-4Y+bFv)M|L!bVb7 z=4Z}>Cy`1~PORGFv1Dp5Z%mDyuwRMACkwxuj30VX=LMoh6zRghi*+(G%|(?9dx77R z9jXGQ$f4skGLE0-IlI_?Wg0pws{GdL-JiqqwDq(ZBP=n6Dk=oymHg$!(e9{lbqHc` zGoyl~DnwOVyMLDWJeC9=cLV0NHdaKbrHSSVq2^bf8Ww4rPUr3p@o$#r5$sk>h=+B0 z9!crJ-$BE|C=wjfA=DBcDmI%sRQ!CO&_4mN^BBt}q*rXyp_Uo8AZ9vv9Ir}VECC?y*ct13qcAL?^dw~_u6RQQy&={?oK1@3%5n(J3tf5T@IeImx zZ&H_wr_fOmn0kAxWjL!-5!z;(=sWQ}AeeTEkpdG5VSqS*M82IE?s+H+^J$oL`f(mn z9juhIYM`20wh4NIczKt0W^DqKiY8GkKR7ODssuQg<5t}oT>mWf zB|#8Z)vccLIIV^N+2JloayJY<9$_&b+Jkn#a4bD33n>%lASl3xheIkb-KS%PR?P>z zuyuS}=~bOt`q<)r+$t14Wfi>%gFvH~W_Me@RNSO~B%H$CZi0cGzX38`9LbW&r+W!o zkW(5}1n|X3m7Q6k=tq_1FwlXAXAAP*!T2b6$4ASUWjB?8o7g9eWLWx%)g0{zk|(Oq zz~mOi3xo!E65M6VX%XO?CmO7Y-AXaoxhH!{mVIu(mX`A7PBqeF=6|X62y~|NI(!yr+7qhVV2BR1nMYX zjHPA>kh%_c`hVYRZ&^Z-2HDu#zu&!qzw4_u<63$vjVt4RP@G*cOZLPW{(!StY{aSx-ynT?t*oT#5230(6?)Kn?`lMC_~PK zy~ETsDx?ze`4E^~<$fWaWcf`>E@nKR5MaS#!9hb&6b(ejYfuY>Bz7u!yj9F*Orkkp z*L^#27OA#B)q!IS26D9xp!7mh@~H&uU>-5)hp{;+X@ZefXchKngR12Ax070m+j3(Q zKAFW&?Nkz^CE5;T4+EtXaN(NuPUmSv=7Ns4U%WvL7-#?rzfQjlk(qB)KT;@hKH_=5 zffxaO+4%HSfj4cp4sx17Zk!@nsb!urqpFhBoTA=!|Bi17>C{ADtymkYQN~AVTfged z^*AvD>tKnJowam;D2XCin$Jo`p(jxRF?W~1h6%!C&b{U^(#D|Bb_jL^ezqTh^Jvl} zLF;!aQR~UhP8QnJ{NkwRjQoeI?4s711!V9~--Ea-&;rQm%mDqUr-MlKjYamxwVi~q zGmE@Oty$rb;Kbk@9i&iTdU2npm33&=BqNyLo*|Ah!Nim)Y^h zjVW?tIYJ!Wd(X6VF+vltn%q_at4)1^%9Sj+T08lwzoj#xD=BExp#2pbfc1LRiDHq0 zuH4#hk5CyxMi>OExpKS=JTs}umi{G5dm0Ds4qaBnf+4Ex*LO_nB9<2XT3UbfnSB#m z@1*4bbc2e_Tuok7KA@K-MPt^YGn$3<1U{%zC$>ij+%}3TR`w+;4TXrfD|+ezVzb}` zFVz`dW=W$8?po?F5ja-$s*(bCwO?CCpWU@u+*R1n64}#SLOvqvbQX4NLnNPu)WmAp z3DEl=?-e8T%-0 z3g)HGcZ4GUvC7J#iFd@Q1DKqj{`!+oyzA}=h=+uYQaA|sK2p9K%KX*B+^AYHWPWQP zQChsNW>oHIK7j$C8t68|6fY9G7J+hRmWx}wu4M8@%ew8p&~!FCnke9+EiG+OO9L8< zxPj-H1umG|vowh4`^nw+h>k(9W*Q4vcfa&PYiDYjiF%(O0{v7fjI_Yp#3p>JGMTKc z*)2;NY1qt`X;5k6fhhPBJrj@V0;a`>1(B&JD_BlRn495l_7jD-g~Lu5XYo`ZeF1kv zF#^0o;Ya%FEk4Fw38J<&5+-f}T0Bv2u)dW-QJe4vFBhCx<7S z1C;L4Te0$^VmWSQ);#*sYw_29^dSF>VuP9mBV}l;B&$wqjkv;Zp_>-Ne^4~GtIp<< zZ&r7YWC`X|rx6Q~Q+Kt-lw-BsvwnvHZ`8el27>PJ0cP7IWH}#7HB?nrXII~z;f+F~ zfHy=vZ7Hiz!TJOS8=h5qi+L;dg-ChA~ z?wHMpGT(kD%(HP7D_06F<$WWHzRr*2pP^bU25amQ<8$=Avg z7#K~FNbk{kX8>0+%$gsgmvUvmi-q{xQ@k7Knni1{XKg5Cr8r>`IrN)|B8;jL!x1kR zp@j`*auiL; zjhLZ*I3S#?I=g2@=I*`&dT5t-2iq~c@Tml638wSFG?2+)m0)h4ZagUT%tcQ;LKi%e zK5^C?VKzfN7sA z8Oz^VPQVLO<-fo-YAu3zp;a!1hS1VGg6XcjOgJ2xPl%xizg-55dntQ=f^NOm_VO0w*U@7(#~L`X5!Ws zwq$uc(l|_6_oxP{x*8~;20&G?efvF&UO^8q{bC2X*2>%)MV`JX^v2)D{>hy?bLGlH zVF%W0N)wv`25i4-6Z(DUc1oU^xPOtYRzt9oG`D3sgug*+)1E42Qe1KdVb6Lq|G1@E z+JDjcnHWVw(;($DXwut1c;m683*<;RCYfc~yT)LnAMO&Y<>vrLusLkLGByeABu#SCC7 zT5_8T^rDl5!8EsN;yujyThvOw))vS-=s7)k)lgG{}YtW{lE%5l8SaCaOuY-Ar=|9D!ER-hZL-G}^RYq?n;Hm5VL7 zkY)vUgkVUnt0Ot0^+C0X-sFy%do7-W&#Ab+uGG!_BCde^$)n?S1wLezrOT#!2hZoC z$rNyQ9z@OQ{1~m_AxKE;`UD+i}a0Jf=^QreV&?GjU~?MZUgJLjcn!B;i=lA`1rZ{XQth zr^?^uNv`5)Lvle{04H&ZHio9I`YVBxBug=kc!-z)BvH`0C9efuk?AH(>d`vLOJ7oU zlB{fkvW=aC5sM6T z4{SE|;J^ZB5R8X*U>^G@U9ytbWD9jx&-9%9c;TZY{JxJ|yRf(2%j+o8*YikEl<`8F zDZklzlMK4;XQ8!?&6|0I$*0&CYMMd6#9I3LhGDRp@v4!@RR@C~U^@b?A?XmWq5OaF zrkK@s$$7_`A$}K1!q^69N9`#qyM3%=^sr4zYQ=z`j?e=Pd25=Qj0tQG=7Y;!16c06 zhHAv)+y~%~J<^<`0J6VZ4nPnz+djunjq}GY<9#?$W~rgxn`Ngvvgsy8p6nLBfX7~! zA=2@_9VH5iZ981m_J`55#-(pCDKMOtpjZH|VMEd(s4AD-z0vPT^mOEGZ z(nH-&m5|{9amH0DKD`6EqaB-uxyGPE?GU(BwG2Ldj6r&@Xr%MRd@Pg)xfjQiHvk9t znw=}Qy;QRUv2z&Ya2S~qJf0@V96(Wvm@W`Mfw3d3aPI=&aC93=Hiw9ch@3BDe&6zq zjt&^C6MbT$3Iz*ES7R9pgX?23$K5N!?cEs-Bw(VzVbyWe#?hWbKc}j~`p3A|sXdt3 z5P(9XK2gj%aVt%vR|iG+x$wkB1M=QU8rD9qBlDeXVzL;X045pHUq5o7NgN%SjfA$q zqxZ7yHpdtj9M~ejf9yP<*wcV-dSf_d*yRgP{y-QuW!7~n0g!js>8B-0fK#7pk{}@| zE59MlhUcCq6oCTv0%_=^QfL2=AlvhB!XBNV_-8>KfEpQ+rzNv<5?%=1?u+%dHWkvE zJEd+IZn(wxE8RgJC~DxcX?SF1%ULa2`iS-pb&*sWm~eB~)jgR?Xe9-7r(*%nUUW6+^PMazly-c;18Jk*%xGCJF%yDC|(A}kZcLN zgyqR~MZ>M*tF9!lsLfe+AmubCAdP?R(G(Ex=sLr>|N zp6L>5+K?KtS%snJTnXi5zN1{RVYvl6Y>^Swvba_91nHM3!y_XA_Az0&O}JLbc6!C! zFgacBzn=i7LXLj;7T?7(dr2C@WZP)9nR%Q$YbaNkFU8(RyK+-^Hy!QmL3)pw6VaZ; znwgoAz58_`!uxnM3D(PE$xT*H{YB+%cgrQlpn|Lb?%l)@HYHPM8OIwO%L%7!XOOw) zrt`(nT>DBxmiUU;owR)zYQwDpBQfp|Tu@VoQP5Hc?sl<$#3h6*($amKx~Zg$#l6b*lB_pk#| zj-p*wG`e0w%<8VtHNzVLU+U!3pt5NuvNb&^U4~-JtRK!iVuE@f`X~bHIb;`aZ-*j+ zwdoGNpRu>?DUYX}X~CqtLL2Pb@YK<1XW3=$l}FhXn$8$v&K${s4=&!4*%o$*Y2siD z-fHJ{k!6!MWhMdfL{ataJuwd_t4Z&X^|n$BqDP1RiSg#-Cgeal*W^Bm;Y+&Mxfq$c zz7^~Ly{EN-uLd(k4#ENkkv?N~dBD+3tKB4o6`0_o{)th1`D&U=*G0I}F(9)uq~KuE z6^2y@0S8?S&7DO8*s!Vw{v^5NqnCkS&WgsdST!a}^H>8w;-8R_&4G7MW*c7xP19Qk zSB?}vvHQI%t7Ua^hSC9)fHV^HzAGnJokt9l#n8c@ z$OWX*CFe7P%_0`~F#t8R`q9# zj8*~>Ut9WC8cumKXKxt>!K@FkoAla)O%HgL?V0kYXI#qD5N+~PSER4^KuM_eU0*6M z1(S+3_9YIglxPEle6|ygOedC%vt$UU)4vdeX(J@8l>@Dk9$E({mbm3i33|XBzf6(X zGrSXqfd~5+&%eA{>-ReUGI({Zbg$n!+{>b}d_a0BY$Zj1#4B(4`^6{v{Zzl7q4qG* znGY!2ji@GHeC}7keLn0NVkC|pZM$uk8uh%{GHa*EfPQ24^|!2k-V~`(&^HoGXCBd$ z-cd#FNF;hn@6N6!sh3u40DgZN*c`sMP}#t*d-(~1;u`}}U1Zi&t|%)v&-av2(j??b zc$~=MdjJM)4Ov0A4j1ov(#~B410$ zmpyo3G9nXsPaUJO+tKtyLSbxK&XiNVCE1JP5r!hQ=_rs)7U?FY9OeFmDa>Fk`IH~B znU}h_E~5TpegV36E7(}Pxsc+1YE$OC<)kHX_M@};K|06OAvo)W@ zrHpRm^5Ak>Q8hS4yv2pnrilmBGazC#f=k;Fj!u^C7ndFot4Q0kAn~*PSe{E|%l`j- z>DAwK{5mS@n~qCBWXdIl7fHu>>+{bg`XOB+EdtLi=!F7s&qkh#e+RnKY4ccO1_ul6 zO+|P5&RLr8;6h2yWt^5nf654O}*l6j9dOZ5{Uu1l-oEfe)aDc%t4$%{=6#KV8gFFZKG0d6I&4D5K4Bq z{L*m1Vzk+r+V!fKQayPPKbIG z!kSAE7(agbd$K9I69Q$=OfsZ&i4NVnYPo3zqFS~!X2p3#P~Di9LZR5*F@ehd9Vt`mm|JLGLh zg-a=imox|-MlC{@H-4cPV7u6)zP#MhH7Go46gT><{dy^ymplIc?sxjXBVPWVh&!7u zGi;%*l6rdSkM}+t^(!%Uc}zY{w>{v*M>qPFbp2?JAqe-?M&I_4Ut8*z3(hC%tB=&2 zA6@HLajJxsU=twDZJdQikZf9KgR2@kr`mp`+J7WwmW;!j<@)N6I{m8cK2qa;RO?r& z_($LB_dET5t=~V}==rAID5bM4b~^J4!_MIZR`Sm%(b)Lt2mSs{zyC>){^&7=lS;?E zEXtOgZ49{`?&!J`IRkx$w7H1r0l7}WPUl^coG zTs)(_dU|#&m&dIc6|jgBTD9q<#eM$FSKC#gA#Jw;p4p%B)- ziC2|LaW5B{$C@}k4Z-L7ec_@Q*r=$wf`U^ZQdD<5X2&$Z7|(C-1G)0;98t5O5TxTi zxC(#>PEWgqzak-Def`>$*+pA*=%_sG93UUQ#dn)skaf_I)qgldvoc@kj8NAH6J6qn zL$>i|`P1V|=KsU3k_c5u<@arjh4kJ`@NxTFIfJS$zzV5I6(YFcNJ|&Msn&3JW-fy4 zRO3t@B)Cf_>R{G_Iq9+Gi;FbkPn;=*>9;Qe979N9)iNe50zj>CZ|zys+sbfV&|HBd zcoL-s$=XQ^F=%U2&3fij`c}c8CZ;N~!GDTBUlIoOEM^>qw2Vl~0a8wK&s7WMN z8nL3$00VRWP7trwF;%}I_3#^M*>oG#;&Y-E+lHyT1FgBC_%=<%&xRxuMf@8v0Koq8XfYfP3oR28}Q2=F@dJ$5#D z>xNxQzJd?m#Obyhr*$)oo*3{v7?6)^2$yg^RbFGxT?X0zvfn#L)m1Buo^6i*)y3&d z4mN0Is}MhC?MU@?*Z7T@ly4BI0JcOfN_N5B-amTbB@|kM07^i$zyGOBe&cd`^vk_! zccUL5{T9-NdagNPd2se7KN~m0H2D+U5-dQDE-n_NTaD`P;!*x_cO6${B#=Xax1Ds` z=`+^jGFrbagn$c-)`J0x6u9YTD#AJV%L(Vu2WPTz*CB{4Pl_E@U^@KF&Ph_KxT*F1 zt2Sre&4a?r6vBl7MedW$Rn>2bY&>LjXb*PfD<)10EV{6Lq~VGHB7%W#lKe98f`3@g z6wb;S|b7cgPkWw9ATvH%}jxIUp_x1c;YjJ{`^jI{Tn5I_FlJGY>Uf!^%r1~2sp z+B6*Qyfg2>>coEGUKr1cZ~f z^r6xdzU3uLU(JaBGnR0_99oz0_feVP?vRKInPBjuLfM);OToX-(A^oZ>mpQCVnW97 zoW?P%P`z=-{!I^zf203(R8Yr9Q$9dr+;G02v%@}S8=%i3X73?9oC zT#gt_jjvq95n4=+{8?{ktD}Hh$0Pymo^-*y3b(1ZI+MEOHp;k1VtUo!P}tBONb>IV zoGM$2Ts`l0p^^8v^smlfaFMM~*?sHzbk3T(%8ysx4EaZTwA0n5&A+e8bo#2vj%&8~ zBu%bs@rXxIn&#kbrYu#kpy2exA}8*?2q305hq29sA}IFy;&fI}2$P3^<`cv;P<)?ol6CgI9_`udq_Q z@l1vdn0bT0SYSJ>B~^bjQ?N!r_ujzOw242~)OLJGFIBbz8P{eM85crdS+djxsiK93 zzWC++fhNF`K^Q?28C@b`Bm|mmx}JaoOBRk=6eKzqPDJoL z&E@YeBt>$`_pYBm0;x3CCA#=WN}CamBp>7z2Yt|@JZe3|CqFgo-Z7kE?MdQ;uT zg4V`@)-+KTeS`#IiS@<1B@X;sYbRxH0k&e9w@7`YWqLoYPy;nSFqHf0J3qM9!Oq*F z_SUIgJFBstts44GRYuNvnY>(oG}5!Z2Y9BNTqUl3z}722hzfZt)tmGPZ|vF|`zFR# zz4eCM#Z@tai_;!l$Hm?~jW<+tbncj(Jc*~gRhzi$j+tLP`ZgXLSNa5Ne6Tg%oUJ#o zEl_E75C&GKypx;G7F(0#+=|GZpZ8Z*-lUZ`(u!tee5LniB9rE=jClC(N5wpzjOUKq&@bg$U+RM$3=GUk|lh@Um^v?C0k zD)Ba4d)u$qevManU3nAj{q5^`qQ7vZao3p-3xNT= zAUZ2r=?;JfA~mxiO2e45@SX)*h(D;vu8qP~YAqK=OfdjJig*bg{S4jeQljsBuTM&0Sih$22se4UbV2DnhRYk4lkqHt3*m(wp`s zrUp~ZZRC{DP^!33XY!xeS~4aHY3Kk{&w%A|iN77W;IUB9W@7mqEVkNw5v@2!x}0#8 zPhR>3p;HXpDUiWgp6B5Ia^femPt=`lRdH>OS=>+~4H@@8N4CT=Y1xRf!bLH66>3rn z2NSRMr}W;Ny-$W78Tsa+PJYUJyho7T90BjAw(ucjV$p{z?54UXUhh+N^k(j2(d;1y zi78hJgABTMIP6B32ffe#-q)7i*R>eeed+Bn#xNt<9$6HzNyJw)p)`frzIUapKg7Lp z?if445ka#a8wE0D;d^wcPl*`(y`LJO$+@veW!^+XSRgN1;jH%tZd?eu=l%Zh!Pn5i zJ8kf;4!JaVrw!Ncg=jZ@I*eznVqQdt4c`8PxBuYi&V6*@;G#AfY+PwuoF-Vbf(yqX z+JHOJSSyFS*;p4(_B{iuHS%4$4ZcK;-dIOpAI2DT@D8kluiP7G^tmv4BaX~UMe&EI zjXsU$(d#>UdydzB5DnmMK6-PFAuNxu5IE~%JoPqS=`A#F9TX?Jp+_FMV`}Lsz~v0U zgFfv-4*uxFD)n*L->(tH2B?zWHn<)V?J~~WE4~TLCzd_Y3qq`ib$;|&H2%xbFfX1x z)Bn2_0L7wP*bT8Q*-_M!!Xctt7yzRyTzBd&(**G9@4T0H-r_qSvOCu_ZeyhGeA@3y ztKMB~X2Y)+n~5E~jn$@$ZT-%N$g0kQKDwQDkwUc=?N8%T!5JK~vl{GW;a zGGcjX$*5_wincAG^rH%pCdWY4~VMhNo%f{Sz+qd4|tFZ<>XxbIPjeS>G)6VO2U|eRj{=2Uq?|H7Vp`eZ$y}D_jjYpdvGwWDdZ)Dq zV7dZP>_ewR-11!r-Y!m1AwV!Y{`Y0C^FHjnCAzKJuZgdpHs2Q+Z=#oK6e11}(RqFX zrkLv3ogc3mY00~%m_U*lMP&+dXq!uq;suO;sg6+)e zp8PqN2M{supUkL9LHfYy0W;TH_->Gh1z0Xc&?~lnOZ?FjL@vgxe}rt#RlP{p#22JE zCTBC+WK>sV@gg&|lh1}){F9H~rQeeP1iky8a0eEx==OpN1Lv^Uh@_Do2wTU1`sbe1AD&*>X4o0h*#P*( zPx(gxIsssb^UGQLFJ}KijZ48Gi^zS0-?X%*iB8o7Ku`Z9XH2-sS%zYB& zKVAA7+~nEPa8i&Nc}NK&NsrKajH;>f9sET4r>*|c(Uj-3-FB3;t>M=Y`^oHw~1VO zTcN1H)r=tIS?u3?GVUZ!u&h@{0Ce+AFhs@-k^}#nr9HPDbi@h`OA;F8l%(0&#q4aB zX^bg_JV{=p$pjt|HpzCG|AQ!w&28t@qD`rki{iF9Do=C9H=R!IoVE)5b)Fus@(%>i zf(ccIt@C31+)w|Lj-=20QkKX?z$qlz9LZTSurEm6kTB0rW{k8x!AElB(Zy&()?I4% zp2E{YwV*z7{1YJ+)Nzw~@y}_&LGUZW8GijX(Fu=qt^E@atnCscVxLLMRIg8R&0~t0WurFTL6loODj$%vLf1>Vo5yO#|On{s~ zUCuE=1z1jYqXj(&)~oW!SyK8`pi|gK3S#l#-;K(I+{Xiy|I2fXp9LHuRd|_w)-n|@ zk^nbbS`a_755v>#>3!7fr7J3>D=H;in>8(hE`~~1(aI=s?Wc$ZqI8X<>>}5Zzb;wH zU9~IW(p^{Ky(wL>DjQqA-y&0DH7Q-?DX~(gNJB`6oGtf}pxtOFB2$aGyHcnJf1sV^lkn1% zOj?4{r7sxXk}KDQSKfjvXCRqI_~5Z)X59$K*eaGCZ_4W;4WaCIt`mW0lDFN%(ZNBWmSo4r;F|8%G+&q3kbA49llVO-aISc;H^3v#IxiIAv^Y|ef_(l zp_P)0xeZn1%~5#E6~2!u{A-HW@wQubVAR&fdEsNI z*q(^4-ltp9^4@rstKx+>3sW*7KZ?^ip&_7`+-+K9oyrK#w;_6IP?z2JgW&`T(=#fg z+7Vdloa%M#yuU@L76R#Q4frWcpWk4gWzcqA=ym=!s8gULK2*XbC^)aP{r|WAZGS;7 zhFAChmr0HO?V(7DBD~QUG#?_BwYUjx4>kF1{R!UFf9X9fXD1BQstZj|^>bPfwD+I02tWn-haK#49?g zX=EPx!7}8yP&|T>ew)Rn*&@@=91^NUXnpbJ-+TjeNihrXy~ren-|F+}&sDBF@rf;6 ze&_IOFv0qH!uHZ2ktiJXoP+33jJ_+kaF|oJ4!is}{@!nYaK1XQr96RuB=Y33gT7{@ zTAx$)zUn1wxB=~`S<3{2K@I$KvvqtZkg z!`=(jHX=g8Rr7>#*}wE6LgogpHQgsI<82+j^}k_fO(H0p2T)K59ku%}6d8==g5U5R zL(Ev0X-BB4*ZRHDuebBx_9|%f>q75uoeFyWBC;v5;rnHN`l#PA*xnOu2P~H1Hc!K? z9rJ$%*$EKy1;wlBJe^+QyBv#DLt0JGo4{&(SEYzNsSjCS!Bmb+hxcOcB)=NH3P;i# z*#QIZR{f#Q_Qgou?C&{%)+mKgsu-4j;vC`#FS4|wJrh0>_3z&?_@eUNoqFzDnlhyJ z+x)*hFoqJGc)I+rBH^J{^yOEkW{aIWW#dzbyBXU*IMo0dxqlR8`9lj}>B2bv*CDF9 zB&9pubpr1T!f9ELy4L0U4|iAyLP7bV(}s$h+8NPxlY1t9sJQi|pm2hYzs0BiKrxLO zVz2o9v&$#|CN7I(*BwsF)|b^XfA}h#roMZa|35gLt1)Ji315iva#l$Yq0i98=bQITIFl0&R9fZk{J!T*Nyg=BR4{FgA$ zd%h>jcsJ({Yb%bEA`6RBV&ISVZPOv9vFw94RVwe5 zCWCwc$jDs`CoS(^{ypGkoG!(DK!^VSJ?gq{kPtu`KxlX0d;9;V(f|NN{hl&xIS;T8 z^zVx|PN9i^^o#@!tn%gACc;sb$G35&Jip`c$(|Or4N+Aj&7&%f5D&6@49t+1h1#ZO`gvNB`&7jQ;kYeT?{3$@azUU^AZycua%C^W>~sB|d$2 zdd8RMPyb+EnECz_2GEo1*Ti~%?Bnax&(rV^6zZp+L7lVR)|JA~!@K(Fr+)AWAS_MV z4Sf32tZOIP%HViNuZU~Ezz)ku3VnxPit=sd~NCMGLW_=2%2M z9F!ww_x1kzH5%pGzIPAPlNrM`nRm8r43W=X_`xTU??{+! zJe3J!Ts4WU&S#FA`0TlTm~#_9Jz=*>=fZAvTPo(w#^P-0BWA3kf_Xg6ns*i}4wQAl zIsG$UVt|ubnb;%YkNnktCfbl6;SQ zat+l{R9#sZoEu)CW)+A#{-5DB; zoya|2bM&eWoKHDMKTa_WsA-h|8R2Q#aWarS8}6u7K~mFgZD~`j*&=WNgJX;2?XL43 zPH?2tianws;v!!VuKou?k2E^WM~`p%*w63X2hbj20`M^0q2`6Ynq9$7kFbkmf6Lyh z=W1Mp06-qXtG8se(D{Mahfu+`Knz4>RBgEP7--YT&XkN~z0NK!H2e&h$00goXM>LY zHhe)}=dTjCuZ#mH(?QpFHtaIc(Id@s--3UQT}5JMeG){cmI;O5`CGqs>o4>#|N7QC z%2_M7;sM5b(q#HlsE}W~No5)VQ3hqv$vm5g|Bb!iJ^eXspx7gSrGd4~E^$5OD~7F_ zOPT(fHZwMT)n|8TW`7JU7*t62I@`)H&|eQ*`x6MS;DbB!+zL)=b9D2H*?+ffx&U4+ z&xM4^x%n6dnwtZaZ}ZU)pn8?YWvgzyIcoy4HC)Xjs}MmPapHqAUFExSu;oVPS8PeR z%_kS_W>6Wllr6kR#<(+6gRPZF17*u>{zQjq$zSu7Jle`tPx>r#_Mmlwl_}Tc%<1s$ zX(_V_2+S6VbW>iPs-&1Jm!7abWy;1XfsDI86S@7LF$7?R;k<7k$mG_b1;HSw()@~* z^i1X+DWL@!>=mC_HigNGx8d|28wp>-&3u)fdrXIeA{NPU{`3UG4)R2s+fRot4>Jx! zt8@+Io+IHK?e1!?BV|L$GzA#;mSg-BUxH>yY*!mtsVDUwXaXCR(-sAG*qp)t2_g7S z9`VGWVwR4b$|)A+Ophyir!?+v04Wr*#A%^n!{dPc`PS7B8Bn&N@Ulw z?8lh$q)^5)SDq(~IxG#ik>-Qx!Y(pWP~T}K1M5kFSOmirT7l5Xh0fv=1R#0>1{Jy~ zWbuU`d?tahgYD&UmR4tz>55g$?_qWw=dQy8V5o?=4>DHE3KgYHy>kz&R8HR>d#=k* zA)!_j3M~7NQ-cJEvJIGeB~>>})XU%d!81?wKGGo)&PoH#dc20x`GX++4m#!+ye`z=%0^{gA1+OptnxCBDu!!k4awU%CqayWe?Q;&x{>VcQk!D5C9 zZid=8EDQ^(J+$8$t>c#YOScAGI_Bdt=;D95j-xo~3eu(HJ}$R$(JGGo>Cc`yr8g9|wQ`io-PMJ+xnlA}!jBMK?h6>*lnkF2pc!(H;-`Ripsyn7fEaDt08ah*!YEoJ+ z7FLn{+kW_=IOjNc=%;@rj@Jzn0T>fmVoWs6EE_MN(0zZHulD3zOkQ`>(#j5r>nwZ# zKiI*UKIeq}Y1ey9!%TRSbhzT(TmIap6EI3?kuRb-N?v#1Pfq{`HPzS;?MN)lbnmc#d42TCCi{N!I)@Vlo(Y5b8CZ0Vg-3%LN$Df|yCbzit zo%7N>&M?eu$fG~5oSj_c_bqs3p)1jNKmK&-gRqUa{p>A9lt(p=kW$RV#bg%bQm|Uy zR$*$*jO%eD#5>4b?9+NQ&f_GhV8QEbW}nGyc&|FKOOJW-j+-mo1sM}}c1uuSQ2ZFV=0D)lc) zr#I>FCLPvVVEZboW>z)Re_v47vDUBvQX5)#yEi9)Rx3c@%!dQGVir~Q*s~(rn(mLj z^@Ft^RDQ7XgKPV^Q*5DIzuK|Te#N-lE7p+V*HzC9Dt3Ua-3IyoxENsZ&-12;Ljus5H?rUeJb`T+1C}JA%oP#}O`u-If>BOu4K}KgX zJ*VrpXx*s*7B9P(vQDPoKC8{h+HB20^F{_voZqqoVA8lDoZdqj6Xpi%s7Qe(;Z z0aHxiDD(}_TP`JINpAf@y8s=QP!uTR+R@Meny9U9$n~^A~ zf1x-H-+&M9{RB|-C5P^-vEzT2ya?#*9>JwJ+x!1e-Fsz>m_GGe1=*YTRcxsbnCt{( zF>t8h5yuQ5n!@xH)7bOqhW@_y-^c}lVE1n1EZge~(IuZ6Qj<&U>oH|(nBIU%J#xF2 zWQ)~`355Vy^t4of72Yd-g2|bPnjZ%c0!%9(8wh!olJ5A9g%-dpp9xPuHJbv+GHg7c z;Jue6zlBGOHBb^w`xr|iFnx@cBNQ}LF^Zc1vL8U;ld_apgofD#`we8Oa~;_9N#T&V z{WaEqT2$Vfk^Wi_m~KX115CpKT(oZArqwE-u!VB4hLy|pOKcWi$e!tMgQ;#N`qK3< zK}Ys*`WAwab8Wl4M^vG&U^-UbK|fl?8pY%>7Ii|AU73=-Mp$5qYDie?mBkl2LNr(y z<6%g^6K3yb_FoM^r-WIM=uF~*nRmXdCrvnMPRuGkm5urxvWA9HbIy$mx{724w+tEX zlPQL1iC3+ydG~-lrYD|i{e?v~YL#!$rn*kH*|!62kfknUD)6l)db~Ks!I;HA3<^Ghdp@K` z;hW~JkEu}gZd$Z(`ns~zFEdWCKrke}Y~z3k&ginrwo+Wp&mu+!`_}Dhb-?_b`;E?l z2qhO^I_641IcS+!EGeIF*XUybzsr&0eB1K!C5J9ELShGq4 zmUITTuP;7(zKoIuAN>;A2a+GZg9XbI1hw*`B91k=&fkB7Vutq({v1s+p~d8~US!9N z|3X{3KNBg*G)O~jb$CddkRs%7$Q*UJ zXXH;G)1_5ax*!r@5+ZC-gQxU7F|V=ht7ArWVOMcE0&^$W9$x097%?hrF$Ts=mnuF;bmL9Xdz*Lk9mtKhN z&Mb3odJI{OAy8zDDwo7C2Il+>)F@ci?CXz(NA3B#jnaV=@nvdo^#$KRd$L4w-KPbT zq~IG^2JHa^HB`+sar8)zs@FVUB2;KjZ-sK;KQdD<@4d3RCIzw}3|lUG>4+ec-tS@k z1y<#XlnG%A$j%dGat}~ivt{l?L=T)rc2P#+GY-_ELmuZ?7#m2IrCsmArU}IQ0++gd z3FQsa7$}7m-70XC22KInN=gaJ4#`{Fq1XS@~zYU48RRRroosB!7==&{~4$PPT69gEiU~~o|PS?jvnSTwG`6^jt z-&|$Kg_Pu^>ppU!61kPU5ung>EE>UxebBVM5`atG6BJe^Tc4OsAoNB=|8~QmRrx%) zOsOc=l|(q{3a&Pz-ZDBGLCA%RY>=4zlgkFcAS5!&c^`Iflz0oZu<-}skX#u&wP4gw z!vvvUesmbIHK{f}besnyt`nx&)8LWBLmS^SIIkLC-P)Rs#R1pyYuk~9=&b<;r+0^- zEve*AaEPb2MOorGOFl-b<;W_F$AU#C1J$!MLIp6=u-|oM=_33xSJ$Dlk|ZbMQ_P?z z8n0HG&TdpKz(VRTeS8tSF0LCZts7)T9PijzO~cPv zhB*{b4!q>ZbDGY%z&`9)#w);=&e#ICPiw_Gr&>Ci%))NHHQ}6|LovS3Ts!Bf(QwSo zoFmgfxRf{SDB8YhLAOoV^mbo^$3Zjz5-`&_*G5#5)Me zi~@Xz89kM&VP?A+3HE%oSD}ZqLxBPh=hdPEV3)OSdzNgNVm?zdc@@A1R0AeE1{v$@ckX& z0Jc$occ%BFtA9K)ZKR0NB;-AK5t$=xxWKCow4$${kMl9b@kjcC{WKYDF%eac$2}Nt zRX@xo37I`vc-WmtLs(BSAn;nmd#3lEtM3)|LNav$fgDz*KRT~RoSpc!aL z?19oP-JNBRE``uKNUtop{~^^?h6BbWpgwTbiXbq0_Wfunrw?&`UwrxrRvce;LE~p! z;EZQ=MPzU*GDwxOG(%_y`O?3@P*q$4WhX-xMihv_pk!Z}dgS$?Xp}C@*a#(Qv?+Z^ zyZ$)4kh!7m9S{Sz83^~c;8{1vU{K5bV%y)RdpD_p$qP>lI#i6Ns^=>B;qc`fp~Z#I zV)7t(T-J@Rx`E7%xXQ=5)Weo71)>B$?XRkiyhVEvO z8TZGEbe2g7!R<(Xn@kM=&?!q+s~shG3w{%XC_OkdrB7ub^;X8~Snt-j`M&|9k=U!T zY}N-EfMYdiK$;mFe&8sdDwW-l+nd2TZ5c_G{R8tH6J1BupmliIEW?7Q?!Fgl${Uyf zLM;05)sTrb+HDj7#(1BlufAvEHRD2s0JQ32xX54FPAZ6j`RA$5q4t7sZE%8Mn+Z>5 zn_T30degc9tLi@>)INCg0;E=SWyY`kITsW;Te81k95LW0_kBw@Rz8 zw;;uuBlHF;L>HNFI8%*{KLuzZ_`lG_P-gE1zA%Mkx03(tc4|w6k902H80ljH1aFHg zgK29c&H+ZsnApVJSG0v=l_)Ou&cb%=dH4A^bs+KY&iZs|8sOSA0jun;5Aeft|lyfWM13np#mb!&B96D5o# z1gXGMMaXi%j42w?AG{H(RHdGemK*wL@l{2hk8BL=_1o#!$zz!&^aXOa@cn9)L9H=t z^^5O1OcHsNn{n@7VdNudFi7An=e&wm`s5vVsUFUc#ctYDwoWDc%UWRbYIPSU7kd63 zGqp)dd-vVwR~0=1>N9Qq^+8AIzthfj9GJ8E0pXWAyVGw=I=iy+$9Am7jaus1&tC2i zKQM%sHd0RTRz1i(?ec^@&OE)_bkEZ7f%?i=emG_lP%rOfPD+0FE9VqXsj%9i1l4=B~J55YQ95(motitc@lKr@C8p0Mqy30C^u z1Z{&&4^4<6I$;eJ&E~lZO79KmtgFtoLY{sdowe`-wfSoRfot&|s|-7HXbIfcOfP~( z)HCZs1wV~r6xhU@3J^Cv?~t)N7+F0)cG`C)u|tJp+(O|9RsWG^^i7>JWEW_p-`&vS zu;JDbMUvBZa@QZ}1M?Qewsl75Wc~S2d6oeYFth41jzII9`6{p*k7zkEo@_;_;m;XK zV;wB|Y?qDmM{fudQn*23>*ZTHlu1Rd#*|ojO>;&AvlfQas+%qkYO!<@*eA2b`FIVpf5B$C6%{r< z8`h^v#n@0X@KiGAUnE@p8QQ~d-j-uyB;a$zUe9-xngn-RdhZ5av*r&eM>1+v7H{*l ze^muJcwcJqy3@*bx1*(0dJ=A4c|hMJ^{sxUPvL}+8vUx-W+dOnCILn7aoX`RByu`f zYF=o^&7deqRN4}lehCFV3-P@Q=j&o{|)>CRd5i z%(6FR|16_*u+#6mP1_Aij<^UecOZZoq9BS$3D5g^@x0!6q8k%KocMsq-xKejbW z#*)G(`v#i`G7~Q8wv>^B7{4)Kf)|giqw-v`rUm#$_-vX-D_SiCS#DB?EbSGZm*j0rq*H4X&FTd0^|+ zl5$MDT6O_kpc!KXrxBKCVgd5*!6<nJwtc02F0c#fPa3m7VE;agD=0D zHa^tJM${)?hM_>o%!WH)VJN_ZW+K5?kwU9TaqAM*b;jZ*U#ycNT!idpc7<8KTV8RE zWrNHJ_SPem4Q?p#Klzx)VT?z;Ax!p?bV>)Cc~bs?l6>4t`Aw=s%cai zaM?q$NXuUVkTFx~P^obYE}f}|Su~I>DKcwbnXw%atjUM~Q}`8g%HRdp*KslNL}2~k zyAS%k6VIIWwd?O2Mubj^B4A_leSRQ&8*C@Oyx#gVD{$(<+*_&wa0odL3HIo^&^g-w znO_heOaLH>xG$4@hDFg0JdAI;g^%zeF+w%+8POwZH5YJgR742hJjUIluF@zj89-#8 zncb$bag=gU_R1t;AWnU9Ir=AG*VO3>AJ3(BQsrju(six9)Sjo*o~QKo(e|#?uq&|u z9=EZ`GQezF80j((&TDLu4S=ruj_MV~0ilIR8kr~qRHz%`mH*tCaU)QS85HiYMY8cQT+UiPX$~{ ztQufMsX2oQrZjg1tW|jcBGKP;{TjySWXRDPo%HrKeDq@;j`#wZv-x`<#i|o>TyXOO zLK^zaNI2!)r!lwIB;`IPPYlCiBnh8s-6-E^JGRmA*=R3Oa_E*#wF8lD;%KPThio*a zwwGpzc$2^b*Qm8Ox7qV_Zu=jw&jG8yB8ghNPoro9bZaCh;2iVJK4U@aYoc=HJsPGP z2_z5nr-FyK3T!IJQ)V{d ziGbf6nOXvxOyVEUlHQS3@comB?G{k&BIo(#J8w0ZZV^BB3L7+)XoTF>;%TS1PDmEI z@p!?S8pdv?o)E^lng$F!kY&LDZHYZKj(WL1a*j6Qp1R%^4Iv&1;by_ckTuGv_7l zsClJ2b%)^Uf4Dg8WV75!4DA};BqeI+pKzn~Wb9{^@`v7fXt7*^EN;mjKqKDU=su0_ z1NtrZi^Yb%D{cq%41HPc&DBaGx7TU=>>gU3fy%pbY14XbWJ73gqKSI!>IX~7HmM-l ztoPPAWM$YGPJ6qx>sGf1bw*b(VBgZA)KzwiTCY=;yNX{4)a(nUXH6Hc!=)3Z*6t=d zFGZ!(;@#b5=k$qPmz9w*yjNmI$7SQXy{5$;Y0h%POq;G{0=+~KiyJ`Z-~nzAEkAR& zTS_F7oYA00oh#>ieL1qKlGuZvzVlDqOK;mVbf|WF-o9a~PO+cw{DC`Nrjgfc{O&b= z_v(^Y5<#!jGXK8d$pHH5P8)(kBkL7I&L<%OKG~m3pVC7{tHSzz@D4mHdTy zJr3oFd41+PV1W+|(~<98o3d9WE5Be!H(^ilgB87J0mHeQR;^Ny&IJ7FJ0%FcFs3Qb zR`CwVcCZr{CSFbccUT@o*aFFOCf%8?-a}^ced0fn?z_R;7XTLSn25g| z3+@|-iU<{3afz`rR=u~xTV)RjB9Lj5y3}ZLrRMK_RCyRoiOaZYL4z4P=UrzMJ*JM% z&1hBc5C#jLdv|WT1n@BMe*hjnailtPnnphj^D|5B6fGnHZKPy~_sUt9j)BJ;w&Ou4 z3Cp>@s~Ft=kFMd(FhCN|0kjH69Ae;4Swy@naopiHUB6RK2W7=_HUDGEUHa(%Nm}76+Uq1Rk{9eSircdr6@r5li+s^5{{E=J^y42Hr0`_Z7&+t+vElS2KCM0r%${VX$}t zTu*a#FFmGlOYSD}+|Vv=Wo|8H6Uhu8N7J`?Az9OX>7V&+E>!x{jd zhtU9Bm^hq2j6P-j1pZ$zmG-H-iPtQ6&By9_G$cR=-Pm^oc#Z`b<5%WEP{Be$?b-1D zt0S!uS!LXDGa)f3c7|TVqXLhs;W#N%^m1#u5bwLS%#7!0--3Y3a=Q^f_bs#b6g!Ob zoU*nbJFZTGfbG2n~ zD~1J378Pmmj1|7jKB)k6EZ6RAtffJYweEy=^gYqir~G|Z-Ja`pKPp=gIT_ROGEISg zwqEv5@zvW&K154A!}7vyON%MyPiH~xxRh4sUYrJri5lP<#QxwejsAnIXJdgNZSJ{$HI4LB?j)+Xbo?Hs@!rq%3cYuK zCBvU(?nLW+1*OumQRr7H>{l>mmdv{GSV0boowM_QdP&>12YqZ|{>$Zjdmxli} zBiMtkHrW(tTC~BZtT~ch^x{DV;Q-BZ;pokZ6lMMo9EOqDX*2j8y`vhLclIKJDXwA0 zI7GH1zO&B=!8AQ0#LS*(@{!-wcdDhD@9MRF)u~s5iTz^6o1ruR!zUbzSjf%3@d!E^ z`59$}P$Gk53?tN_r|c6HfGTl%657Z!b_;nl!mpvcbCAPprA?!r_nWuj04qdy%FqKp zjk#yV%zPnAWT(Jc1<3EKwf>jB@|Ax7q%Gc88W3M$u91%Q>b1l8okY+iGq3v=Q#EL3 zFS1=1FZc$Tg&j^~F+dqp_!G|$MyKvMqlW1%#nb!T+eo!Ec_%=u(hdea$Q{yh%gjKN z@6AKcegFKee?^nUVcj!$>%SZkfh+#vOSXv2{!Dw*t6~@8+?JV?ihyg6ZpOCQXakEZ zF?{X%=U+hb6;YqQv--=H1B$RE8U_K@IO|0y&U#)dZ<%_|Nx|#9jl{7N=%#++V2~ ze50tV-si5-n83pSfYyFEED|i~M&(C-{9kyxfb!rgrC@Jm5E2 zFCvT<^9dj$(8A+$|E3}PH@GT#n>hQrC7sb>%bRJK55A72q(F2#VF`yN)`~U8!YoX4wDkE<;&q%zp=pLqjU; zwvN16b1+3V>lOhT0OoEaWWGx=;x)Myc)C+EZwCDn&rowq2u{?}9gu$|y2az|O%>iT zXa%1n2B;U2DVC7;hYHPOFE$;+y{FbZZU>n+z7;DR$6cQt%a-*6C`An?Lu>}O#A%VB zon1)HLD!;Pffs+A@{Fmn)4PX`{oimi0EeqdF3@up2K}0ck~3t;gyzmS79ADZ6fDQe zYViF~$nfSp{e!m9e+xY54f!Am`eN2~n0XkNLOxqmvgX#;h&9|GQxG-iL5E0KF8z?6 zFbE7!nBaGx>>+;M_zA7~|ILhvcP*M&760#R15;*3qaj7|!Gp)I*@zgFK00heq^tHY zJrDziCipK3yCi`gFl>`u*N*xflAU18yux~cTJ~A zhSn_z0ga#%mQ^lEmY$8Qzd0~#2U_w!0}1Nb>hBA`1w!n)5+vz?5f|=YGN9Bxp5hyw zDV10vH$K>$7slc&-0lhy5=UP>|F@a{i_dEKqH%tzoBs>Pt0nZAUi9aC1ZmGN*vS-e^-Qs&i1ST$M6aW(Y~8h_PhCgCv}8KD6X$*zO)?)x*eo8|Mp=0{Kc2w{D~va zWX?nJgV~&*c0{Vdpb^xTYRBX+mgxh_57ZqxF$ZpypUzr_95*_-(AJr>>VX>_8ExzS zMhw=+83%AhaIzD2JOEO2nT08jL~$h2U?(}?e1CVIViwE8y}~Vv9dbl{xha^;(QH_U zhVK}c`dx_3D~B{cQpWNncaUIPyU4Ki5RivJMImkgwZjr>XvawgDX0$*T13KKg(Zd6 zs-OLbjd=2XX8$bpL8(fJ6+nQrRicvU9q6NFtlf$Ry*oy9dOS~Yg#7rWrb@uIiq}b_ z4AjolVg>lwEQex5U#+!kfdhvG%ae5W6vB+>?HURovQKBBYEEYf#tZwXt7BPk+e{7& zZ?i$>i;a^HM%*D*)4v$l@^SFp8HDfy&w!HQWMgB~9@g1Va|5RCZD2@prx^XBiW92I zY{(#Muz8?9AG}G#izVmMqam#=ZW(Ew+6MC?5Rp%3oI(P{JBad(Oc&4@Y@pI$NIgjh zrKh}<4Tbnbj!AZmHAuH}{M#dkn;@_>OEP;AdDez}qqAlg!kt3WKzl33s3!uA(q2ip zVimT+kwd8L(5Y67GLE3Q6=*AX04ahC_%X=GWkF64X(;zz4m43- zWk6&w=#Esg#vp(+^@b|1H81U1I17J^@k~~DaoDbT-I6Hh#8K(d$FQ$s-;av?=_Nm& zHe(nDz+*p*^dVdOEVCXTyn9U3zymJ*GWjkLSsKIn4m~A9`}XEQt|QuAOk?}u-X+q@9&Go(;I+11NUP~B2jT$`wIY7q0OqgX$T>}P|9n^x+%hulBd&lMPY-4KYKK%n&pN28H zmQnnPjfZ{CK*Vz9lxnytK7iXuZ~62y{eG_BFQCuRrx6xI=Y04P%O@IW(jBX%gjqNi zFi;xMI@&%Ck+>es_*k@6vX{G4-m8ubbwdVsR0ThKuHP3-A)$PvPo0`zuWZcmz-2ll zw!rWPiP1P{vmS}IZmIkR$YD{NQ~5DzvyNL*73jK!>fyi7^oy}}N8q9r{6Q7}gD&_3 zD58pHB&-bLz>(9`#l%9SJ|3VSD-NHXRbt)Ai02ae#ir4d8RkuI{n%!UjYoAjUGY`` z6&|qkXebTUPo8<^9^ajtf?61jAHmouz%sexci}<%n|RR22)_1k zcGD!96hN0X0j1J5Dnugt2N}!6dE}W{ z*|ML1qF?py=NjIhFU0q5#=WS-=&BkUM3;V?zY5HVb-z8@I*#*Sd>*jgjF^F^H1fXq zRKJ!r>dUV!OMnVq05BxeDDlKDD7f4h-kwnL*)OHC$ot7#54D}XNojPh7q#|fNKCVRo9iDKx z(v$c4UjqKJUliAw=xk?V%=5S+U;MJJ@{z zyaFQWE;av4R&k-py?6#QjYwM0<@2(g!c)2b0;m-m)^_WF08m8nIhw8uO;<~@$@+e* zHX)DB)Uf8}LynNO(SzMo3McfS2^cW3x5It^F}SxMdqC*=u16@tty`ZM*tt<&iaXnj z;mx!z=Bg77mH1q#ij=DDLXYV6KhRijBHv_WBD@+~M~{df#=0Y-ArcMXGhQrHpK2s3 zcOBNav%SkEp8Gx4|9MIvylDsKATwAa0{-+W_`I_{wAJ)nO6orQe~}8FJ8B39#CDh{n~6ZKY~t&{1!>&+)Oz-b+xK?1vzjYBzC(Ma zRp*)N%OoK7DOwfxqHc?nt0@B&YmOkKLB^O#D9A$H&;MJ7>CiHBnAjT%0;ZjWhL0ot z`XF-?clJz?@(!E={Mo^;mvb@|Jb`xMmW%~AQLtx0&7aa>mnWHih_3+Ti9Z1HY=^F? z^!k-}in#e1>GWXi+=nyF0r!5x?DB#=wE>XA-NKD>!8Nf9En0Gy{RNgoBh-59v$0XT zBKql9<`{Z|YG)doQ`=MoJf5IJ-&5+wl#c=5M~pYYSuByl%!D!1E`~pZj9R-8{EMgv zj>9T7$|4;u<-AhtD~^_eOmLq7m8xNeYT-*x`&ikNECLiXM2#r<_cEdzZ2)RJKto8( zt}I;g+NHW7y$R_MPa5cnGyKArmdKfA&?`_`IER36{qEBPmns@WG-{SgmL$6CjZ#i3S05_5BkU?I zLGeU281pQy7%yqGR4ITN#0p+bb8mmScDrOl35Z+TsM6>cH+~Se?+6=6hEbJon4hq( zN*N?Aj`(8mtwy^D;^g2s^ixZ}0XoP4GNboSH!0RdPIV2>-LFqfPLv4gs~dH;x8pJ$ zF5sw36kDX1wPZ*7T>}xQ+;qg5Ykh$Lwt^9eDe$w7@Q+xAo^4CEgccc~@Qr-KfMM@U zVmAG{ zYq)LxEm{nMZJ<;!Vs{^Pdq4&asD1AHuFeTvtRr!9;E&Q`Dd{|Rij%1MZC<(|@cY12 zIla?J3aL4%ZFkpox9>YIl;~m*e$J|51BX2!w{ov5G5$J|Xsn2j=^JT?_hh;tKp}B- z0*)$cIe=#K-K%x_-!TcS*-{1u`{vQ<3_uMziv6X|FfI1pZ@&2xLx2IO3OwO>Ixx5# z_QHsE{ckatm=UJ)M&B=ZQD5<*MV!ERx?<)vT*9TNz}lmdbPX-vmR_6N_ks=swIkze z2IYx)sB~vR)5OXqF7T=`Hd5ofF5v63n(ZO5zE(6uwU|3 zxi?Vu82{L2jH7|x%aNmyn{^L}=R3v)(bP2G;Nme#c5&3?cd9FTH6?jkI<{5X+XBS5 z2UAxZE}Tks8UsgXmbBbR)4}s+)1L>oz*77KOLF1+M9x8hibQ)!x^E9CkweI^x=>wKtM8o|y+4V#l z{j-_5(gzJOn*gY4-(;wwQng2jYH>XBXY|*gv%Vr(BQJ~^Y>DO6fkJI=#L1C z75@Z_R#_{mpiI*Ct||)7%l56-yHpgL^P{Bc84nF{zXl&v=_rVWlg5m>~iv)o9%n%}Qo@;x$**!MDc;rNvP;XoES`aTz9-E#t-S%0vd;t4ZFNO5;(&kuh<}(u%k4=Nng!KAod6f zr&7WSR9bL-l<_=0N?#+#AYK0`*>eflDha&IzK`TRL0H63MZf074S#mdq1-FX?h0w> z`9aLeDQ>uTfUov5E)2_(sWhOoCE-Ek|03DHDY(qfx`-oe!kjBca0)Jtn1X^Ig{nz) z3P%ec=ZL+|BM`2Xe$*6u*E5*+272pqhSaN=pG}py5Z^EW{!zY2&ud+`qC<$8h1IWP z#2Au`6tIMCJ+`(^f1j@~egjr9C;fW=4T(mZQ18##dfyCwAPW=JYTL-$?=&0-y0Hu4 zPp1-sE7O)##mu1ldOL_AACi*#mS>ygyN5De3HN|+9T^qF&tWi<67sri*g-XAeTl$& zMJJ*4qf`KkJa1_V18)O=y@OdbxegtS-rvIhoiiSgV{fOT?KotsT4*z`(JNp&18C9H zvN&aUz3v$@1P^aFPnBDjRPb%%hg{dJNIdQ?fQgT{VpC6s8THb>zQL z_Ly?BJu*9PmM4*+myaFjGAiU3Nx@3$GNiJ7z?=h^2;&_y+)o8HV~mMF_&-{0-m+Tp zYVGNbbb!WVrvc5F|8eD5FnbYl(tx!rsQKY1SK@(+ED4;gZ6w`<>cDmmnt=;t`GMhz z!EM1q`Qck)0Gxm4s=P(I_!P=OU%8EhTwX@%Rzk9E@b32{Hf}3Kt@hcrwY0>!7fOi* z_d#NYJ2Ff9d7#-reT7-EN^BblFtUH$##p5l#KsYV}t03ynh;yMZJg>HC0>yM zxTX`ET}$P!#PS5gg5x{9xCgU@(sq~bUH6k!Bup5KO_ZwVHbazP|MMzzDqB5fv4Vcx zHjYLvL3#f5WKs8vj+Dz7*`f#X2GspkTEb{_m>O{EsDpG5&VVPynWG|R90%0DD9~9Z>)P+|TXN5rrstIdW?J-xRij;kv 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

  • bN~GUP)F;!!xECEHr*OT>!${}svm*DSr#zVGe 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≪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