Repository: hectorqin/reader Branch: master Commit: 49e6d4aefed0 Files: 375 Total size: 2.7 MB Directory structure: gitextract_mc5_awkk/ ├── .dockerignore ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── bug-report--问题反馈-.md │ └── workflows/ │ ├── Dockerfile │ ├── Openj9-Dockerfile │ ├── build.yml │ ├── pull-request.yml │ ├── release-github.yml │ ├── release-openj9.yml │ └── release.yml ├── .gitignore ├── Dockerfile ├── Dockerfile.source ├── LICENSE ├── README.md ├── UPDATELOG.md ├── assets/ │ └── mac/ │ └── reader.icns ├── build.gradle.kts ├── build.sh ├── cli.gradle ├── doc.md ├── docker-compose.yaml ├── docker-compose.yml ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── nixpacks.toml ├── preview.md ├── reader.sh ├── server/ │ ├── bin/ │ │ ├── shutdown.cmd │ │ ├── shutdown.sh │ │ ├── startup.cmd │ │ └── startup.sh │ └── conf/ │ └── application.properties ├── settings.gradle ├── src/ │ ├── lib/ │ │ ├── rhino-1.7.13-1.jar │ │ └── xmlpull-1.1.3.1.jar │ ├── main/ │ │ ├── java/ │ │ │ ├── com/ │ │ │ │ └── htmake/ │ │ │ │ └── reader/ │ │ │ │ ├── ReaderApplication.kt │ │ │ │ ├── ReaderUIApplication.kt │ │ │ │ ├── SpringEvent.java │ │ │ │ ├── api/ │ │ │ │ │ ├── ReturnData.kt │ │ │ │ │ ├── YueduApi.kt │ │ │ │ │ └── controller/ │ │ │ │ │ ├── BaseController.kt │ │ │ │ │ ├── BookController.kt │ │ │ │ │ ├── BookSourceController.kt │ │ │ │ │ ├── BookmarkController.kt │ │ │ │ │ ├── ReplaceRuleController.kt │ │ │ │ │ ├── RssSourceController.kt │ │ │ │ │ ├── UserController.kt │ │ │ │ │ └── WebdavController.kt │ │ │ │ ├── config/ │ │ │ │ │ ├── AppConfig.kt │ │ │ │ │ └── BookConfig.kt │ │ │ │ ├── entity/ │ │ │ │ │ ├── BasicError.kt │ │ │ │ │ ├── Size.kt │ │ │ │ │ └── User.kt │ │ │ │ ├── init/ │ │ │ │ │ └── appCtx.kt │ │ │ │ ├── utils/ │ │ │ │ │ ├── Ext.kt │ │ │ │ │ ├── SpringContextUtils.java │ │ │ │ │ └── VertExt.kt │ │ │ │ └── verticle/ │ │ │ │ └── RestVerticle.kt │ │ │ ├── io/ │ │ │ │ └── legado/ │ │ │ │ └── app/ │ │ │ │ ├── README.md │ │ │ │ ├── constant/ │ │ │ │ │ ├── Action.kt │ │ │ │ │ ├── AppConst.kt │ │ │ │ │ ├── AppPattern.kt │ │ │ │ │ ├── BookType.kt │ │ │ │ │ ├── DeepinkBookSource.kt │ │ │ │ │ ├── PreferKey.kt │ │ │ │ │ ├── RSSKeywords.kt │ │ │ │ │ └── Status.kt │ │ │ │ ├── data/ │ │ │ │ │ └── entities/ │ │ │ │ │ ├── BaseBook.kt │ │ │ │ │ ├── BaseSource.kt │ │ │ │ │ ├── Book.kt │ │ │ │ │ ├── BookChapter.kt │ │ │ │ │ ├── BookGroup.kt │ │ │ │ │ ├── BookSource.kt │ │ │ │ │ ├── Bookmark.kt │ │ │ │ │ ├── Cache.kt │ │ │ │ │ ├── Cookie.kt │ │ │ │ │ ├── ReplaceRule.kt │ │ │ │ │ ├── RssArticle.kt │ │ │ │ │ ├── RssSource.kt │ │ │ │ │ ├── SearchBook.kt │ │ │ │ │ ├── SearchKeyword.kt │ │ │ │ │ ├── SearchResult.kt │ │ │ │ │ ├── TxtTocRule.kt │ │ │ │ │ └── rule/ │ │ │ │ │ ├── BookInfoRule.kt │ │ │ │ │ ├── BookListRule.kt │ │ │ │ │ ├── ContentRule.kt │ │ │ │ │ ├── ExploreRule.kt │ │ │ │ │ ├── SearchRule.kt │ │ │ │ │ └── TocRule.kt │ │ │ │ ├── exception/ │ │ │ │ │ ├── ConcurrentException.kt │ │ │ │ │ ├── ContentEmptyException.kt │ │ │ │ │ ├── NoStackTraceException.kt │ │ │ │ │ ├── RegexTimeoutException.kt │ │ │ │ │ └── TocEmptyException.kt │ │ │ │ ├── help/ │ │ │ │ │ ├── BookHelp.kt │ │ │ │ │ ├── CacheManager.kt │ │ │ │ │ ├── DefaultData.kt │ │ │ │ │ ├── EncodingDetectHelp.java │ │ │ │ │ ├── JsExtensions.kt │ │ │ │ │ ├── coroutine/ │ │ │ │ │ │ ├── CompositeCoroutine.kt │ │ │ │ │ │ ├── Coroutine.kt │ │ │ │ │ │ └── CoroutineContainer.kt │ │ │ │ │ └── http/ │ │ │ │ │ ├── AjaxWebView.kt │ │ │ │ │ ├── ByteConverter.kt │ │ │ │ │ ├── CookieStore.kt │ │ │ │ │ ├── CoroutinesCallAdapterFactory.kt │ │ │ │ │ ├── EncodeConverter.kt │ │ │ │ │ ├── HttpHelper.kt │ │ │ │ │ ├── OkHttpUtils.kt │ │ │ │ │ ├── RequestMethod.kt │ │ │ │ │ ├── Res.kt │ │ │ │ │ ├── SSLHelper.kt │ │ │ │ │ ├── StrResponse.kt │ │ │ │ │ └── api/ │ │ │ │ │ └── CookieManager.kt │ │ │ │ ├── lib/ │ │ │ │ │ └── icu4j/ │ │ │ │ │ ├── CharsetDetector.java │ │ │ │ │ ├── CharsetMatch.java │ │ │ │ │ ├── CharsetRecog_2022.java │ │ │ │ │ ├── CharsetRecog_UTF8.java │ │ │ │ │ ├── CharsetRecog_Unicode.java │ │ │ │ │ ├── CharsetRecog_mbcs.java │ │ │ │ │ ├── CharsetRecog_sbcs.java │ │ │ │ │ └── CharsetRecognizer.java │ │ │ │ ├── model/ │ │ │ │ │ ├── Debug.kt │ │ │ │ │ ├── DebugLog.kt │ │ │ │ │ ├── Debugger.kt │ │ │ │ │ ├── README.md │ │ │ │ │ ├── analyzeRule/ │ │ │ │ │ │ ├── AnalyzeByJSonPath.kt │ │ │ │ │ │ ├── AnalyzeByJSoup.kt │ │ │ │ │ │ ├── AnalyzeByRegex.kt │ │ │ │ │ │ ├── AnalyzeByXPath.kt │ │ │ │ │ │ ├── AnalyzeRule.kt │ │ │ │ │ │ ├── AnalyzeUrl.kt │ │ │ │ │ │ ├── QueryTTF.java │ │ │ │ │ │ ├── RuleAnalyzer.kt │ │ │ │ │ │ ├── RuleData.kt │ │ │ │ │ │ └── RuleDataInterface.kt │ │ │ │ │ ├── localBook/ │ │ │ │ │ │ ├── CbzFile.kt │ │ │ │ │ │ ├── EpubFile.kt │ │ │ │ │ │ ├── LocalBook.kt │ │ │ │ │ │ ├── TextFile.kt │ │ │ │ │ │ └── UmdFile.kt │ │ │ │ │ ├── rss/ │ │ │ │ │ │ ├── Rss.kt │ │ │ │ │ │ ├── RssParserByRule.kt │ │ │ │ │ │ └── RssParserDefault.kt │ │ │ │ │ └── webBook/ │ │ │ │ │ ├── BookChapterList.kt │ │ │ │ │ ├── BookContent.kt │ │ │ │ │ ├── BookInfo.kt │ │ │ │ │ ├── BookList.kt │ │ │ │ │ └── WebBook.kt │ │ │ │ └── utils/ │ │ │ │ ├── ACache.kt │ │ │ │ ├── AnkoHelps.kt │ │ │ │ ├── Base64.java │ │ │ │ ├── EncoderUtils.kt │ │ │ │ ├── EncodingDetect.kt │ │ │ │ ├── FileExtensions.kt │ │ │ │ ├── FilesUtil.kt │ │ │ │ ├── GsonExtensions.kt │ │ │ │ ├── HtmlFormatter.kt │ │ │ │ ├── JsonExtensions.kt │ │ │ │ ├── JsoupExtensions.kt │ │ │ │ ├── LogUtils.kt │ │ │ │ ├── MD5Utils.kt │ │ │ │ ├── NetworkUtils.kt │ │ │ │ ├── SourceAnalyzer.kt │ │ │ │ ├── StringExtensions.kt │ │ │ │ ├── StringUtils.kt │ │ │ │ ├── TextUtils.java │ │ │ │ ├── ThrowableExtensions.kt │ │ │ │ ├── UTF8BOMFighter.kt │ │ │ │ ├── Utf8BomUtils.kt │ │ │ │ └── ZipUtils.kt │ │ │ ├── me/ │ │ │ │ └── ag2s/ │ │ │ │ ├── epublib/ │ │ │ │ │ ├── Constants.java │ │ │ │ │ ├── browsersupport/ │ │ │ │ │ │ ├── NavigationEvent.java │ │ │ │ │ │ ├── NavigationEventListener.java │ │ │ │ │ │ ├── NavigationHistory.java │ │ │ │ │ │ ├── Navigator.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ ├── domain/ │ │ │ │ │ │ ├── Author.java │ │ │ │ │ │ ├── Date.java │ │ │ │ │ │ ├── EpubBook.java │ │ │ │ │ │ ├── EpubResourceProvider.java │ │ │ │ │ │ ├── FileResourceProvider.java │ │ │ │ │ │ ├── Guide.java │ │ │ │ │ │ ├── GuideReference.java │ │ │ │ │ │ ├── Identifier.java │ │ │ │ │ │ ├── LazyResource.java │ │ │ │ │ │ ├── LazyResourceProvider.java │ │ │ │ │ │ ├── ManifestItemProperties.java │ │ │ │ │ │ ├── ManifestItemRefProperties.java │ │ │ │ │ │ ├── ManifestProperties.java │ │ │ │ │ │ ├── MediaType.java │ │ │ │ │ │ ├── MediaTypes.java │ │ │ │ │ │ ├── Metadata.java │ │ │ │ │ │ ├── Relator.java │ │ │ │ │ │ ├── Resource.java │ │ │ │ │ │ ├── ResourceInputStream.java │ │ │ │ │ │ ├── ResourceReference.java │ │ │ │ │ │ ├── Resources.java │ │ │ │ │ │ ├── Spine.java │ │ │ │ │ │ ├── SpineReference.java │ │ │ │ │ │ ├── TOCReference.java │ │ │ │ │ │ ├── TableOfContents.java │ │ │ │ │ │ └── TitledResourceReference.java │ │ │ │ │ ├── epub/ │ │ │ │ │ │ ├── BookProcessor.java │ │ │ │ │ │ ├── BookProcessorPipeline.java │ │ │ │ │ │ ├── DOMUtil.java │ │ │ │ │ │ ├── EpubProcessorSupport.java │ │ │ │ │ │ ├── EpubReader.java │ │ │ │ │ │ ├── EpubWriter.java │ │ │ │ │ │ ├── HtmlProcessor.java │ │ │ │ │ │ ├── NCXDocumentV2.java │ │ │ │ │ │ ├── NCXDocumentV3.java │ │ │ │ │ │ ├── PackageDocumentBase.java │ │ │ │ │ │ ├── PackageDocumentMetadataReader.java │ │ │ │ │ │ ├── PackageDocumentMetadataWriter.java │ │ │ │ │ │ ├── PackageDocumentReader.java │ │ │ │ │ │ ├── PackageDocumentWriter.java │ │ │ │ │ │ └── ResourcesLoader.java │ │ │ │ │ └── util/ │ │ │ │ │ ├── CollectionUtil.java │ │ │ │ │ ├── IOUtil.java │ │ │ │ │ ├── NoCloseOutputStream.java │ │ │ │ │ ├── NoCloseWriter.java │ │ │ │ │ ├── ResourceUtil.java │ │ │ │ │ ├── StringUtil.java │ │ │ │ │ └── commons/ │ │ │ │ │ └── io/ │ │ │ │ │ ├── BOMInputStream.java │ │ │ │ │ ├── ByteOrderMark.java │ │ │ │ │ ├── IOConsumer.java │ │ │ │ │ ├── ProxyInputStream.java │ │ │ │ │ ├── XmlStreamReader.java │ │ │ │ │ └── XmlStreamReaderException.java │ │ │ │ └── umdlib/ │ │ │ │ ├── domain/ │ │ │ │ │ ├── UmdBook.java │ │ │ │ │ ├── UmdChapters.java │ │ │ │ │ ├── UmdCover.java │ │ │ │ │ ├── UmdEnd.java │ │ │ │ │ └── UmdHeader.java │ │ │ │ ├── tool/ │ │ │ │ │ ├── StreamReader.java │ │ │ │ │ ├── UmdUtils.java │ │ │ │ │ └── WrapOutputStream.java │ │ │ │ └── umd/ │ │ │ │ └── UmdReader.java │ │ │ └── org/ │ │ │ └── kxml2/ │ │ │ ├── io/ │ │ │ │ ├── KXmlParser.java │ │ │ │ └── KXmlSerializer.java │ │ │ ├── kdom/ │ │ │ │ ├── Document.java │ │ │ │ ├── Element.java │ │ │ │ └── Node.java │ │ │ └── wap/ │ │ │ ├── Wbxml.java │ │ │ ├── WbxmlParser.java │ │ │ ├── WbxmlSerializer.java │ │ │ ├── syncml/ │ │ │ │ └── SyncML.java │ │ │ ├── wml/ │ │ │ │ └── Wml.java │ │ │ └── wv/ │ │ │ └── WV.java │ │ └── resources/ │ │ ├── META-INF/ │ │ │ └── services/ │ │ │ └── org.xmlpull.v1.XmlPullParserFactory │ │ ├── application-prod.yml │ │ ├── application.yml │ │ ├── banner.txt │ │ ├── defaultData/ │ │ │ └── txtTocRule.json │ │ ├── dtd/ │ │ │ ├── openebook.org/ │ │ │ │ └── dtds/ │ │ │ │ └── oeb-1.2/ │ │ │ │ ├── oeb12.ent │ │ │ │ └── oebpkg12.dtd │ │ │ ├── www.daisy.org/ │ │ │ │ └── z3986/ │ │ │ │ └── 2005/ │ │ │ │ └── ncx-2005-1.dtd │ │ │ └── www.w3.org/ │ │ │ └── TR/ │ │ │ ├── ruby/ │ │ │ │ └── xhtml-ruby-1.mod │ │ │ ├── xhtml-modularization/ │ │ │ │ └── DTD/ │ │ │ │ ├── xhtml-arch-1.mod │ │ │ │ ├── xhtml-attribs-1.mod │ │ │ │ ├── xhtml-base-1.mod │ │ │ │ ├── xhtml-bdo-1.mod │ │ │ │ ├── xhtml-blkphras-1.mod │ │ │ │ ├── xhtml-blkpres-1.mod │ │ │ │ ├── xhtml-blkstruct-1.mod │ │ │ │ ├── xhtml-charent-1.mod │ │ │ │ ├── xhtml-csismap-1.mod │ │ │ │ ├── xhtml-datatypes-1.mod │ │ │ │ ├── xhtml-datatypes-1.mod.1 │ │ │ │ ├── xhtml-edit-1.mod │ │ │ │ ├── xhtml-events-1.mod │ │ │ │ ├── xhtml-form-1.mod │ │ │ │ ├── xhtml-framework-1.mod │ │ │ │ ├── xhtml-hypertext-1.mod │ │ │ │ ├── xhtml-image-1.mod │ │ │ │ ├── xhtml-inlphras-1.mod │ │ │ │ ├── xhtml-inlpres-1.mod │ │ │ │ ├── xhtml-inlstruct-1.mod │ │ │ │ ├── xhtml-inlstyle-1.mod │ │ │ │ ├── xhtml-lat1.ent │ │ │ │ ├── xhtml-link-1.mod │ │ │ │ ├── xhtml-list-1.mod │ │ │ │ ├── xhtml-meta-1.mod │ │ │ │ ├── xhtml-notations-1.mod │ │ │ │ ├── xhtml-object-1.mod │ │ │ │ ├── xhtml-param-1.mod │ │ │ │ ├── xhtml-pres-1.mod │ │ │ │ ├── xhtml-qname-1.mod │ │ │ │ ├── xhtml-script-1.mod │ │ │ │ ├── xhtml-special.ent │ │ │ │ ├── xhtml-ssismap-1.mod │ │ │ │ ├── xhtml-struct-1.mod │ │ │ │ ├── xhtml-style-1.mod │ │ │ │ ├── xhtml-symbol.ent │ │ │ │ ├── xhtml-symbol.ent.1 │ │ │ │ ├── xhtml-table-1.mod │ │ │ │ ├── xhtml-text-1.mod │ │ │ │ └── xhtml11-model-1.mod │ │ │ ├── xhtml1/ │ │ │ │ └── DTD/ │ │ │ │ ├── xhtml-lat1.ent │ │ │ │ ├── xhtml-special.ent │ │ │ │ ├── xhtml-symbol.ent │ │ │ │ ├── xhtml1-strict.dtd │ │ │ │ └── xhtml1-transitional.dtd │ │ │ └── xhtml11/ │ │ │ └── DTD/ │ │ │ └── xhtml11.dtd │ │ ├── epub/ │ │ │ ├── chapter.html │ │ │ ├── cover.html │ │ │ ├── fonts.css │ │ │ ├── intro.html │ │ │ └── main.css │ │ └── logback-spring.xml │ └── test/ │ └── java/ │ └── com/ │ └── htmake/ │ └── reader/ │ └── ReaderApplicationTests.java ├── vetur.config.js └── web/ ├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── jsconfig.json ├── package.json ├── postcss.config.js ├── public/ │ ├── bookSourceDebug/ │ │ ├── index.css │ │ ├── index.html │ │ └── index.js │ ├── browsertest.html │ ├── index.html │ ├── robots.txt │ └── sw.js ├── src/ │ ├── App.vue │ ├── assets/ │ │ └── fonts/ │ │ └── iconfont.css │ ├── components/ │ │ ├── AddUser.vue │ │ ├── BookGroup.vue │ │ ├── BookInfo.vue │ │ ├── BookManage.vue │ │ ├── BookShelf.vue │ │ ├── BookSource.vue │ │ ├── Bookmark.vue │ │ ├── BookmarkForm.vue │ │ ├── Content.vue │ │ ├── Explore.vue │ │ ├── LocalStore.vue │ │ ├── MPCode.vue │ │ ├── PopCatalog.vue │ │ ├── ReadSettings.vue │ │ ├── ReplaceRule.vue │ │ ├── ReplaceRuleForm.vue │ │ ├── RssArticle.vue │ │ ├── RssArticleList.vue │ │ ├── RssSourceList.vue │ │ ├── SearchBookContent.vue │ │ ├── UserManage.vue │ │ └── WebDAV.vue │ ├── main.js │ ├── plugins/ │ │ ├── animate.js │ │ ├── axios.js │ │ ├── cache.js │ │ ├── chinese.js │ │ ├── config.js │ │ ├── element.js │ │ ├── eventBus.js │ │ ├── helper.js │ │ ├── jump.js │ │ ├── md5.js │ │ ├── safe-json-stringify.js │ │ └── vuex.js │ ├── registerServiceWorker.js │ ├── router/ │ │ └── index.js │ └── views/ │ ├── Index.vue │ └── Reader.vue └── vue.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ HELP.md .gradle build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/** !**/src/test/** ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr out/ ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ ### VS Code ### .vscode/ .vertx out #/src/main/resources/application-prod.yml /src/main/resources/application-default.yml /src/main/resources/web /storage* /logs /bin /file-uploads node_modules /reader-assets ================================================ FILE: .gitattributes ================================================ *.java linguist-language=Kotlin ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report--问题反馈-.md ================================================ --- name: Bug report (问题反馈) about: 描述你在使用中遇到的问题(issue语言:1. 中文;2. 英文) title: '' labels: '' assignees: '' --- **为避免无效问题和冗余问题,提问前请确认** 1. 你确定Google不能解决你的问题 2. 你确定已有的issue不能解决你的问题 3. 你确定issue的title按照格式如下:[web/simple-web/server]:description **Describe the bug 描述你遇到的问题** A clear and concise description of what the bug is. 简洁有效的说明。 **To Reproduce 如何重现问题** Steps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior 期待修复的效果** A clear and concise description of what you expected to happen. 简单描述。 **Screenshots 如有必要,可以截图说明** If applicable, add screenshots to help explain your problem. **版本说明** - OS: [e.g. win] 说明操作系统 - Deploy Method 说明软件部署方式 - Program Version 说明软件版本 - Browser [e.g. chrome, safari] 说明终端、浏览器型号 **Additional context 其他说明** Add any other context about the problem here. 添加你认为有必要的内容,否则不写。 ================================================ FILE: .github/workflows/Dockerfile ================================================ FROM openjdk:8-jdk-alpine # Install base packages RUN \ # apk update; \ # apk upgrade; \ # Add CA certs tini tzdata apk add --no-cache ca-certificates tini tzdata; \ update-ca-certificates; \ # Clean APK cache rm -rf /var/cache/apk/*; # 时区 ENV TZ=Asia/Shanghai EXPOSE 8080 ENTRYPOINT ["/sbin/tini", "--"] ADD ./reader.jar /app/bin/reader.jar CMD ["java", "-jar", "/app/bin/reader.jar" ] ================================================ FILE: .github/workflows/Openj9-Dockerfile ================================================ FROM ibm-semeru-runtimes:open-8u332-b09-jre # Install base packages RUN \ apt-get update; \ apt-get install -y ca-certificates tini tzdata; \ update-ca-certificates; \ # Clean apt cache rm -rf /var/lib/apt/lists/* # 时区 ENV TZ=Asia/Shanghai EXPOSE 8080 ENTRYPOINT ["/usr/bin/tini", "--"] ADD ./reader.jar /app/bin/reader.jar CMD ["java", "-jar", "/app/bin/reader.jar" ] ================================================ FILE: .github/workflows/build.yml ================================================ name: Build Docker Image on: push: branches: - master jobs: build: if: github.repository == 'hectorqin/reader' runs-on: ubuntu-latest steps: - uses: actions/checkout@master - name: Publish to Registry uses: elgohr/Publish-Docker-Github-Action@master with: name: hectorqin/reader-basic username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} snapshot: true tags: "test" ================================================ FILE: .github/workflows/pull-request.yml ================================================ name: Pull Request Check on: pull_request: types: [synchronize, reopened, labeled] branches: - master concurrency: group: ${{ github.head_ref }} cancel-in-progress: true defaults: run: shell: bash jobs: docker: if: github.repository == 'hectorqin/reader' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 with: ref: ${{ github.event.pull_request.base.sha }} clean: false - name: Setup node uses: actions/setup-node@v2 with: node-version: '14' - name: Build web run: cd web && npm install && npm run build - name: Setup Java uses: actions/setup-java@v2 with: distribution: 'temurin' java-version: '8' cache: 'gradle' - name: Build Java run: mv ./web/dist ./src/main/resources/web && rm src/main/java/com/htmake/reader/ReaderUIApplication.kt && gradle -b cli.gradle assemble --info && mv ./build/libs/*.jar ./reader.jar ================================================ FILE: .github/workflows/release-github.yml ================================================ name: Publish Github Releases on: # push: # tags: # - 'v**' # branches: # - master workflow_dispatch: jobs: buildRelease: if: github.repository == 'hectorqin/reader' name: "Build And Release" runs-on: macos-11 steps: - name: Checkout uses: actions/checkout@v2 - name: Setup node uses: actions/setup-node@v2 with: node-version: '14' - name: Build web run: cd web && npm install && npm run build && mv ./dist ../src/main/resources/web - name: Setup Java uses: actions/setup-java@v2 with: distribution: 'temurin' java-version: '11' cache: 'gradle' - name: Build MacOS package run: JAVAFX_PLATFORM=mac ./gradlew packageReaderMac - name: Build Linux package run: JAVAFX_PLATFORM=linux ./gradlew packageReaderLinux - name: Build Windows package run: JAVAFX_PLATFORM=win ./gradlew packageReaderWin - name: Build server jar run: rm src/main/java/com/htmake/reader/ReaderUIApplication.kt && gradle -b cli.gradle assemble --info - name: Show files. run: | echo Showing current directory: ls echo Showing ./target directory: ls ./build echo Showing ./target directory: ls ./build/libs - name: Pre Release if: ${{contains(github.ref, 'master')}} uses: "marvinpinto/action-automatic-releases@latest" with: repo_token: "${{ secrets.GITHUB_TOKEN }}" automatic_release_tag: "latest" prerelease: true title: "Development Build" files: | ./build/*.pkg ./build/*.zip ./build/libs/*.jar - name: Tagged Release if: ${{contains(github.ref, 'v')}} uses: "marvinpinto/action-automatic-releases@latest" with: repo_token: "${{ secrets.GITHUB_TOKEN }}" prerelease: false files: | ./build/*.pkg ./build/*.zip ./build/libs/*.jar ================================================ FILE: .github/workflows/release-openj9.yml ================================================ name: Publish Docker Multi-Platform Images Using Openj9 on: # push: # tags: # - 'v**' workflow_dispatch: jobs: docker: if: github.repository == 'hectorqin/reader' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Setup node uses: actions/setup-node@v2 with: node-version: '14' - name: Build web run: cd web && npm install && npm run build - name: Setup Java uses: actions/setup-java@v2 with: distribution: 'adopt-openj9' java-version: '8' cache: 'gradle' - name: Build Java run: mv ./web/dist ./src/main/resources/web && rm src/main/java/com/htmake/reader/ReaderUIApplication.kt && gradle -b cli.gradle assemble --info && mv ./build/libs/*.jar ./reader.jar - name: Docker meta id: meta uses: docker/metadata-action@v3 with: # list of Docker images to use as base name for tags images: | hectorqin/reader-basic # generate Docker tags based on the following events/attributes flavor: | latest=false prefix=openj9-,onlatest=true suffix= tags: | type=semver,pattern={{version}} type=raw,value=latest,enable=${{ !contains(github.ref, 'beta') }} - name: Set up QEMU uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - name: Login to DockerHub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push uses: docker/build-push-action@v2 with: context: . file: ./.github/workflows/Openj9-Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} platforms: | linux/amd64 linux/arm64/v8 linux/ppc64le linux/s390x ================================================ FILE: .github/workflows/release.yml ================================================ name: Publish Docker Multi-Platform Images on: # push: # tags: # - 'v**' workflow_dispatch: jobs: docker: if: github.repository == 'hectorqin/reader' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Setup node uses: actions/setup-node@v2 with: node-version: '14' - name: Build web run: cd web && npm install && npm run build - name: Setup Java uses: actions/setup-java@v2 with: distribution: 'temurin' java-version: '8' cache: 'gradle' - name: Build Java run: mv ./web/dist ./src/main/resources/web && rm src/main/java/com/htmake/reader/ReaderUIApplication.kt && gradle -b cli.gradle assemble --info && mv ./build/libs/*.jar ./reader.jar - name: Docker meta id: meta uses: docker/metadata-action@v3 with: # list of Docker images to use as base name for tags images: | hectorqin/reader-basic # generate Docker tags based on the following events/attributes tags: | type=semver,pattern={{version}} type=raw,value=latest,enable=${{ !contains(github.ref, 'beta') }} - name: Set up QEMU uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - name: Login to DockerHub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push uses: docker/build-push-action@v2 with: context: . file: ./.github/workflows/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} platforms: | linux/amd64 linux/arm64 linux/arm/v6 linux/arm/v7 linux/386 linux/ppc64le linux/s390x ================================================ FILE: .gitignore ================================================ HELP.md .gradle build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/** !**/src/test/** ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr out/ ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ ### VS Code ### .vscode/ .vertx out #/src/main/resources/application-prod.yml /src/main/resources/application-default.yml /src/main/resources/web /storage* /logs /bin /file-uploads /reader-assets /.lh/ /simple-web/ /server/logs /server/storage /server/target ================================================ FILE: Dockerfile ================================================ FROM hectorqin/reader # 时区 ENV TZ=Asia/Shanghai EXPOSE 8080 ENTRYPOINT ["/sbin/tini", "--"] CMD ["java", "-jar", "/app/bin/reader.jar" ] ================================================ FILE: Dockerfile.source ================================================ FROM node:lts-alpine3.14 AS build-web ADD . /app WORKDIR /app/web # Build web RUN yarn && yarn build # Build jar FROM gradle:6.1.1-jdk8 AS build-env ADD --chown=gradle:gradle . /app WORKDIR /app COPY --from=build-web /app/web/dist /app/src/main/resources/web RUN \ rm src/main/java/com/htmake/reader/ReaderUIApplication.kt; \ gradle -b cli.gradle assemble --info; \ mv ./build/libs/*.jar ./build/libs/reader.jar FROM amazoncorretto:8u332-alpine3.14-jre # Install base packages RUN \ # apk update; \ # apk upgrade; \ # Add CA certs tini tzdata apk add --no-cache ca-certificates tini tzdata; \ update-ca-certificates; \ # Clean APK cache rm -rf /var/cache/apk/*; # 时区 ENV TZ=Asia/Shanghai #RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ # && echo Asia/Shanghai > /etc/timdezone \ # && dpkg-reconfigure -f noninteractive tzdata EXPOSE 8080 ENTRYPOINT ["/sbin/tini", "--"] # COPY --from=hengyunabc/arthas:latest /opt/arthas /opt/arthas COPY --from=build-env /app/build/libs/reader.jar /app/bin/reader.jar CMD ["java", "-jar", "/app/bin/reader.jar" ] ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Reader Copyright (C) 2022 hectorqin This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ # reader 阅读3服务器版,不需要手机。 加入TG群(限时开放) 👉 [https://t.me/+pQ8HDlANPZ84ZWNl](https://t.me/+pQ8HDlANPZ84ZWNl) 关注公众号,查看教程和书源👇 ![](imgs/mpcode.png) [![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](https://dartnode.com "Powered by DartNode - Free VPS for Open Source") > 注意❗️ > > Reader 完整源码仅开放到 v2.5.4,新版本当前仅开放部分开源源码,见 https://github.com/hectorqin/reader-legado.
免责声明(Disclaimer) 阅读是一款提供网络文学搜索的工具,为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。 当您搜索一本书的时,阅读会将该书的书名以关键词的形式提交到各个第三方网络文学网站。各第三方网站返回的内容与阅读无关,阅读对其概不负责,亦不承担任何法律责任。任何通过使用阅读而链接到的第三方网页均系他人制作或提供,您可能从第三方网页上获得其他服务,阅读对其合法性概不负责,亦不承担任何法律责任。第三方搜索引擎结果根据您提交的书名自动搜索获得并提供试读,不代表阅读赞成或被搜索链接到的第三方网页上的内容或立场。您应该对使用搜索引擎的结果自行承担风险。 阅读不做任何形式的保证:不保证第三方搜索引擎的搜索结果满足您的要求,不保证搜索服务不中断,不保证搜索结果的安全性、正确性、及时性、合法性。因网络状况、通讯线路、第三方网站等任何原因而导致您不能正常使用阅读,阅读不承担任何法律责任。阅读尊重并保护所有使用阅读用户的个人隐私权,您注册的用户名、电子邮件地址等个人资料,非经您亲自许可或根据相关法律、法规的强制性规定,阅读不会主动地泄露给第三方。 阅读致力于最大程度地减少网络文学阅读者在自行搜寻过程中的无意义的时间浪费,通过专业搜索展示不同网站中网络文学的最新章节。阅读在为广大小说爱好者提供方便、快捷舒适的试读体验的同时,也使优秀网络文学得以迅速、更广泛的传播,从而达到了在一定程度促进网络文学充分繁荣发展之目的。阅读鼓励广大小说爱好者通过阅读发现优秀网络小说及其提供商,并建议阅读正版图书。任何单位或个人认为通过阅读搜索链接到的第三方网页内容可能涉嫌侵犯其信息网络传播权,应该及时向阅读提出书面权力通知,并提供身份证明、权属证明及详细侵权情况证明。阅读在收到上述法律文件后,将会依法尽快断开相关链接内容。
功能说明 书源管理
- 书架管理
- 书架布局
- 搜索
- 书海
- 看书
- 移动端适配
- 换源
- 翻页方式
- 手势支持
- 自定义主题
- 自定义样式
- WebDAV同步
- 文字替换过滤
- 听书<仅部分浏览器支持,手机端会因为锁屏而失效>
- 用户配置备份恢复
- 支持漫画
- 支持音频
- 书源失效检测
- 导入本地TXT、EPUB、UMD、PDF格式的书籍
- 书籍分组
- RSS订阅
- 定时更新书架
- 并发搜书
- 本地书仓
- 支持kindle阅读
## 下载与安装 详见[文档](https://github.com/hectorqin/reader/blob/master/doc.md) ## 问题 - 部分使用了 `Javascript` 的书源可能会报错,如调用原生java等高级Javascript功能 - `webview` 功能需要另外部署接口,不支持 `sourceRegex` 匹配资源响应 - 不支持书源登录功能 ## 感谢 - 项目初期参考了 [lightink-小说API](https://github.com/lightink-qingmo/lightink-server) - [阅读](https://github.com/gedoor/MyBookshelf) - [阅读3.0](https://github.com/gedoor/legado) - 项目初期参考了 [阅读3.0Web端](https://github.com/celetor/web-yuedu3) ## 其它 - [帮助文档](https://github.com/hectorqin/reader/blob/master/doc.md) - [界面预览](https://github.com/hectorqin/reader/blob/master/preview.md) ================================================ FILE: UPDATELOG.md ================================================ # Update Log ## v3.2.6 ### Features - 新增清除最近阅读功能 - 新增编辑文章内容功能 - 优化多个弹窗显示 - 优化书源导入逻辑 - 优化订阅同步逻辑 ## v3.2.5 ### Features - 新增simple-web书架排序 - 优化文件读写锁 - 开启连读优化后缓存后三段TTS ### Bug Fixes - 修改simple-web书架搜索bug ## v3.2.4 ### Features - 修改服务器版本脚本 - 去掉gc,优化文件读写锁 - 测试 iOS 朗读 - 修改接口请求默认超时时间为30秒 ## v3.2.3 ### Features - 新增直接添加书签 - 新增simple-web页面设置 - 添加定时gc逻辑 - 优化simple-web页面 - 新增服务器端脚本 ### Bug Fixes - 修改后端链接状态bug ## v3.2.2 ### Features - 优化自动阅读 - 优化书海,书源管理样式 - 优化simple-web兼容性 - 新增PC端设置提示 ### Bug Fixes - 修复windows环境漫画路径问题 - 修复往前翻页bug ## v3.2.1 ### Features - 新增kindle7天试用申请 - 优化epub阅读位置记忆跳转 ### Bug Fixes - 尝试修复PC端启动jvm参数bug ## v3.2.0 ### Features - 新增视频源支持 - epub支持朗读和自动阅读,优化阅读界面 - 新增simple-web搜索及RSS页面,优化simple-web页面样式 - 修改epub导入兼容性 - 新增书源管理搜索功能 - 修改桌面端jvm启动参数 - 新增书源订阅管理 - 优化探索样式 - 优化书源调试页面 ### Bug Fixes - 修复epub书名包含+号时注入js失败问题 - 修复simple-web分页组件 - 修复remote-webview不能访问https问题 - 修改封面上传后弹窗无法关闭的bug - 修改上下滚动模式目录跳转bug ## v3.1.1 ### Features - 新增在线TTS朗读(Edge大声朗读) ### Bug Fixes - 修复朗读bug ## v3.1.0 ### Features - 新增下载数据备份功能,新增自动备份功能。通过 --reader.app.autoBackupUserData=true 启用,每天23:50开始会自动备份用户数据到webdav目录 - 延长kindle试用期至 2023-06-30,过期以后需要购买授权来使用kindle页面 - 新增书籍设置pdf图片宽度选项 - 新增朗读时跳过全标点段落,跳过段末标点符号 - 新增更新错误分组排除不追更书籍 ### Bug Fixes - 修复用户管理界面排序bug ## v3.0.5 ### Features - 去掉书籍上限启动参数 - 延长kindle试用期 ## v3.0.4 ### Features - 新增封面代理设置 - 优化文件并发读写加锁逻辑 - 优化多源搜索 - 延长kindle试用期 ## v3.0.3 ### Features - 新增章节请求超时设置 - 新增simple—web搜索书架功能 - 桌面端新增jvm配置 - 尝试修复音频音量bug,尝试优化pwa - 页面优化 - 延长kindle试用期 ### Bug Fixes - 修复simple-web端bug ## v3.0.2 ### Features - 支持pdf格式 - 页面优化 - 延长kindle试用期15天 ### Bug Fixes - 修复用户默认书源bug - simple-web兼容kindle ## v3.0.1 ### Features - 新增书架布局设置,新增分列布局 - 修改书籍分组的字段类型,支持更多分组 - 新增用户更多设置项 - 优化simple-web分页逻辑 - 优化simple-web兼容性 - 优化音频音量设置 ### Bug Fixes - 修复simple-web渲染器bug - 修复删除书籍未刷新书架bug ## v3.0.0 ### Features - 新增epub iframe模式自定义字体支持 - 新增simple-web端,支持kindle使用(限时免费) - 新增授权管理,多用户版用户上限降低至 15(已超出的无法再注册,但可以继续使用) - 新增用户管理、书籍管理、书签管理等分页排序过滤功能 - 新增书源请求头设置 - 新增书籍批量缓存操作 - 新增contextPath设置项 - 新增书架搜索作者及分类 - 优化书籍信息页面 - 优化阅读界面功能按钮 ### Bug Fixes - 修复音频播放bug - 修复调试书源跳转链接bug ## v2.7.4 ### Features - 新增一键导入本地书籍功能 - 添加新增替换规则入口,优化替换逻辑 - 新增更新错误内置分组 - 优化日志跟踪 - 优化音频时长获取逻辑 ### Bug Fixes - 修复Windows环境webdav路径判断bug - 尝试修复音频时长获取bug - 修复书架刷新并发bug - 修复书籍封面接口bug ## v2.7.3 ### Features - 新增书仓搜索及解析书籍功能 - 修改语音库选择样式 - 优化书源分组 - 优化协程逻辑,拆分解析库和控制层库 - 新增原文阅读模式,优化缓存判断逻辑 ### Bug Fixes - 尝试修复自动切换主题bug ## v2.7.2 ### Features - 新增Epub解析模式, 支持简繁切换、左右翻页 - 新增Epub iframe 模式左右翻页功能 - 修改日志配置,仅保留7天 - 新增书仓文件管理筛选功能 - 修改简繁切换库,样式优化,感谢 [@terry3041](https://github.com/hectorqin/reader/pull/227) ### Bug Fixes - 修复桌面端bug ## v2.7.1 ### Bug Fixes - 修复书仓下载bug - 修复书籍追更选项设置bug ## v2.7.0 ### Features - 新增远程webview镜像,支持使用 `hectorqin/remote-webview` 镜像作为远程 `webview`,使用 --reader.app.remoteWebviewApi="http://0.0.0.0:8050" 启用。 - 新增书源`cookie`,`cache`功能支持 - 新增上下左右边距设置 ## v2.6.4 ### Features - 新增远程 `webview` 支持,目前仅支持 `scrapinghub/splash` 镜像作为远程 `webview`,使用 --reader.app.remoteWebviewApi="http://0.0.0.0:8050" 启用。 - 优化听书逻辑 ### Bug Fixes - 修复清理用户bug ## v2.6.3 ### Features - 新增清理不活跃用户功能,使用 --reader.app.autoClearInactiveUser=31 (不活跃天数) 启用 - 优化书架更新逻辑 - 新增书架更新间隔设置选项,使用 --reader.app.shelfUpdateInteval=10 (更新间隔分钟,必须是10的倍数) 启用 ### Bug Fixes - 修复加入书架bug - 修改CI - 修复配置方案失效bug --- ## v2.6.2 ### Features - 添加用户并发修改锁 ### Bug Fixes - 修改CI --- ## v2.6.1 ### Bug Fixes - 修改CI --- ## v2.6.0 ### Bug Fixes - 修复本地书籍路径问题 --- ## v2.5.8 ### Features - 测试CI --- ## v2.5.7 ### Features - 临时书籍使用临时缓存 - 新增支持mongodb存放数据 --- ## v2.5.6 ### Bug Fixes - 修复书架路径bug --- ## v2.5.5 ### Features - 统一文件管理 - 优化书籍内容使用缓存图片 ### Bug Fixes - 修复书架绝对路径bug --- ## v2.5.4 ### Features - 新增分组排序功能 - 新增朗读定时功能 - 新增音频音量调整功能 ### Bug Fixes - 修复书架更新并发bug - 修复window环境问题 - 修复书源调试bug - 修复音频bug --- ## v2.5.3 ### Features - 新增 webdav 书仓功能,新增修改目录规则功能,优化本地书籍换源功能 --- ## v2.5.2 ### Bug Fixes - 修复jdk8编译依赖 --- ## v2.5.1 ### Features - 新增自定义字体功能,优化阅读设置功能,新增书签同步功能 --- ## v2.5.0 ### Features - 新增全文搜索功能,本地书籍生成封面优化,epub设置增强 - 新增书签功能 --- ## v2.4.1 ### Features - 完善注册登录,新增删除用户书源和恢复默认书源功能,测试CI - 新增本地书仓功能,新增自定义书籍封面功能,新增用户上限和用户书籍上限 - 更新阅读内核 - 新增清空书源功能,新增自动缓存下一章 - 优化本地导入逻辑 - 完成替换规则改版逻辑,新增配置方案设置功能,修复bug,优化页面 - 新增上下滚动翻页模式,优化页面 - 新增简繁转换功能,新增替换规则导入功能,新增设置默认书源功能,新增像素滚动自动翻页,兼容ie - 新增书源调试功能,优化书海功能,优化缓存功能 - 新增支持CBZ书籍,新增支持卷名,优化调试功能 - 新增书籍管理功能,新增缓存及导出功能,优化书海功能 ### Bug Fixes - 页面优化 - bug修复 --- ## v2.0.3 ### Features - 更新阅读解析库 - 优化多源搜索和书源搜索功能 - 新增服务器缓存章节内容功能,优化阅读宽度设置 - 迁移缓存到indexdb - 新增简洁模式 ### Bug Fixes - 修复书源分组搜索选择bug - 页面优化 - bug修复 --- ## v1.9.4 ### Features - 新增缓存管理,优化缓存逻辑 - 新增页面模式设置 - 新增自定义主题模式设置 - 新增远程书源导入功能 - 新增读取epub封面,优化导入逻辑,优化书源错误标记 - 新增书源导出功能 - 新增按分组搜索书源功能 - 新增刷新章节内容功能 - 新增翻页动画时长设置 ### Bug Fixes - 修复iPad兼容问题 - 修复精确搜索bug,优化json序列化 - 页面优化 - bug修复 --- ## V1.8.0 ### Features - 新增点击翻页和选择文字过滤关闭选项 - 支持设置代理(待测试) - 修复旧版本自动迁移bug - 修复搜索bug - 完善失败源标记和恢复逻辑 - 优化ios pwa样式 - 优化书籍标签显示 ### Bug Fixes - 页面优化 - bug修复 ================================================ FILE: build.gradle.kts ================================================ import org.openjfx.gradle.* import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.lang.reflect.* import io.github.fvarrui.javapackager.model.Platform import io.github.fvarrui.javapackager.model.WindowsConfig import de.undercouch.gradle.tasks.download.Download buildscript { val kotlin_version: String by extra{"1.5.21"} // extra["kotlin_version"] = "1.5.21" repositories { mavenLocal() mavenCentral() } dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") classpath("io.github.fvarrui:javapackager:1.6.5") } } plugins { id("org.springframework.boot") version "2.1.6.RELEASE" id("java") id("application") id("org.openjfx.javafxplugin") version "0.0.9" id("org.jetbrains.kotlin.plugin.spring") version "1.3.61" } configure { version = "11.0.2" modules = listOf("javafx.web") // Set JAVAFX_PLATFORM to "linux", "win", or "mac" val javafxPlatformOverride = System.getenv("JAVAFX_PLATFORM") if (javafxPlatformOverride != null) { val javafxPlatform: JavaFXPlatform = JavaFXPlatform.values() .firstOrNull { it.classifier == javafxPlatformOverride } ?: throw IllegalArgumentException("JAVAFX_PLATFORM $javafxPlatformOverride not in list:" + " ${JavaFXPlatform.values().map { it.classifier }}") logger.info("Overriding JavaFX platform to {}", javafxPlatform) // Override the private platform field val platformField: Field = JavaFXOptions::class.java.getDeclaredField("platform") platformField.isAccessible = true platformField.set(this, javafxPlatform) // Invoke the private updateJavaFXDependencies() method val updateDeps: Method = JavaFXOptions::class.java.getDeclaredMethod("updateJavaFXDependencies") updateDeps.isAccessible = true updateDeps.invoke(this) } } apply(plugin = "io.spring.dependency-management") apply(plugin = "kotlin") apply(plugin = "io.github.fvarrui.javapackager.plugin") group = "com.htmake" version = "2.5.4" java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } repositories { mavenCentral() maven("https://jitpack.io") maven("https://gitlab.com/api/v4/projects/26729549/packages/maven") google() jcenter() } val compileOnly by configurations.getting { extendsFrom(configurations["annotationProcessor"]) } dependencies { val kotlin_version: String by extra{"1.5.21"} // val kotlin_version: String by extra implementation("org.springframework.boot:spring-boot-starter") testImplementation("org.springframework.boot:spring-boot-starter-test") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") // vertx implementation("io.vertx:vertx-core:3.8.1") implementation("io.vertx:vertx-lang-kotlin:3.8.1") implementation("io.vertx:vertx-lang-kotlin-coroutines:3.8.1") implementation("io.vertx:vertx-web:3.8.1") implementation("io.vertx:vertx-web-client:3.8.1") // json implementation("com.google.code.gson:gson:2.8.5") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.+") // log implementation("io.github.microutils:kotlin-logging:1.6.24") implementation("uk.org.lidalia:sysout-over-slf4j:1.0.2") implementation("com.google.guava:guava:28.0-jre") // 网络 implementation("com.squareup.okhttp3:okhttp:4.9.1") implementation("com.squareup.okhttp3:logging-interceptor:4.1.0") // Retrofit implementation("com.squareup.retrofit2:retrofit:2.6.1") implementation("com.julienviet:retrofit-vertx:1.1.3") //JS rhino // implementation("com.github.gedoor:rhino-android:1.6") implementation(fileTree("src/lib").include("rhino-*.jar")) // 规则相关 implementation("org.jsoup:jsoup:1.14.1") implementation("cn.wanghaomiao:JsoupXpath:2.5.0") implementation("com.jayway.jsonpath:json-path:2.6.0") // xml // 弃用 xmlpull-1.1.4.0,因为它需要 Java9 // implementation("org.xmlpull:xmlpull:1.1.4.0") implementation(fileTree("src/lib").include("xmlpull-*.jar")) // implementation("com.github.stefanhaustein:kxml2:2.5.0") //加解密类库 implementation("cn.hutool:hutool-crypto:5.8.0.M1") // 转换繁体 // implementation("com.github.liuyueyi.quick-chinese-transfer:quick-transfer-core:0.2.1") } // val compileKotlin: KotlinCompile by tasks // val compileTestKotlin: KotlinCompile by tasks // compileKotlin.kotlinOptions { // jvmTarget = "1.8" // } // compileTestKotlin.kotlinOptions { // jvmTarget = "1.8" // } tasks.withType { kotlinOptions.jvmTarget = "1.8" } application { // Define the main class for the application mainClassName = "com.htmake.reader.ReaderUIApplicationKt" } tasks.create("buildReader"){ dependsOn("build") // mandatory mainClass = "com.htmake.reader.ReaderUIApplicationKt" // optional setBundleJre(false) vmArgs = arrayListOf("-Dreader.app.showUI=true", "-Dspring.profiles.active=prod", "-Dreader.app.packaged=true", "-Dreader.app.debug=true") } tasks.create("packageReaderMac") { dependsOn("build") // mandatory mainClass = "com.htmake.reader.ReaderUIApplicationKt" // optional setBundleJre(false) // bundleJre = false // setCreateZipball(true) platform = Platform.mac vmArgs = arrayListOf("-Dreader.app.showUI=true", "-Dspring.profiles.active=prod", "-Dreader.app.packaged=true", "-Dreader.app.debug=false", "-Dlogging.path=\$HOME/.reader/logs") } tasks.create("packageReaderWin") { dependsOn("build") // mandatory mainClass = "com.htmake.reader.ReaderUIApplicationKt" // optional setBundleJre(false) // bundleJre = true // jrePath = File(buildDir, "win64-jre") setCreateZipball(true) platform = Platform.windows vmArgs = arrayListOf("-Dreader.app.showUI=true", "-Dspring.profiles.active=prod", "-Dreader.app.debug=false") withGroovyBuilder { "winConfig" { "setWrapJar"(false) } } // winConfig { // wrapJar = false // } } tasks.create("packageReaderLinux") { dependsOn("build") // mandatory mainClass = "com.htmake.reader.ReaderUIApplicationKt" // optional setBundleJre(false) // bundleJre = false setCreateZipball(true) platform = Platform.linux vmArgs = arrayListOf("-Dreader.app.showUI=true", "-Dspring.profiles.active=prod", "-Dreader.app.debug=false") withGroovyBuilder { "linuxConfig" { "setWrapJar"(false) } } } tasks { val downloadWinJre by registering(Download::class) { src("https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.8%2B10/OpenJDK11U-jre_x64_windows_hotspot_11.0.8_10.zip") dest(File(buildDir, "win64-jre.zip")) onlyIfModified(true) } } tasks.register("unpackWinJre") { dependsOn("downloadWinJre") from(zipTree("$buildDir/win64-jre.zip")) { include("jdk*/**") eachFile { relativePath = RelativePath(true, *relativePath.segments.drop(1).toTypedArray()) } includeEmptyDirs = false } into(File(buildDir, "win64-jre")) } // javafx { // version = "11.0.2" // modules = [ 'javafx.web' ] // } ================================================ FILE: build.sh ================================================ #!/bin/bash oldJAVAHome=$JAVA_HOME task=$1 version="" checkJava() { if [ -d /Library/Java/JavaVirtualMachines/openjdk-11.jdk/Contents/Home ]; then export JAVA_HOME=/Library/Java/JavaVirtualMachines/openjdk-11.jdk/Contents/Home fi javaVersion=$(java -version 2>&1 | sed -n ';s/.* version "\(.*\)\.\(.*\)\..*".*/\1\2/p;') if [[ "$javaVersion" -lt "110" ]]; then echo "Java version must not lower than 11.0" exit 1 fi } getVersion() { version=$(grep -Eo "^version = .*" $1 | grep -Eo "['\"].*['\"]" | tr -d "'\"") } getVersion ./build.gradle.kts case $task in build) checkJava # 调试打包 ./gradlew buildReader ;; run) checkJava # 运行 javafx UI port=$2 if [[ -z "$port" ]]; then port=8080 fi ./gradlew assemble --info if test $? -eq 0; then shift shift java -jar build/libs/reader-$version.jar --reader.app.showUI=true --reader.server.port=$port $@ fi ;; win) checkJava # 打包 windows 安装包 JAVAFX_PLATFORM=win ./gradlew packageReaderWin ;; linux) checkJava # 打包 linux 安装包 JAVAFX_PLATFORM=linux ./gradlew packageReaderLinux ;; mac) checkJava # 打包 mac 安装包 JAVAFX_PLATFORM=mac ./gradlew packageReaderMac ;; serve) # 服务端一键运行 port=$2 if [[ -z "$port" ]]; then port=8080 fi mv src/main/java/com/htmake/reader/ReaderUIApplication.kt src/main/java/com/htmake/reader/ReaderUIApplication.kt.back getVersion ./cli.gradle ./gradlew -b cli.gradle assemble --info if test $? -eq 0; then shift shift mv src/main/java/com/htmake/reader/ReaderUIApplication.kt.back src/main/java/com/htmake/reader/ReaderUIApplication.kt java -jar build/libs/reader-$version.jar --reader.server.port=$port $@ else mv src/main/java/com/htmake/reader/ReaderUIApplication.kt.back src/main/java/com/htmake/reader/ReaderUIApplication.kt fi ;; cli) # 服务端打包命令 shift export JAVA_HOME=$oldJAVAHome mv src/main/java/com/htmake/reader/ReaderUIApplication.kt src/main/java/com/htmake/reader/ReaderUIApplication.kt.back getVersion ./cli.gradle ./gradlew -b cli.gradle $@ mv src/main/java/com/htmake/reader/ReaderUIApplication.kt.back src/main/java/com/htmake/reader/ReaderUIApplication.kt ;; yarn) # yarn 快捷命令,默认 install shift cd web yarn $@ ;; web) # 开发web页面 cd web yarn serve ;; sync) # 编译同步web资源 cd web yarn sync ;; *) echo " USAGE: ./build.sh build|run|win|linux|mac|serve|cli|yarn|web|sync build 调试打包 run 桌面端编译运行,需要先执行 sync 命令编译同步web资源 win 打包 windows 安装包 linux 打包 linux 安装包 mac 打包 mac 安装包 serve 服务端编译运行 cli 服务端打包命令 yarn web页面 yarn 快捷命令,默认 install web 开发web页面 sync 编译同步web资源 " ;; esac export JAVA_HOME=$oldJAVAHome ================================================ FILE: cli.gradle ================================================ buildscript { ext.kotlin_version = '1.5.21' repositories { mavenCentral() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } plugins { id 'org.springframework.boot' version '2.1.6.RELEASE' id 'java' id "org.jetbrains.kotlin.plugin.spring" version "1.3.61" } apply plugin: 'io.spring.dependency-management' apply plugin: 'kotlin' group = 'com.htmake' version = '2.5.4' sourceCompatibility = '1.8' repositories { mavenCentral() maven { url "https://jitpack.io" } maven { url "https://gitlab.com/api/v4/projects/26729549/packages/maven" } google() jcenter() } configurations { compileOnly { extendsFrom annotationProcessor } } dependencies { implementation 'org.springframework.boot:spring-boot-starter' testImplementation 'org.springframework.boot:spring-boot-starter-test' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" // vertx implementation "io.vertx:vertx-core:3.8.1" implementation "io.vertx:vertx-lang-kotlin:3.8.1" implementation "io.vertx:vertx-lang-kotlin-coroutines:3.8.1" implementation 'io.vertx:vertx-web:3.8.1' implementation 'io.vertx:vertx-web-client:3.8.1' // json implementation "com.google.code.gson:gson:2.8.5" implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.13.+" // log implementation "io.github.microutils:kotlin-logging:1.6.24" implementation "uk.org.lidalia:sysout-over-slf4j:1.0.2" implementation "com.google.guava:guava:28.0-jre" // 网络 implementation "com.squareup.okhttp3:okhttp:4.9.1" implementation "com.squareup.okhttp3:logging-interceptor:4.1.0" // Retrofit implementation "com.squareup.retrofit2:retrofit:2.6.1" implementation "com.julienviet:retrofit-vertx:1.1.3" //JS rhino // implementation "com.github.gedoor:rhino-android:1.6" implementation(fileTree(dir: 'src/lib', include: ['rhino-*.jar'])) // 规则相关 implementation "org.jsoup:jsoup:1.14.1" implementation "cn.wanghaomiao:JsoupXpath:2.5.0" implementation "com.jayway.jsonpath:json-path:2.6.0" // xml // 弃用 xmlpull-1.1.4.0,因为它需要 Java9 // implementation "org.xmlpull:xmlpull:1.1.4.0" implementation(fileTree(dir: 'src/lib', include: ['xmlpull-*.jar'])) // implementation "com.github.stefanhaustein:kxml2:2.4.2" //加解密类库 implementation "cn.hutool:hutool-crypto:5.8.0.M1" // 转换繁体 // implementation "com.github.liuyueyi.quick-chinese-transfer:quick-transfer-core:0.2.1" } compileKotlin { kotlinOptions { jvmTarget = "1.8" } } compileTestKotlin { kotlinOptions { jvmTarget = "1.8" } } ================================================ FILE: doc.md ================================================ # 文档 - [文档](#文档) - [免责声明(Disclaimer)](#免责声明disclaimer) - [数据存储](#数据存储) - [本地书仓](#本地书仓) - [阅读页面地址](#阅读页面地址) - [全功能web端](#全功能web端) - [适配kindle的 `simple-web`](#适配kindle的-simple-web) - [自定义阅读主题](#自定义阅读主题) - [自定义样式](#自定义样式) - [接口服务配置](#接口服务配置) - [WebDAV同步配置](#webdav同步配置) - [客户端](#客户端) - [Windows / MacOS / Linux](#windows--macos--linux) - [手机端](#手机端) - [服务器版](#服务器版) - [Docker版](#docker版) - [Docker-Compose版(推荐)](#docker-compose版推荐) - [通过脚本一键部署](#通过脚本一键部署) - [Arch Linux 安装](#Arch-Linux-安装) - [配置文件](#配置文件) - [Nginx反向代理(如果有域名可以考虑80端口复用)](#nginx反向代理如果有域名可以考虑80端口复用) - [开发编译](#开发编译) - [编译脚本](#编译脚本) - [编译前端](#编译前端) - [编译接口](#编译接口) - [接口文档](#接口文档) - [新增接口](#新增接口) - [加入书架](#加入书架) - [获取书籍书源](#获取书籍书源) - [搜索书籍更多书源](#搜索书籍更多书源) - [书籍换源](#书籍换源) ## 免责声明(Disclaimer) 阅读是一款提供网络文学搜索的工具,为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。 当您搜索一本书的时,阅读会将该书的书名以关键词的形式提交到各个第三方网络文学网站。各第三方网站返回的内容与阅读无关,阅读对其概不负责,亦不承担任何法律责任。任何通过使用阅读而链接到的第三方网页均系他人制作或提供,您可能从第三方网页上获得其他服务,阅读对其合法性概不负责,亦不承担任何法律责任。第三方搜索引擎结果根据您提交的书名自动搜索获得并提供试读,不代表阅读赞成或被搜索链接到的第三方网页上的内容或立场。您应该对使用搜索引擎的结果自行承担风险。 阅读不做任何形式的保证:不保证第三方搜索引擎的搜索结果满足您的要求,不保证搜索服务不中断,不保证搜索结果的安全性、正确性、及时性、合法性。因网络状况、通讯线路、第三方网站等任何原因而导致您不能正常使用阅读,阅读不承担任何法律责任。阅读尊重并保护所有使用阅读用户的个人隐私权,您注册的用户名、电子邮件地址等个人资料,非经您亲自许可或根据相关法律、法规的强制性规定,阅读不会主动地泄露给第三方。 阅读致力于最大程度地减少网络文学阅读者在自行搜寻过程中的无意义的时间浪费,通过专业搜索展示不同网站中网络文学的最新章节。阅读在为广大小说爱好者提供方便、快捷舒适的试读体验的同时,也使优秀网络文学得以迅速、更广泛的传播,从而达到了在一定程度促进网络文学充分繁荣发展之目的。阅读鼓励广大小说爱好者通过阅读发现优秀网络小说及其提供商,并建议阅读正版图书。任何单位或个人认为通过阅读搜索链接到的第三方网页内容可能涉嫌侵犯其信息网络传播权,应该及时向阅读提出书面权力通知,并提供身份证明、权属证明及详细侵权情况证明。阅读在收到上述法律文件后,将会依法尽快断开相关链接内容。 ## 数据存储 接口服务使用文件存储书源及目录等信息,存储位置为 storage 目录(可通过运行时添加 `-Dreader.app.storagePath=/path/to/storage` 修改)。 > MacOS客户端的存储目录是 `~/.reader/storage`,Window和Linux客户端为 `运行目录/storage` 数据存储目录结构如下: ```bash storage ├── assets # 静态资源 │ ├── hector # 用户 hector 的资源目录 │ | |── covers # 本地 epub 书籍的封面图片目录 │ │ ├── background # 自定义阅读背景图片保存目录 │ │ │ └── 6.jpg │ └── reader.css # 自定义CSS样式文件 ├── cache # 缓存目录 │ ├── 6190ac40068e74c2c82624e91a5f8a0c.jpg # 书籍封面缓存 │ ├── bookInfoCache # 书籍搜索缓存 ACache 目录 │ └── ea11967236129bdae6133c3c9ff8c2dd.jpg ├── data # 数据目录 │ ├── default # 系统默认用户的数据目录 (reader.app.secure为false时) │ │ ├── bookSource.json # 书源列表 │ │ ├── bookshelf.json # 书架书籍列表 │ │ ├── 斗罗大陆_唐家三少 # 书籍缓存目录 │ │ │ ├── 5d01bc88d6b19ebbe974acaac1675811 # A书源章节缓存目录 │ │ │ ├── 5d01bc88d6b19ebbe974acaac1675811.json # A书源目录列表 │ │ │ ├── 7e5ca1cc2a1ea2e09fdec4ee2e150f02 # B书源章节缓存目录 │ │ │ ├── 7e5ca1cc2a1ea2e09fdec4ee2e150f02.json # B书源目录列表 │ │ │ └── bookSource.json # 书籍书源列表 │ ├── hector # 用户 hector 的数据目录 (reader.app.secure为true时的用户目录) │ │ ├── bookSource.json # 书源列表 │ │ ├── bookshelf.json # 书架书籍列表 │ │ ├── webdav # webdav 存储目录 可能会存在 legado 子目录 │ │ │ ├── backup2021-09-15.zip # 阅读3备份文件 │ │ │ └── bookProgress # 阅读3书籍进度备份目录 │ │ │ └── 斗罗大陆_唐家三少.json # 阅读3书籍进度 │ │ └── 斗罗大陆_唐家三少 # 书籍缓存目录 │ │ |── 2d44d0ec2397b6c1d4010b97d914031e # A书源章节缓存目录 │ │ └── 2d44d0ec2397b6c1d4010b97d914031e.json # A书源目录列表 │ └── users.json # 用户列表 ├── localStore # 本地书仓,所有用户共享(用户需要开启书仓权限,才能访问) │ |── 斗破苍穹.txt # 本地书仓书籍 │ └── 斗罗大陆.txt # 本地书仓书籍 └── windowConfig.json # 窗口配置文件 ``` ### 本地书仓 在 `storage/localStore` 中可以集中存放管理本地书籍,开启访问权限的用户可以在 `页面-浏览书仓` 中选择批量导入到自己的书架进行阅读。 ## 阅读页面地址 ### 全功能web端 `http://ip:端口/` ### 适配kindle的 `simple-web` `http://ip:端口/simple-web` > 注意,加入TG群了解详情 ## 自定义阅读主题 书架页面仅支持白天模式和黑夜模式。 阅读页面支持设置多款主题,还可以自定义主题。自定义阅读主题包括: - 自定义页面背景颜色 - 自定义浮窗背景颜色 - 自定义阅读背景颜色 - 自定义阅读背景图片 ## 自定义样式 页面还会加载应用目录下的 `reader-assets/reader.css` 这个CSS样式文件,在这个文件中可以自定义页面样式。 > 自定义样式可能需要配合 `!important` 来设定属性 ## 接口服务配置 ```yml reader: app: workDir: "" # 工作目录 secure: false # 是否需要登录鉴权,开启后将支持多用户模式 inviteCode: "" # 注册邀请码,为空时则开放注册,否则注册时需要输入邀请码 secureKey: "" # 管理密码,开启鉴权时,前端管理用户空间的管理密码 cacheChapterContent: false # 是否缓存章节内容 debugLog: false # 是否打开调试日志 autoClearInactiveUser: 0 # 是否自动清理不活跃用户,为0不清理,大于0为清理超过 autoClearInactiveUser 天未登录的用户 mongoUri: "" # mongodb uri 用于备份数据 mongoDbName: "reader" # mongodb 数据库名称 shelfUpdateInteval: 10 # 书架自动更新间隔时间,单位分钟,必须是10的倍数 userLimit: 15 # 用户上限,最大 15 remoteWebviewApi: "" # remote-webview 地址 defaultUserEnableWebdav: true # 新用户是否默认启用webdav defaultUserEnableLocalStore: true # 新用户是否默认启用localStore defaultUserEnableBookSource: true # 新用户是否默认可编辑书源,如果为false,则只能使用默认书源,不能新增/修改/删除 defaultUserEnableRssSource: true # 新用户是否默认可编辑RSS源 defaultUserBookSourceLimit: 100 # 新用户默认书源上限 defaultUserBookLimit: 200 # 新用户默认书籍上限 autoBackupUserData: false # 是否自动备份用户数据 minUserPasswordLength: 8 # 用户密码最小长度 remoteBookSourceUpdateInterval: 720 # 远程书源定时更新间隔时间,单位分钟,必须是10的倍数 server: port: 8080 # 监听端口 contextPath: "" # 二级目录,为空则不使用二级目录 webUrl: http://localhost:${reader.server.port} # web链接 ``` ## WebDAV同步配置 1. 首先需要在阅读App里面配置 `WebDAV备份` 服务器地址: `http://IP:端口/reader3/webdav/` 如果开启了 `reader.app.secure` 选项,那么使用网页注册的用户名和密码登录,否则使用用户名 `default` 和 密码 `123456` 登录 2. 然后在阅读App里面点击备份 3. 在网页里面查看WebDAV文件,确认是否备份成功 4. 备份成功之后 - 服务器会自动同步书籍阅读进度(暂不支持章节内阅读位置,也不会自动同步书架信息变更) - 可以直接选择阅读App的备份文件进行恢复,这样会直接覆盖书源和书架信息 - 可以备份当前书源和书架信息到WebDAV,但是必须要先备份成功 - 需要通过恢复备份文件来同步书籍和书源信息 5. PS: 本地书源的书籍同步后无法打开,除非换源 ## 客户端 ### Windows / MacOS / Linux 从 [releases](https://github.com/hectorqin/reader/releases) 下载对应平台安装包安装即可,需要安装java8及以上环境 MacOS 版 `storage` 默认是 `用户目录/.reader/storage`,其它版本 `storage` 默认是 `程序目录/storage` #### 配置文件 `storage/windowConfig.json` 包含图形界面和接口服务的相关配置,JSON格式,修改后,程序重启才会生效 > 请仔细检查配置内容,不支持注释,此处注释只是为了方便理解 ```json { "serverPort": 8080, // web服务端口,默认为 8080 "showUI": true, // 是否显示UI界面,默认为显示 "debug": false, // 是否调试模式,默认为否 "positionX": 0.0, // 窗口位置 横坐标 "positionY": 0.0, // 窗口位置 纵坐标 "width": 1280.0, // 窗口大小 宽度 "height": 800.0, // 窗口大小 高度 "rememberSize": true, // 改变窗口大小时,是否记住窗口大小,默认记住 "rememberPosition": false, // 移动窗口时,是否记住窗口位置,默认不记住 "setWindowPosition": false, // 启动时是否设置窗口位置,默认不设置,窗口默认居中 "setWindowSize": true, // 启动时是否设置窗口大小,默认按照配置文件进行设置 "serverConfig": { // 接口服务配置,此处配置会被 `serverPort|showUI|debug` 等覆盖 "reader.app.secure": false, // 是否需要登录鉴权,开启后将支持多用户模式 "reader.app.inviteCode": "", // 注册邀请码,为空时则开放注册,否则注册时需要输入邀请码。仅多用户模式下有效 "reader.app.secureKey": "", // 管理密码,开启鉴权时,前端管理用户空间的管理密码。仅多用户模式下有效 } } ``` ### 手机端 使用docker版本或者服务器版本,访问web页面 可以添加为桌面应用 ### 服务器版 从 [releases](https://github.com/hectorqin/reader/releases) 下载 `reader-server-$version.zip` 解压后运行即可,需要安装java8及以上环境 ```bash # 安装jdk10以上环境... # 解压文件 unzip reader-server-$version.zip # 运行 cd reader-server-$version ./bin/startup.sh # windows 上直接点击 bin/startup.cmd 文件 # startup 脚本支持以下选项,这些选项如果使用命令行参数修改,则会覆盖配置文件的设置 # -m single|multi 选择单用户/多用户模式,默认 以配置文件 conf/application.properties 为准 # -s reader-xx 选择 jar 文件名(不含.jar后缀),默认使用target目录里最新的jar # -i inviteCode 设置多用户模式下的邀请码,默认 以配置文件 conf/application.properties 为准 # -k secureKey 设置多用户模式下的管理密码,默认 以配置文件 conf/application.properties 为准 # 注意!!!startup 脚本在单用户模式下 默认占用 256m 内存,在多用户模式下 默认占用 1g 内存,如果内存不够,请自行修改脚本 # web端 http://localhost:8080/ # 接口地址 http://localhost:8080/reader3/ ``` ### Docker版 ```bash # 自行编译 # docker build -t reader:latest . # 使用环境变量覆盖服务配置,环境变量采用大写字母,不允许使用.-符号,采用下划线“_”取代点“.” 减号“-”直接删除 # docker run -d --restart=always --name=reader -e "SPRING_PROFILES_ACTIVE=prod" -v $(pwd)/logs:/logs -v $(pwd)/storage:/storage -p 8080:8080 reader:latest # 跨平台镜像 # 新建构建器 # docker buildx create --use --name mybuilder # 启动构建器 # docker buildx inspect mybuilder --bootstrap # 查看构建器及其所支持的cpu架构 # docker buildx ls # 构建跨平台镜像 # docker buildx build -t reader:latest --platform=linux/arm,linux/arm64,linux/amd64 . --push # 使用预编译的镜像 # 自用版(建议修改映射端口) docker run -d --restart=always --name=reader -e "SPRING_PROFILES_ACTIVE=prod" -v $(pwd)/logs:/logs -v $(pwd)/storage:/storage -p 8080:8080 hectorqin/reader # 多用户版(建议修改映射端口) docker run -d --restart=always --name=reader -v $(pwd)/logs:/logs -v $(pwd)/storage:/storage -p 8080:8080 hectorqin/reader java -jar /app/bin/reader.jar --spring.profiles.active=prod --reader.app.secure=true --reader.app.secureKey=管理密码 --reader.app.inviteCode=注册邀请码 # 多用户版 使用环境变量(建议修改映射端口) docker run -d --restart=always --name=reader -e "SPRING_PROFILES_ACTIVE=prod" -e "READER_APP_SECURE=true" -e "READER_APP_SECUREKEY=管理密码" -e "READER_APP_INVITECODE=注册邀请码" -v $(pwd)/logs:/logs -v $(pwd)/storage:/storage -p 8080:8080 hectorqin/reader # 更新docker镜像 # docker pull hectorqin/reader #:后面的端口修改为映射端口 # web端 http://localhost:8080/ # 接口地址 http://localhost:8080/reader3/ # 通过watchtower手动更新 docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --cleanup --run-once reader # 使用 remote-webview 功能 # 1.创建 remote-webview 容器 docker run -d --network host --restart=always hectorqin/remote-webview # 2.重建 reader 容器 reader使用宿主机网络:--network host reader添加环境变量:-e "READER_APP_REMOTEWEBVIEWAPI=http://localhost:8050" 获取reader添加参数:--reader.app.remoteWebviewApi=http://localhost:8050" ``` ### Docker-Compose版(推荐) ```shell #腾讯云,阿里云,华为云,甲骨文等服务器提供商需在控制台面板手动关闭防火墙并放行端口 #安装docker 及 docker-compose #Debian/Ubuntu apt install docker-compose -y #CentOS curl -fsSL https://get.docker.com | bash -s docker #国外服务器 curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun #国内服务器 # 下载项目里的 docker-compose.yaml wget https://mirror.ghproxy.com/https://raw.githubusercontent.com/hectorqin/reader/master/docker-compose.yaml # 根据 docker-compose.yaml 里面的注释编辑所需配置 vim docker-compose.yaml # 保存 esc :wq # 启动 docker-compose docker-compose up -d # 停止 docker-compose docker-compose stop # 查看实时日志 docker logs -f reader # 自行导入远程书源(打开链接后复制网址导入即可) https://legado.aoaostar.com/ # 手动更新 docker-compose pull && docker-compose up -d ``` ### 通过脚本一键部署 ```shell # 此脚本对甲骨文非Ubuntu系统,CentOS9可能不兼容。建议网上手动搜索 #curl bash <(curl -L -s https://mirror.ghproxy.com/https://raw.githubusercontent.com/hectorqin/reader/master/reader.sh) #wget bash <(wget -qO- --no-check-certificate https://mirror.ghproxy.com/https://raw.githubusercontent.com/hectorqin/reader/master/reader.sh) ``` ### Arch Linux 安装 > 注意,此软件源并非官方提供,后果自负 从 [AUR 仓库](https://aur.archlinux.org/packages/reader-pro-bin)安装或[自建软件源](https://github.com/taotieren/aur-repo) ```bash yay -Syu reader-pro # 开启开机自启 sudo systemctl enable reader-pro-single sudo systemctl enable reader-pro-multi # 运行 sudo systemctl start reader-pro-single sudo systemctl start reader-pro-multi # 状态 sudo systemctl status reader-pro-single sudo systemctl status reader-pro-multi # 停止 sudo systemctl stop reader-pro-single sudo systemctl stop reader-pro-multi # 停止开机自启 sudo systemctl disable reader-pro-single sudo systemctl disable reader-pro-multi ``` > Arch Linux 的存储目录是 `/var/lib/reader-pro/` > Arch Linux 的配置文件是 `/usr/share/java/reader-pro/conf/application.properties` ## Nginx反向代理(如果有域名可以考虑80端口复用) ```shell # 宝塔等各种面板不适用下列教程 # Debian/Ubuntu apt install nginx -y # CentOS yum install nginx -y vim /etc/nginx/conf.d/reader.conf 将下面代码复制进reader.conf后,修改域名输入 esc :wq 保持即可 ``` ```nginx server { listen 80; server_name 域名; #开启ssl解除注释 # SSL证书获取 # https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E #listen 443 ssl; #ssl_certificate 证书.cer; #ssl_certificate_key 证书.key; #ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; #ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; #ssl_prefer_server_ciphers on; #ssl_session_cache shared:SSL:10m; #ssl_session_timeout 10m; #if ($server_port !~ 443){ # rewrite ^(/.*)$ https://$host$1 permanent; #} #error_page 497 https://$host$request_uri; gzip on; #开启gzip压缩 gzip_min_length 1k; #设置对数据启用压缩的最少字节数 gzip_buffers 4 16k; gzip_http_version 1.0; gzip_comp_level 6; #设置数据的压缩等级,等级为1-9,压缩比从小到大 gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml; #设置需要压缩的数据格式 gzip_vary on; client_max_body_size 50m; #允许上传50MB文件,上传本地书籍需要修改此项大小.如nginx主配置文件已添加,删除此行并修改主配置即可 location / { proxy_pass http://127.0.0.1:4396; #端口自行修改为映射端口 proxy_http_version 1.1; proxy_cache_bypass $http_upgrade; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; } } ``` ## 开发编译 ### 编译脚本 ```bash $ ./build.sh USAGE: ./build.sh build|run|win|linux|mac|serve|cli|yarn|web|sync build 调试打包 run 桌面端编译运行,需要先执行 sync 命令编译同步web资源 win 打包 windows 安装包 linux 打包 linux 安装包 mac 打包 mac 安装包 serve 服务端编译运行 cli 服务端打包命令 yarn web页面 yarn 快捷命令,默认 install web 开发web页面 sync 编译同步web资源 ``` ### 编译前端 ```bash cd web # 启动开发服务 访问 http://localhost:8081/ # yarn serve # 编译,并拷贝到 src/main/resources/web 目录 yarn sync ``` ### 编译接口 ```bash ./gradlew assemble --info java -jar build/libs/reader-${version}.jar # 指定 storage 路径 默认为相对路径 storage # java -Dreader.app.storagePath=cacheStorage -jar build/libs/reader-${version}.jar # web端 http://localhost:8080/ # 接口地址 http://localhost:8080/reader3/ ``` ## 接口文档 与 [阅读3Web接口](https://github.com/gedoor/legado/blob/master/api.md) 基本一致,只是多了接口前缀 `/reader3/` ### 新增接口 #### 加入书架 - URL `http://localhost:8080/reader3/saveBook` - Method `POST` - Body `json 格式` ```JSON { "infoHtml": "", "tocHtml": "", "bookUrl": "https://www.damixs.com/book/dmfz.html", "origin": "https://www.damixs.com", "originName": "🎉大米小说", "type": 0, "name": "道门法则", "author": "八宝饭", "kind": "02-14", "intro": "在道门掌控的天下,应该怎么修炼?符箓、丹药、道士、灵妖、斋醮科仪......想要修仙,很好,请从扫厕所开始做起!符诏到来的时候,你需要站在什么位置?Q群:1701556(需验证订阅截图)、954782460“盟主群”", "wordCount": "", "latestChapterTitle": "番外四(贺消脱止-M荣升盟主)", "tocUrl": "", "time": 1628756214810, "originOrder": 16 } ``` - Response Body [Book字段参考](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt) ```JSON { "isSuccess": true, "errorMsg": "", "data": Book } ``` #### 获取书籍书源 - URL `http://localhost:8080/reader3/getBookSource?url=xxx` - Method `GET` 获取指定URL对应的书源信息, 和 `阅读3Web接口` 的 `getSource` 接口相同 - Response Body [SearchBook字段参考](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/SearchBook.kt) ```JSON { "isSuccess": true, "errorMsg": "", "data": [SearchBook] } ``` #### 搜索书籍更多书源 - URL `http://localhost:8080/reader3/searchBookSource?name=xxx&lastIndex=0` - Method `GET` 搜索指定name对应的书源列表信息 lastIndex 是上次搜索结果中返回的字段,默认为 0,可以传入 `getBookSource` 接口返回的SearchBook列表长度 - Response Body [SearchBook字段参考](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/SearchBook.kt) ```JSON { "isSuccess": true, "errorMsg": "", "data": [SearchBook] } ``` #### 书籍换源 - URL `http://localhost:8080/reader3/setBookSource` - Method `POST` - Body `json 格式` ```JSON { "newUrl": "新源书籍链接", "name": "书籍名称", "bookSourceUrl": "书源链接" } ``` - Response Body [Book字段参考](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt) ```JSON { "isSuccess": true, "errorMsg": "", "data": Book } ``` ================================================ FILE: docker-compose.yaml ================================================ version: '3.1' services: # reader 在线阅读 # 公开服务器(服务器位于日本):[https://reader.nxnow.top](https://reader.nxnow.top) 测试账号/密码分别为guest/guest123,也可自行创建账号添加书源,不定期删除长期未登录账号(2周) # 书源集合 : [https://legado.aoaostar.com/](https://legado.aoaostar.com/) 点击打开连接,添加远程书源即可 # 公众号汇总 : [https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MzMyMDgyMA==&action=getalbum&album_id=2397535253763801090#wechat_redirect](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MzMyMDgyMA==&action=getalbum&album_id=2397535253763801090#wechat_redirect) # 手动更新方式 : docker-compose pull && docker-compose up -d reader: image: hectorqin/reader #image: hectorqin/reader:openj9-latest #docker镜像,arm64架构或小内存机器优先使用此镜像.启用需删除上一行 container_name: reader #容器名 可自行修改 restart: always ports: - 4396:8080 #4396端口映射可自行修改 networks: - share_net volumes: - /home/reader/logs:/logs #log映射目录 /home/reader/logs 映射目录可自行修改 - /home/reader/storage:/storage #数据映射目录 /home/reader/storage 映射目录可自行修改 environment: - SPRING_PROFILES_ACTIVE=prod - READER_APP_USERLIMIT=50 #用户上限,默认50 - READER_APP_USERBOOKLIMIT=200 #用户书籍上限,默认200 - READER_APP_CACHECHAPTERCONTENT=true #开启缓存章节内容 V2.0 # 如果启用远程webview,需要取消注释下面的 remote-webview 服务 # - READER_APP_REMOTEWEBVIEWAPI=http://remote-webview:8050 #开启远程webview # 下面都是多用户模式配置 - READER_APP_SECURE=true #开启登录鉴权,开启后将支持多用户模式 - READER_APP_SECUREKEY=adminpwd #管理员密码 建议修改 - READER_APP_INVITECODE=registercode #注册邀请码 建议修改,如不需要可注释或删除 # remote-webview: # image: hectorqin/remote-webview # container_name: remote-webview #容器名 可自行修改 # restart: always # ports: # - 8050:8050 # networks: # - share_net # 自动更新docker镜像 watchtower: image: containrrr/watchtower container_name: watchtower restart: always # 环境变量,设置为上海时区 environment: - TZ=Asia/Shanghai volumes: - /var/run/docker.sock:/var/run/docker.sock command: reader watchtower --cleanup --schedule "0 0 4 * * *" networks: - share_net # 仅更新reader与watchtower容器,如需其他自行添加 '容器名' ,如:reader watchtower nginx # --cleanup 更新后清理旧版本镜像 # --schedule 自动检测更新 crontab定时(限定6位crontab) 此处代表凌晨4点整 networks: share_net: driver: bridge ================================================ FILE: docker-compose.yml ================================================ version: '3.1' services: # reader 在线阅读 # 公开服务器(服务器位于日本):[https://reader.nxnow.top](https://reader.nxnow.top) 测试账号/密码分别为guest/guest123,也可自行创建账号添加书源,不定期删除长期未登录账号(2周) # 书源集合 : [https://legado.aoaostar.com/](https://legado.aoaostar.com/) 点击打开连接,添加远程书源即可 # 公众号汇总 : [https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MzMyMDgyMA==&action=getalbum&album_id=2397535253763801090#wechat_redirect](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MzMyMDgyMA==&action=getalbum&album_id=2397535253763801090#wechat_redirect) # 手动更新方式 : docker-compose pull && docker-compose up -d reader: #image: hectorqin/reader # 普通镜像 image: hectorqin/reader:openj9-latest # Openj9镜像,arm64架构或小内存机器优先使用 container_name: reader #容器名 可自行修改 restart: always ports: - 4396:8080 #4396端口映射可自行修改,8080请勿修改 volumes: - /home/reader/logs:/logs #log映射目录 /home/reader/logs 映射目录可自行修改 - /home/reader/storage:/storage #数据映射目录 /home/reader/storage 映射目录可自行修改 environment: - SPRING_PROFILES_ACTIVE=prod #- READER_APP_USERLIMIT=50 #用户上限,默认且最大值为50 - READER_APP_USERBOOKLIMIT=200 #用户书籍上限,默认200 - READER_APP_CACHECHAPTERCONTENT=true #开启缓存章节内容 - READER_APP_REMOTEWEBVIEWAPI=http://readerwebview:8050 #启用webview(若下方readerwebview容器不开启需注释此行 # ↓多用户模式配置↓ - READER_APP_SECURE=true #开启登录鉴权,开启后将支持多用户模式 - READER_APP_SECUREKEY=adminpwd #管理员密码 建议修改 - READER_APP_INVITECODE=registercode #注册邀请码 建议修改,如不需要可注释或删除 # 如需支持webview书源,打开(占用较大,不需要可加 # 注释) readerwebview: image: hectorqin/remote-webview container_name: readerwebview restart: always environment: - TZ=Asia/Shanghai # 自动更新docker镜像 watchtower: image: containrrr/watchtower container_name: watchtower restart: always environment: - TZ=Asia/Shanghai volumes: - /var/run/docker.sock:/var/run/docker.sock command: reader readerwebview watchtower --cleanup --schedule "0 0 4 * * *" # 仅更新reader与watchtower容器,如需其他自行添加 '容器名' ,如:reader watchtower nginx # --cleanup 更新后清理旧版本镜像 # --schedule 自动检测更新 crontab定时(限定6位crontab) 此处代表凌晨4点整 volumes: reader: readerwebview: ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ org.gradle.jvmargs=-Xmx2048m ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$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"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # 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 ;; 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" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @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 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" @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 init 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 init 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 :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :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 %CMD_LINE_ARGS% :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 ================================================ FILE: nixpacks.toml ================================================ [phases.build] cmds = ['railway run'] [start] runImage = 'hectorqin/reader' onlyIncludeFiles = ['Dockerfile'] cmd = '/sbin/tini --' ================================================ FILE: preview.md ================================================ # 预览 ![](imgs/1.jpg) ![](imgs/2.jpg) ![](imgs/3.jpg) ![](imgs/4.jpg) ![](imgs/5.jpg) ![](imgs/6.jpg) ![](imgs/7.jpg) ![](imgs/8.jpg) ![](imgs/9.jpg) ![](imgs/10.jpg) ================================================ FILE: reader.sh ================================================ #!/bin/bash red='\033[0;31m' green="\033[32m" yellow='\033[0;33m' plain='\033[0m' file_dir="" remotePort="" isMultiUser="" adminPassword="" registerCode="" strTrue="true" dockerImages="" # CheckRoot if [[ $EUID -ne 0 ]]; then echo "请使用root用户登录!" 1>&2 exit 1 fi # CheckSystem if [[ -f /etc/redhat-release ]]; then release="centos" elif cat /etc/issue | grep -q -E -i "debian"; then release="debian" elif cat /etc/issue | grep -q -E -i "ubuntu"; then release="ubuntu" elif cat /etc/issue | grep -q -E -i "centos|red hat|redhat"; then release="centos" elif cat /proc/version | grep -q -E -i "debian"; then release="debian" elif cat /proc/version | grep -q -E -i "ubuntu"; then release="ubuntu" elif cat /proc/version | grep -q -E -i "centos|red hat|redhat"; then release="centos" fi bit=$(uname -m) if test "$bit" != "x86_64"; then bit="arm64" else bit="amd64" fi os_version="" # os version if [[ -f /etc/os-release ]]; then os_version=$(awk -F'[= ."]' '/VERSION_ID/{print $3}' /etc/os-release) fi if [[ -z "$os_version" && -f /etc/lsb-release ]]; then os_version=$(awk -F'[= ."]+' '/DISTRIB_RELEASE/{print $2}' /etc/lsb-release) fi if [[ x"${release}" == x"centos" ]]; then if [[ ${os_version} -le 6 ]]; then echo -e "${red}请使用 CentOS 7 或更高版本的系统!${plain}\n" && exit 1 fi elif [[ x"${release}" == x"ubuntu" ]]; then if [[ ${os_version} -lt 16 ]]; then echo -e "${red}请使用 Ubuntu 16 或更高版本的系统!${plain}\n" && exit 1 fi elif [[ x"${release}" == x"debian" ]]; then if [[ ${os_version} -lt 9 ]]; then echo -e "${red}请使用 Debian 9 或更高版本的系统!${plain}\n" && exit 1 fi fi install_dockercompose() { if [[ x"${release}" == x"centos" ]]; then yum install wget curl -y echo -e "${green} 正在移除CentOS遗留无效Docker文件 ${plain}" yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine -y echo -e "${green} 正在安装Docker ${plain}" yum install yum-utils device-mapper-persistent-data lvm2 -y yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo yum install docker-ce docker-ce-cli containerd.io docker-compose-plugin -y echo -e "${green} 正在启动Docker ${plain}" systemctl start docker systemctl restart docker systemctl enable docker echo -e "${green} 正在安装docker-compose ${plain}" curl -L "https://mirror.ghproxy.com/https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose && chmod +x /usr/local/bin/docker-compose else echo -e "${green} 正在安装docker-compose ${plain}" apt update && apt install wget curl docker-compose -y fi } install_reader() { mkdir -p ${orgin_file_dir}/storage/data/default cd ${orgin_file_dir} rm docker-compose* wget https://mirror.ghproxy.com/https://raw.githubusercontent.com/hectorqin/reader/master/docker-compose.yml echo -e "${green} 正在配置默认书源 ${plain}" wget https://jihulab.com/aoaostar/legado/-/raw/release/cache/6c35d84798ddbf4aad3fe3f0fd6cec53dd788be8.json -O storage/data/default/bookSource.json # 判断是否合法json local first_character=$(head -c 1 "storage/data/default/bookSource.json") if [[ x"$first_character" == x"[" ]]; then # echo "" else echo -e "${red} 书源错误,已为您删除,请自行导入书源 ${plain}" echo "[]" > storage/data/default/bookSource.json fi echo -e "${green} 正在配置docker变量 ${plain}" sed -i "s/\/home\/reader/${file_dir}/" docker-compose.yml sed -i "s/4396/${remotePort}/" docker-compose.yml sed -i "s/openj9-latest/${dockerImages}/" docker-compose.yml # 多用户 sed -i "s/READER_APP_SECURE\=true/READER_APP_SECURE\=${isMultiUser}/" docker-compose.yml sed -i "s/adminpwd/${adminPassword}/" docker-compose.yml sed -i "s/registercode/${registerCode}/" docker-compose.yml echo -e "${green} 准备启动 ${plain}" # 远程webview docker-compose up -d } getRemotePort () { echo "请输入部署端口,例如 4396" read -p "不填默认为4396: " remotePort if [[ -z "$remotePort" ]];then remotePort="4396" fi if [ "$remotePort" -gt 0 ] 2>/dev/null;then if [[ $remotePort -lt 0 || $remotePort -gt 65535 ]];then echo -e "${red} 端口号不正确,请输入0-65535${plain}" getRemotePort exit 0 fi else echo -e "${red} 端口号不正确,请输入0-65535${plain}" getRemotePort exit 0 fi } getfileDir () { echo -e "${green} 请输入数据存放目录,例如 /home/reader : ${plain}" read -p "不填默认为/home/reader : " file_dir if [[ -z "$file_dir" ]];then file_dir="/home/reader" fi orgin_file_dir=$file_dir file_dir=${file_dir//\//\\\/} } getMultiUser () { echo -e "${green} 是否需要开启多用户 : ${plain}" read -p "填0不开启,不填开启 : " isMultiUser if [[ -z "$isMultiUser" ]];then isMultiUser="true" else isMultiUser="false" fi } getPwdOrCode () { echo -e "${green} 请输入管理密码,用于加载用户空间 : ${plain}" read -p "建议修改此参数,默认为adminpwd : " adminPassword if [[ -z "$adminPassword" ]];then adminPassword="adminpwd" fi echo -e "${green} 请输入邀请码,用于注册使用 : ${plain}" read -p "不填默认为空 : " registerCode if [[ -z "$registerCode" ]];then registerCode="" fi } getDockerImages () { echo -e "${green} 请输入需要的镜像 arm或者小内存(1G)机器建议openj9,其余建议基础镜像 : ${plain}" read -p "不输入为基础镜像,输入其他值为openj9 : " dockerImages if [[ -z "$dockerImages" ]];then dockerImages="latest" else dockerImages="openj9-latest" fi } Server_IP='' Public_IP='' getIpaddr () { Server_IP=$(hostname -I | awk -F " " '{printf $1}') Public_IP=$(curl http://pv.sohu.com/cityjson 2>> /dev/null | awk -F '"' '{print $4}') } echo -e "${green}准备部署reader${plain}" echo -e "${green}甲骨文官方系统可能并不适用此脚本,本脚本仅测试CentOS7,8,Ubuntu20+,Debian10+${plain}" install_dockercompose getfileDir getRemotePort getMultiUser if [ $isMultiUser == "true" ]; then getPwdOrCode fi getDockerImages install_reader getIpaddr echo -e "${green}初步部署完成,已配置默认书源,国内服务器等有控制台面板的服务器厂商请手动在控制台打开reader所需的端口${remotePort}${plain}" if [ $Server_IP == $Public_IP ];then echo -e "${green}网址:${plain} http://${Server_IP}:${remotePort}" else echo -e "${green}内网网址:${plain} http://${Server_IP}:${remotePort}" echo -e "${green}公网网址:${plain} http://${Public_IP}:${remotePort}" fi echo -e "${green}如需修改其他配置请前往 cd${orgin_file_dir} 根据注释修改 vim docker-compose.yml文件后${plain}" echo -e "${green}先自行学习vim用法,否则建议使用sftp或WindTerm等ssh自带sftp的软件直接打开编辑${plain}" echo -e "${green}修改后前往 cd${orgin_file_dir} 后通过命令docker-compose up -d 重启即可${plain}" ================================================ FILE: server/bin/shutdown.cmd ================================================ @echo off rem Copyright 1999-2018 Alibaba Group Holding Ltd. 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 http://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. if not exist "%JAVA_HOME%\bin\java.exe" ( rem find java_home from reg for /f "tokens=2*" %%i in ('reg query "HKLM\SOFTWARE\JavaSoft\Java Runtime Environment" /s ^| findstr "JavaHome"') do ( set "JAVA_HOME=%%j" ) ) if exist "%JAVA_HOME%\bin\java.exe" ( set "JAVA=%JAVA_HOME%\bin\java.exe" ) else ( echo Please set the JAVA_HOME variable in your environment, We need jdk8 or later! pause EXIT /B 1 ) setlocal set "PATH=%JAVA_HOME%\bin;%PATH%" echo killing reader server for /f "tokens=1" %%i in ('jps -m ^| find "reader.server"') do ( taskkill /F /PID %%i ) echo Done! ================================================ FILE: server/bin/shutdown.sh ================================================ #!/bin/bash # Copyright 1999-2018 Alibaba Group Holding Ltd. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. cd `dirname $0`/../target target_dir=`pwd` pid=`ps ax | grep -i 'reader.server' | grep ${target_dir} | grep java | grep -v grep | awk '{print $1}'` if [ -z "$pid" ] ; then echo "No reader running." exit -1; fi echo "The reader(${pid}) is running..." kill ${pid} echo "Send shutdown request to reader(${pid}) OK" ================================================ FILE: server/bin/startup.cmd ================================================ @echo off REM Copyright 1999-2018 Alibaba Group Holding Ltd. 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 http://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. if not exist "%JAVA_HOME%\bin\java.exe" ( rem find java_home from reg for /f "tokens=2*" %%i in ('reg query "HKLM\SOFTWARE\JavaSoft\Java Runtime Environment" /s ^| findstr "JavaHome"') do ( set "JAVA_HOME=%%j" ) ) if exist "%JAVA_HOME%\bin\java.exe" ( set "JAVA=%JAVA_HOME%\bin\java.exe" ) else ( rem check java command for /f "usebackq delims=" %%i in (`where java`) do ( set JAVA=%%i ) if not "%JAVA%" == "" if exist "%JAVA%" ( rem java path is exist ) else ( echo Please set the JAVA_HOME variable in your environment, We need jdk8 or later! pause EXIT /B 1 ) ) setlocal enabledelayedexpansion set BASE_DIR=%~dp0 rem added double quotation marks to avoid the issue caused by the folder names containing spaces. rem removed the last 5 chars(which means \bin\) to get the base DIR. set BASE_DIR="%BASE_DIR:~0,-5%" set CUSTOM_SEARCH_LOCATIONS=file:%BASE_DIR%/conf/ set SERVER=reader for /f "delims=" %%i in ('dir /b /o:-n %BASE_DIR%\target\reader*.jar') do set NEWEST_JAR=%%i if not "%NEWEST_JAR%"=="" ( set SERVER=%NEWEST_JAR:.jar=% ) set MODE="" set INVITE_CODE="" set SECURE_KEY="" set MODE_INDEX=-1 set INVITE_CODE_INDEX=-1 set SERVER_INDEX=-1 set SECURE_KEY_INDEX=-1 set EMBEDDED_STORAGE="" set i=0 for %%a in (%*) do ( if "%%a" == "-m" ( set /a MODE_INDEX=!i!+1 ) if "%%a" == "-i" ( set /a INVITE_CODE_INDEX=!i!+1 ) if "%%a" == "-s" ( set /a SERVER_INDEX=!i!+1 ) if "%%a" == "-k" ( set /a SECURE_KEY_INDEX=!i!+1 ) set /a i+=1 ) set i=0 for %%a in (%*) do ( if %MODE_INDEX% == !i! ( set MODE="%%a" ) if %INVITE_CODE_INDEX% == !i! ( set INVITE_CODE="%%a" ) if %SERVER_INDEX% == !i! (set SERVER="%%a") if %SECURE_KEY_INDEX% == !i! (set SECURE_KEY="%%a") set /a i+=1 ) rem if reader startup mode is single if %MODE% == "" ( echo The running mode of the Reader is determined by the configuration file conf/application.properties. Please note that there is currently no memory limit set for the JVM. ) if %MODE% == "single" ( echo The running mode of the Reader is determined by the configuration file conf/application.properties. Please note that the current memory limit is set to 256m. set "READER_JVM_OPTS=-Xms256m -Xmx256m -Xmn128m" ) rem if reader startup mode is multi-user if not %MODE% == "" if not %MODE% == "single" ( set READER_TIPS="" set "READER_OPTS=-Dreader.app.secure=true" if not "%INVITE_CODE%" == "" { set "READER_OPTS=%READER_OPTS% -Dreader.app.inviteCode=%INVITE_CODE%" set "READER_TIPS=%READER_TIPS% inviteCode: %INVITE_CODE%" } if not "%SECURE_KEY%" == "" { set "READER_OPTS=%READER_OPTS% -Dreader.app.secureKey=%SECURE_KEY%" set "READER_TIPS=%READER_TIPS% secureKey: %SECURE_KEY%" } if "%READER_TIPS%" == "" { set "READER_TIPS=The invitation code and administrator password are determined by the configuration file conf\application.properties." } set "READER_TIPS=%READER_TIPS%. Please note that the current memory limit is set to 1g." echo The Reader will running in multi-user mode. %READER_TIPS% set "READER_JVM_OPTS=-server -Xms1g -Xmx1g -Xmn512m -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=160m -XX:-OmitStackTraceInFastThrow -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=%BASE_DIR%\logs\java_heapdump.hprof -XX:-UseLargePages" ) rem set reader options @REM set "READER_OPTS=%READER_OPTS% -Dloader.path=%BASE_DIR%/plugins,%BASE_DIR%/plugins/health,%BASE_DIR%/plugins/cmdb,%BASE_DIR%/plugins/selector" set "READER_OPTS=%READER_OPTS% -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 -Dspring.profiles.active=prod -Dreader.app.workDir=%BASE_DIR%" set "READER_OPTS=%READER_OPTS% -jar %BASE_DIR%\target\%SERVER%.jar" rem set reader spring config location set "READER_CONFIG_OPTS=--spring.config.additional-location=%CUSTOM_SEARCH_LOCATIONS%" rem set reader log4j file location @REM set "READER_LOG4J_OPTS=--logging.config=%BASE_DIR%/conf/reader-logback.xml" set COMMAND="%JAVA%" %READER_JVM_OPTS% %READER_OPTS% %READER_CONFIG_OPTS% %READER_LOG4J_OPTS% reader.server %* echo Run command: echo %COMMAND% echo echo Reader is starting, you can check the %BASE_DIR%\logs rem start reader command %COMMAND% ================================================ FILE: server/bin/startup.sh ================================================ #!/bin/bash # Copyright 1999-2018 Alibaba Group Holding Ltd. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. cygwin=false darwin=false os400=false case "`uname`" in CYGWIN*) cygwin=true;; Darwin*) darwin=true;; OS400*) os400=true;; esac error_exit () { echo "ERROR: $1 !!" exit 1 } [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/java [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/opt/taobao/java [ ! -e "$JAVA_HOME/bin/java" ] && unset JAVA_HOME if [ -z "$JAVA_HOME" ]; then if $darwin; then if [ -x '/usr/libexec/java_home' ] ; then export JAVA_HOME=`/usr/libexec/java_home` elif [ -d "/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home" ]; then export JAVA_HOME="/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home" fi else JAVA_PATH=`dirname $(readlink -f $(which javac))` if [ "x$JAVA_PATH" != "x" ]; then export JAVA_HOME=`dirname $JAVA_PATH 2>/dev/null` fi fi if [ -z "$JAVA_HOME" ]; then error_exit "请设置 JAVA_HOME 环境变量,需要jdk8及以上的java环境!" fi fi export BASE_DIR=`cd $(dirname $0)/..; pwd` SERVER="reader" NEWEST_JAR=$(ls $BASE_DIR/target | grep -Eo 'reader.*\.jar' | sort -nr | head -1) if [ -n "$NEWEST_JAR" ]; then SERVER=${NEWEST_JAR/.jar/} fi MODE="" INVITE_CODE="" SECURE_KEY="" while getopts ":m:s:i:k:" opt do case $opt in m) MODE=$OPTARG;; s) SERVER=$OPTARG;; i) INVITE_CODE=$OPTARG;; k) SECURE_KEY=$OPTARG;; ?) echo "未知的参数: $opt" exit 1;; esac done export JAVA_HOME export JAVA="$JAVA_HOME/bin/java" export CUSTOM_SEARCH_LOCATIONS=file:${BASE_DIR}/conf/ #=========================================================================================== # JVM Configuration #=========================================================================================== if [[ "${MODE}" == "" ]]; then echo "Reader 的运行模式以配置文件 conf/application.properties 为准。注意,当前未限制jvm内存" elif [[ "${MODE}" == "single" ]]; then JAVA_OPT="${JAVA_OPT} -Xms256m -Xmx256m -Xmn128m" JAVA_OPT="${JAVA_OPT} -Dreader.app.secure=false" echo "Reader 将以单用户模式运行。注意,当前内存限制为256m" else JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn512m -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=160m" JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${BASE_DIR}/logs/java_heapdump.hprof" JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages" JAVA_OPT="${JAVA_OPT} -Dreader.app.secure=true" TIPS="" if [[ "${INVITE_CODE}" != "" ]]; then JAVA_OPT="${JAVA_OPT} -Dreader.app.inviteCode=${INVITE_CODE}" TIPS="${TIPS} 邀请码:${INVITE_CODE}" fi if [[ "${SECURE_KEY}" != "" ]]; then JAVA_OPT="${JAVA_OPT} -Dreader.app.secureKey=${SECURE_KEY}" TIPS="${TIPS} 管理员密码:${SECURE_KEY}" fi if [[ "${TIPS}" == "" ]]; then TIPS="邀请码和管理员密码以配置文件 conf/application.properties 为准" fi TIPS="${TIPS}。注意,当前内存限制为1g" echo "Reader 将以多用户模式运行。${TIPS}" fi JAVA_MAJOR_VERSION=$($JAVA -version 2>&1 | sed -E -n 's/.* version "([0-9]*).*$/\1/p') if [[ "$JAVA_MAJOR_VERSION" -ge "9" ]] ; then JAVA_OPT="${JAVA_OPT} -Xlog:gc*:file=${BASE_DIR}/logs/reader_gc.log:time,tags:filecount=10,filesize=100m" else JAVA_OPT_EXT_FIX="-Djava.ext.dirs=${JAVA_HOME}/jre/lib/ext:${JAVA_HOME}/lib/ext" JAVA_OPT="${JAVA_OPT} -Xloggc:${BASE_DIR}/logs/reader_gc.log -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M" fi # JAVA_OPT="${JAVA_OPT} -Dloader.path=${BASE_DIR}/plugins,${BASE_DIR}/plugins/health,${BASE_DIR}/plugins/cmdb,${BASE_DIR}/plugins/selector" JAVA_OPT="${JAVA_OPT} -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 -Dreader.app.workDir=${BASE_DIR}" JAVA_OPT="${JAVA_OPT} -jar ${BASE_DIR}/target/${SERVER}.jar" JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}" JAVA_OPT="${JAVA_OPT} --spring.config.additional-location=${CUSTOM_SEARCH_LOCATIONS}" # JAVA_OPT="${JAVA_OPT} --logging.config=${BASE_DIR}/conf/nacos-logback.xml" JAVA_OPT="${JAVA_OPT} --server.max-http-header-size=524288" if [ ! -d "${BASE_DIR}/logs" ]; then mkdir ${BASE_DIR}/logs fi echo "启动命令:" echo "$JAVA $JAVA_OPT_EXT_FIX ${JAVA_OPT}" echo # check the start.out log output file if [ ! -f "${BASE_DIR}/logs/start.out" ]; then touch "${BASE_DIR}/logs/start.out" else mv ${BASE_DIR}/logs/start.out ${BASE_DIR}/logs/start-$(date +'%Y-%m-%d_%H_%M').out fi # start echo "$JAVA $JAVA_OPT_EXT_FIX ${JAVA_OPT}" > ${BASE_DIR}/logs/start.out 2>&1 & if [[ "$JAVA_OPT_EXT_FIX" == "" ]]; then nohup "$JAVA" ${JAVA_OPT} reader.server >> ${BASE_DIR}/logs/start.out 2>&1 & else nohup "$JAVA" "$JAVA_OPT_EXT_FIX" ${JAVA_OPT} reader.server >> ${BASE_DIR}/logs/start.out 2>&1 & fi echo "Reader 正在启动中,你可以在 ${BASE_DIR}/logs/start.out 查看日志" ================================================ FILE: server/conf/application.properties ================================================ # 是否多用户模式,如果启动 startup 脚本时使用了 -m 1 选择多用户模式,-m single 运行单用户模式,否则根据此处的参数选择模式 reader.app.secure=true # 邀请码,如果启动 startup 脚本时使用了参数 -i 邀请码,则会覆盖此处 reader.app.inviteCode= # 管理密码,如果启动 startup 脚本时使用了参数 -k 管理密码,则会覆盖此处 reader.app.secureKey= # 书源代理可通过 header 设置 # 是否缓存章节内容 reader.app.cacheChapterContent=true # 用户上限,免费版用户上限最大15 reader.app.userLimit=15 # 是否开启书源调试日志 reader.app.debugLog=false # 自动清理不活跃用户,单位天,0为不清理,大于0的数字为清理多少天未登录用户 reader.app.autoClearInactiveUser=0 # mongodb数据备份,mongodb链接地址 reader.app.mongoUri= # mongodb数据备份,mongodb数据库名 reader.app.mongoDbName=reader # 书架自动更新间隔,单位分钟 reader.app.shelfUpdateInteval=10 # 远程webview接口地址,可通过部署 hectorqin/remote-webview 来设置,http://IP:8050 reader.app.remoteWebviewApi= # 新用户默认是否启用 webdav reader.app.defaultUserEnableWebdav=true # 新用户是否默认启用 本地书仓 reader.app.defaultUserEnableLocalStore=true # 新用户是否默认启用 书源编辑 reader.app.defaultUserEnableBookSource=true # 新用户是否默认启用 RSS源编辑 reader.app.defaultUserEnableRssSource=true # 新用户是否默认书源数量上限 reader.app.defaultUserBookSourceLimit=100 # 新用户是否默认书籍数量上限 reader.app.defaultUserBookLimit=200 # 是否自动备份用户数据 reader.app.autoBackupUserData=false # reader服务监听端口 reader.server.port=8080 # reader接口目录 reader.server.contextPath= ================================================ FILE: settings.gradle ================================================ pluginManagement { repositories { gradlePluginPortal() } } rootProject.name = 'reader' ================================================ FILE: src/main/java/com/htmake/reader/ReaderApplication.kt ================================================ package com.htmake.reader import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.module.kotlin.registerKotlinModule import io.vertx.core.Future import io.vertx.core.Vertx import io.vertx.core.http.* import io.vertx.core.json.Json import io.vertx.ext.web.client.WebClient import io.vertx.ext.web.client.WebClientOptions import mu.KotlinLogging import com.htmake.reader.api.YueduApi import com.htmake.reader.verticle.RestVerticle import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.context.annotation.Bean import javax.annotation.PostConstruct private val logger = KotlinLogging.logger {} @SpringBootApplication @EnableScheduling class ReaderApplication { @Autowired private lateinit var yueduApi: YueduApi companion object { val vertx by lazy { Vertx.vertx() } fun vertx() = vertx } @PostConstruct fun deployVerticle() { Json.mapper.apply { registerKotlinModule() } Json.prettyMapper.apply { registerKotlinModule() } Json.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); vertx().deployVerticle(yueduApi) } @Bean fun webClient(): WebClient { val webClientOptions = WebClientOptions() webClientOptions.isTryUseCompression = true webClientOptions.logActivity = true webClientOptions.isFollowRedirects = true webClientOptions.isTrustAll = true val httpClient = vertx().createHttpClient(HttpClientOptions().setTrustAll(true)) // val webClient = WebClient.wrap(HttpClient(delegateHttpClient), webClientOptions) val webClient = WebClient.wrap(httpClient, webClientOptions) return webClient } } fun main(args: Array) { SpringApplication.run(ReaderApplication::class.java, *args) } ================================================ FILE: src/main/java/com/htmake/reader/ReaderUIApplication.kt ================================================ package com.htmake.reader import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.module.kotlin.registerKotlinModule import io.vertx.core.Future import io.vertx.core.Vertx import io.vertx.core.http.* import io.vertx.core.http.impl.HttpUtils import io.vertx.core.json.Json import io.vertx.ext.web.client.WebClient import io.vertx.ext.web.client.WebClientOptions import io.vertx.core.json.JsonObject import mu.KotlinLogging import com.htmake.reader.api.YueduApi import com.htmake.reader.entity.Size import com.htmake.reader.verticle.RestVerticle import com.htmake.reader.utils.SpringContextUtils import com.htmake.reader.SpringEvent import com.htmake.reader.utils.getStorage import com.htmake.reader.utils.saveStorage import com.htmake.reader.utils.asJsonObject import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent import org.springframework.boot.context.event.ApplicationReadyEvent import org.springframework.context.annotation.Bean import org.springframework.context.ConfigurableApplicationContext import org.springframework.context.ApplicationListener import org.springframework.context.ApplicationEvent import uk.org.lidalia.sysoutslf4j.context.SysOutOverSLF4J import javax.annotation.PostConstruct import javafx.application.Application import javafx.application.Platform import javafx.scene.Scene import javafx.scene.web.WebView import javafx.scene.web.WebErrorEvent import javafx.stage.Stage import javafx.stage.WindowEvent import javafx.stage.StageStyle import javafx.event.EventHandler import com.sun.javafx.application.LauncherImpl import com.sun.javafx.scene.text.FontHelper import javafx.scene.text.Font import javafx.scene.control.ProgressBar import javafx.scene.control.Dialog import javafx.scene.control.ButtonType import javafx.scene.image.ImageView import javafx.scene.layout.VBox import javafx.scene.paint.Color; import javafx.scene.image.Image; import javafx.beans.value.ChangeListener import javafx.beans.value.ObservableValue import javafx.concurrent.Worker import org.springframework.core.env.Environment import org.springframework.core.env.ConfigurableEnvironment import org.springframework.core.env.MapPropertySource import java.util.concurrent.CompletableFuture private val logger = KotlinLogging.logger {} private var launchArgs = arrayOf() class ReaderUIApplication: Application() { private lateinit var primaryStage: Stage; private lateinit var splashStage: Stage; lateinit var webUrl: String lateinit var env: ConfigurableEnvironment var windowConfigMap = mutableMapOf() var isSpringBootLaunched = false var springBootError = "" var showUI = false var defaultIcons = arrayOf(); fun boot() { launch(*launchArgs) } override fun init() { Thread() { var app = SpringApplication(ReaderApplication::class.java) var envListener = object: ApplicationListener { override fun onApplicationEvent(event: ApplicationEnvironmentPreparedEvent) { env = event.getEnvironment() // 加载 windowConfig var windowConfigSource = loadPropertySourceFromWindowConfig() env.getPropertySources().addFirst(windowConfigSource) // 获取应用相关配置 showUI = env.getProperty("reader.app.showUI", Boolean::class.java) ?: false logger.info("showUI: {}", showUI) var debug = env.getProperty("reader.app.debug", Boolean::class.java) logger.info("debug: {}", debug) var serverPort = env.getProperty("reader.server.port", Int::class.java) logger.info("serverPort: {}", serverPort) var port = 8080 if (serverPort != null && serverPort > 0) { port = serverPort; } webUrl = env.getProperty("reader.server.webUrl") ?: ("http://localhost:" + port) var sep = if(webUrl.contains("?")) { "&" } else { "?" } if (debug != null && debug) { webUrl = webUrl + sep + "debug=1&nopwa=1" } else { webUrl = webUrl + sep + "nopwa=1" } logger.info("webUrl: {}", webUrl) // System.setProperty("reader.system.fonts", Font.getFontNames().joinToString(separator = ",")) if (showUI && ::primaryStage.isInitialized){ Platform.runLater(object : Runnable { override fun run() { showSplashScreen() } }) } } } app.addListeners(envListener) var springListener = object: ApplicationListener { override fun onApplicationEvent(event: SpringEvent) { val eventType = event.getEvent() if (eventType == "READY") { isSpringBootLaunched = true if (showUI && ::primaryStage.isInitialized && ::webUrl.isInitialized){ Platform.runLater(object : Runnable { override fun run() { splashStage.hide() splashStage.setScene(null) showWebScreen(primaryStage, webUrl) } }) } } else if (eventType == "START_ERROR") { springBootError = event.getMessage() if (showUI){ Platform.runLater(object : Runnable { override fun run() { if (::splashStage.isInitialized) { splashStage.hide() splashStage.setScene(null) } showAlert(springBootError); stop(); } }) } else { logger.error(springBootError); stop(); } } } } app.addListeners(springListener) app.run(*launchArgs) }.start() } override fun start(stage: Stage) { try { logger.info("javafx start: {}", stage) primaryStage = stage if (showUI) { defaultIcons = arrayOf( Image(ReaderUIApplication::class.java.getResource("/icons/16x16.png").toExternalForm()), Image(ReaderUIApplication::class.java.getResource("/icons/24x24.png").toExternalForm()), Image(ReaderUIApplication::class.java.getResource("/icons/32x32.png").toExternalForm()), Image(ReaderUIApplication::class.java.getResource("/icons/48x48.png").toExternalForm()), Image(ReaderUIApplication::class.java.getResource("/icons/64x64.png").toExternalForm()), Image(ReaderUIApplication::class.java.getResource("/icons/128x128.png").toExternalForm()) ) if (isSpringBootLaunched) { showWebScreen(stage, webUrl) } else { if (springBootError.isNotEmpty()) { showAlert(springBootError) stop() } else { showSplashScreen() } } } } catch(e: Exception) { e.printStackTrace() } } fun showSplashScreen() { splashStage = Stage() var imageView = ImageView(ReaderUIApplication::class.java.getResource("/images/loading.gif").toExternalForm()); // var splashProgressBar = ProgressBar(); // splashProgressBar.setPrefWidth(imageView.getImage().getWidth()); // splashProgressBar.setPrefHeight(10.0); var vbox = VBox(); vbox.getChildren().addAll(imageView); // vbox.setStyle("-fx-background-color: transparent;" + // "-fx-padding: 0;" + // "-fx-border-style: solid inside;" + // "-fx-border-width: 1;" + // "-fx-border-insets: 0;" + // "-fx-border-radius: 0;" + // "-fx-border-color: #999;"); var splashScene = Scene(vbox, Color.TRANSPARENT); splashStage.setScene(splashScene); splashStage.getIcons().addAll(defaultIcons); splashStage.initStyle(StageStyle.TRANSPARENT); logger.info("showSplashScreen: {}", splashStage) splashStage.show() } fun showAlert(message: String, wait: Boolean = true) { var alert = Dialog(); alert.getDialogPane().setContentText(message); alert.getDialogPane().getButtonTypes().add(ButtonType.OK); if (wait) { alert.showAndWait(); } else { alert.show(); } } fun showConfirm(message: String): Boolean { var confirm = Dialog(); confirm.getDialogPane().setContentText(message); confirm.getDialogPane().getButtonTypes().addAll(ButtonType.YES, ButtonType.NO); val result = confirm.showAndWait().filter(ButtonType.YES::equals).isPresent(); return result } fun loadPropertySourceFromWindowConfig(): MapPropertySource { loadWindowConfig() var windowConfigPort = 0 var windowConfigSource = mutableMapOf() try { // 支持配置 接口服务 val serverConfig = windowConfigMap.getOrDefault("serverConfig", null) as? MutableMap if (serverConfig != null) { windowConfigSource = serverConfig } val serverPort = windowConfigMap.getOrDefault("serverPort", null) if (serverPort != null) { windowConfigPort = serverPort as Int if (windowConfigPort > 0) { windowConfigSource.put("reader.server.port", windowConfigPort) } } val showUI = windowConfigMap.getOrDefault("showUI", true) as Boolean? ?: true windowConfigSource.put("reader.app.showUI", showUI) val debug = windowConfigMap.getOrDefault("debug", null) if (debug != null) { windowConfigSource.put("reader.app.debug", debug as Boolean) } } catch(e: Exception) { e.printStackTrace() } logger.info("windowConfigSource: {}", windowConfigSource) return MapPropertySource("windowConfig", windowConfigSource) } fun loadWindowConfig() { val windowConfigObject = asJsonObject(getStorage("windowConfig")) if (windowConfigObject != null) { windowConfigMap = windowConfigObject.map } logger.info("windowConfigMap: {}", windowConfigMap) } fun getWindowConfigDoubleProperty(name: String, defaultVal: Double): Double { var value = windowConfigMap.getOrDefault(name, defaultVal) return when(value) { is Int -> value.toDouble() is Double -> value else -> defaultVal } } fun applyWindowConfig(stage: Stage): Size { var width = 1280.0; var height = 800.0; try { loadWindowConfig() val setWindowPosition = windowConfigMap.getOrDefault("setWindowPosition", false) as Boolean? ?: false if (setWindowPosition) { var positionX = getWindowConfigDoubleProperty("positionX", 0.0) var positionY = getWindowConfigDoubleProperty("positionY", 0.0) stage.setX(positionX) stage.setY(positionY) } val rememberSize = windowConfigMap.getOrDefault("rememberSize", true) as Boolean? ?: true val rememberPosition = windowConfigMap.getOrDefault("rememberPosition", false) as Boolean? ?: false if (rememberSize) { stage.widthProperty().addListener{_, _, w -> windowConfigMap.put("width", w) } // stage.heightProperty().addListener{_, _, h -> // windowConfigMap.put("height", h) // } stage.sceneProperty().addListener{_, _, s -> s.heightProperty().addListener{_, _, h -> windowConfigMap.put("height", h) } } } if (rememberPosition) { stage.xProperty().addListener{_, _, x -> windowConfigMap.put("positionX", x) } stage.yProperty().addListener{_, _, y -> windowConfigMap.put("positionY", y) } } val setWindowSize = windowConfigMap.getOrDefault("setWindowSize", true) as Boolean? ?: true if (setWindowSize) { width = getWindowConfigDoubleProperty("width", width) height = getWindowConfigDoubleProperty("height", height) } } catch(e: Exception) { showAlert("窗口配置加载失败,请检查窗口配置文件(windowConfig.json)", false) e.printStackTrace() } return Size(width, height) } fun showWebScreen(stage: Stage, url: String) { // 配置主窗口 var windowSize = applyWindowConfig(stage); System.setProperty("sun.net.http.allowRestrictedHeaders", "true") // logger.info("Font.getFontNames: {}", Font.getFontNames()) // logger.info("showWebScreen: {}", url) var webView = WebView(); var webEngine = webView.getEngine(); webEngine.setOnError{ event -> logger.info("error: {}", event) }; webEngine.setOnAlert{ event -> showAlert(event.data.toString()) }; webEngine.setConfirmHandler{ message -> showConfirm(message) }; var reloadCount = 0; webEngine.getLoadWorker().stateProperty().addListener{_, oldState, newState -> logger.info("State from {} to {} , exception: {}", oldState, newState, webEngine.getLoadWorker().getException()); if (newState == Worker.State.FAILED) { if (reloadCount < 5) { reloadCount += 1 logger.info("reload {}", url) webEngine.load(url); } } } webEngine.titleProperty().addListener{_, _, t -> if (t != null && t.isNotEmpty()) { stage.setTitle(t) } } webEngine.load(url); val scene = Scene(webView, windowSize.width, windowSize.height) stage.setScene(scene) stage.setTitle("阅读") stage.getIcons().addAll(defaultIcons); stage.initStyle(StageStyle.UNIFIED); stage.show() } override fun stop() { saveStorage("windowConfig", value = windowConfigMap, pretty = true) super.stop() var context = SpringContextUtils.getApplicationContext() logger.info("application stop: {}", context) System.exit(SpringApplication.exit(context)) } } fun main(args: Array) { logger.info("args: {}", args) launchArgs = args val app = ReaderUIApplication() app.boot() } ================================================ FILE: src/main/java/com/htmake/reader/SpringEvent.java ================================================ package com.htmake.reader; import org.springframework.context.ApplicationEvent; public class SpringEvent extends ApplicationEvent { private String event; private String message; public SpringEvent(Object source, String event, String message) { super(source); this.event = event; this.message = message; } public String getEvent() { return event; } public void setEvent(String event) { this.event = event; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } } ================================================ FILE: src/main/java/com/htmake/reader/api/ReturnData.kt ================================================ package com.htmake.reader.api class ReturnData { var isSuccess: Boolean = false private set var errorMsg: String = "未知错误,请联系开发者!" private set var data: Any? = null private set fun setErrorMsg(errorMsg: String): ReturnData { this.isSuccess = false this.errorMsg = errorMsg return this } fun setData(data: Any): ReturnData { this.isSuccess = true this.errorMsg = "" this.data = data return this } } ================================================ FILE: src/main/java/com/htmake/reader/api/YueduApi.kt ================================================ package com.htmake.reader.api import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssArticle import io.legado.app.model.webBook.WebBook import io.vertx.ext.web.Router import io.vertx.ext.web.RoutingContext import io.vertx.ext.web.handler.StaticHandler; import mu.KotlinLogging import com.htmake.reader.config.AppConfig import com.htmake.reader.config.BookConfig import io.legado.app.constant.DeepinkBookSource import com.htmake.reader.api.controller.BookController import com.htmake.reader.api.controller.BookSourceController import com.htmake.reader.api.controller.RssSourceController import com.htmake.reader.api.controller.UserController import com.htmake.reader.api.controller.WebdavController import com.htmake.reader.api.controller.ReplaceRuleController import com.htmake.reader.api.controller.BookmarkController import com.htmake.reader.utils.error import com.htmake.reader.utils.success import com.htmake.reader.utils.getStorage import com.htmake.reader.utils.saveStorage import com.htmake.reader.utils.asJsonArray import com.htmake.reader.utils.asJsonObject import com.htmake.reader.utils.toDataClass import com.htmake.reader.utils.toMap import com.htmake.reader.utils.fillData import com.htmake.reader.utils.getWorkDir import com.htmake.reader.utils.getRandomString import com.htmake.reader.utils.genEncryptedPassword import com.htmake.reader.entity.User import com.htmake.reader.utils.SpringContextUtils import com.htmake.reader.utils.deleteRecursively import com.htmake.reader.utils.unzip import com.htmake.reader.utils.zip import com.htmake.reader.utils.jsonEncode import com.htmake.reader.utils.getRelativePath import com.htmake.reader.verticle.RestVerticle import com.htmake.reader.SpringEvent import org.springframework.stereotype.Component import io.vertx.core.json.JsonObject import io.vertx.core.json.JsonArray import io.vertx.core.http.HttpMethod import com.htmake.reader.api.ReturnData import io.legado.app.utils.MD5Utils import java.net.URLDecoder; import java.net.URLEncoder; import java.net.URL; import java.util.UUID; import io.vertx.ext.web.client.WebClient import org.springframework.beans.factory.annotation.Autowired import org.springframework.core.env.Environment import java.io.File import java.lang.Runtime import kotlin.collections.mutableMapOf import kotlin.system.measureTimeMillis import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.text.SimpleDateFormat; import io.legado.app.utils.EncoderUtils import io.legado.app.model.rss.Rss import org.springframework.scheduling.annotation.Scheduled import io.legado.app.model.localBook.LocalBook import java.nio.file.Paths import kotlinx.coroutines.withContext import kotlinx.coroutines.async import kotlinx.coroutines.Deferred import kotlinx.coroutines.CoroutineScope private val logger = KotlinLogging.logger {} @Component class YueduApi : RestVerticle() { @Autowired private lateinit var appConfig: AppConfig @Autowired private lateinit var env: Environment override suspend fun initRouter(router: Router) { setupPort() // 旧版数据迁移 migration() // web界面 router.route("/*").handler(StaticHandler.create("web").setDefaultContentEncoding("UTF-8")); // assets var assetsDir = getWorkDir("storage", "assets"); var assetsDirFile = File(assetsDir); if (!assetsDirFile.exists()) { assetsDirFile.mkdirs(); } var assetsCss = getWorkDir("storage", "assets", "reader.css"); var assetsCssFile = File(assetsCss); if (!assetsCssFile.exists()) { assetsCssFile.writeText("/* 在此处可以编写CSS样式来自定义页面 */"); } router.route("/assets/*").handler(StaticHandler.create().setAllowRootFileSystemAccess(true).setWebRoot(assetsDir).setDefaultContentEncoding("UTF-8")); // epub资源 var dataDir = getWorkDir("storage", "data"); router.route("/epub/*").handler { var path = it.request().path().replace("/epub/", "/", true) path = URLDecoder.decode(path, "UTF-8") if (path.endsWith("html", true)) { var filePath = File(dataDir + path) if (filePath.exists()) { // 处理 js 注入脚本 BookConfig.injectJavascriptToEpubChapter(filePath.toString()) } } it.next() } router.route("/epub/*").handler(StaticHandler.create().setAllowRootFileSystemAccess(true).setWebRoot(dataDir).setDefaultContentEncoding("UTF-8")); // 获取系统信息 router.get("/reader3/getSystemInfo").coroutineHandler { getSystemInfo(it) } ////////// 接口部分 val bookController = BookController(coroutineContext) val bookSourceController = BookSourceController(coroutineContext) val rssSourceController = RssSourceController(coroutineContext) val userController = UserController(coroutineContext) val webdavController = WebdavController(coroutineContext, router) { ctx, error -> onHandlerError(ctx, error) } val replaceRuleController = ReplaceRuleController(coroutineContext) val bookmarkController = BookmarkController(coroutineContext) /** 书源模块 */ router.post("/reader3/saveBookSource").coroutineHandler { bookSourceController.saveBookSource(it) } router.post("/reader3/saveBookSources").coroutineHandler { bookSourceController.saveBookSources(it) } router.get("/reader3/getBookSource").coroutineHandler { bookSourceController.getBookSource(it) } router.post("/reader3/getBookSource").coroutineHandler { bookSourceController.getBookSource(it) } router.get("/reader3/getBookSources").coroutineHandler { bookSourceController.getBookSources(it) } router.post("/reader3/getBookSources").coroutineHandler { bookSourceController.getBookSources(it) } router.post("/reader3/deleteAllBookSources").coroutineHandler { bookSourceController.deleteAllBookSources(it) } router.post("/reader3/deleteBookSource").coroutineHandler { bookSourceController.deleteBookSource(it) } router.post("/reader3/deleteBookSources").coroutineHandler { bookSourceController.deleteBookSources(it) } // 上传书源文件 router.post("/reader3/readSourceFile").coroutineHandler { bookSourceController.readSourceFile(it) } // 读取远程书源文件 router.post("/reader3/readRemoteSourceFile").coroutineHandlerWithoutRes { bookSourceController.readRemoteSourceFile(it) } // 设置默认书源 router.post("/reader3/setAsDefaultBookSources").coroutineHandler { bookSourceController.setAsDefaultBookSources(it) } router.post("/reader3/deleteUserBookSource").coroutineHandler { bookSourceController.deleteUserBookSource(it) } router.post("/reader3/deleteBookSourcesFile").coroutineHandler { bookSourceController.deleteBookSourcesFile(it) } /** 书籍模块 */ // 书架 router.get("/reader3/getBookshelf").coroutineHandler { bookController.getBookshelf(it) } router.get("/reader3/getShelfBook").coroutineHandler { bookController.getShelfBook(it) } router.post("/reader3/saveBook").coroutineHandler { bookController.saveBook(it) } router.post("/reader3/deleteBook").coroutineHandler { bookController.deleteBook(it) } router.post("/reader3/deleteBooks").coroutineHandler { bookController.deleteBooks(it) } // 失效书源 router.post("/reader3/getInvalidBookSources").coroutineHandler { bookController.getInvalidBookSources(it) } // 探索 router.post("/reader3/exploreBook").coroutineHandler { bookController.exploreBook(it) } router.get("/reader3/exploreBook").coroutineHandler { bookController.exploreBook(it) } // 搜索 router.get("/reader3/searchBook").coroutineHandler { bookController.searchBook(it) } router.post("/reader3/searchBook").coroutineHandler { bookController.searchBook(it) } router.get("/reader3/searchBookMulti").coroutineHandler { bookController.searchBookMulti(it) } router.post("/reader3/searchBookMulti").coroutineHandler { bookController.searchBookMulti(it) } router.get("/reader3/searchBookMultiSSE").coroutineHandlerWithoutRes { bookController.searchBookMultiSSE(it) } // 书籍详情 router.get("/reader3/getBookInfo").coroutineHandler { bookController.getBookInfo(it) } router.post("/reader3/getBookInfo").coroutineHandler { bookController.getBookInfo(it) } // 章节列表 router.get("/reader3/getChapterList").coroutineHandler { bookController.getChapterList(it) } router.post("/reader3/getChapterList").coroutineHandler { bookController.getChapterList(it) } // 内容 router.get("/reader3/getBookContent").coroutineHandler { bookController.getBookContent(it) } router.post("/reader3/getBookContent").coroutineHandler { bookController.getBookContent(it) } // 保存阅读进度 router.post("/reader3/saveBookProgress").coroutineHandler { bookController.saveBookProgress(it) } // 封面 router.get("/reader3/cover").coroutineHandlerWithoutRes { bookController.getBookCover(it) } // 搜索其它来源 router.get("/reader3/searchBookSource").coroutineHandler { bookController.searchBookSource(it) } router.post("/reader3/searchBookSource").coroutineHandler { bookController.searchBookSource(it) } router.get("/reader3/getAvailableBookSource").coroutineHandler { bookController.getAvailableBookSource(it) } router.post("/reader3/getAvailableBookSource").coroutineHandler { bookController.getAvailableBookSource(it) } router.get("/reader3/searchBookSourceSSE").coroutineHandlerWithoutRes { bookController.searchBookSourceSSE(it) } // 换源 router.get("/reader3/setBookSource").coroutineHandler { bookController.setBookSource(it) } router.post("/reader3/setBookSource").coroutineHandler { bookController.setBookSource(it) } // 修改分组 router.post("/reader3/saveBookGroupId").coroutineHandler { bookController.saveBookGroupId(it) } router.post("/reader3/addBookGroupMulti").coroutineHandler { bookController.addBookGroupMulti(it) } router.post("/reader3/removeBookGroupMulti").coroutineHandler { bookController.removeBookGroupMulti(it) } // 导入本地文件 router.post("/reader3/importBookPreview").coroutineHandler { bookController.importBookPreview(it) } router.post("/reader3/refreshLocalBook").coroutineHandler { bookController.refreshLocalBook(it) } // 获取txt章节规则 router.get("/reader3/getTxtTocRules").coroutineHandler { bookController.getTxtTocRules(it) } router.post("/reader3/getChapterListByRule").coroutineHandler { bookController.getChapterListByRule(it) } // 书籍分组 router.get("/reader3/getBookGroups").coroutineHandler { bookController.getBookGroups(it) } router.post("/reader3/saveBookGroup").coroutineHandler { bookController.saveBookGroup(it) } router.post("/reader3/deleteBookGroup").coroutineHandler { bookController.deleteBookGroup(it) } router.post("/reader3/saveBookGroupOrder").coroutineHandler { bookController.saveBookGroupOrder(it) } // 书仓功能 // 获取书仓文件列表 router.get("/reader3/getLocalStoreFileList").coroutineHandler { bookController.getLocalStoreFileList(it) } // 下载书仓文件 router.get("/reader3/getLocalStoreFile").coroutineHandlerWithoutRes { bookController.getLocalStoreFile(it) } // 删除书仓文件 router.post("/reader3/deleteLocalStoreFile").coroutineHandler { bookController.deleteLocalStoreFile(it) } router.post("/reader3/deleteLocalStoreFileList").coroutineHandler { bookController.deleteLocalStoreFileList(it) } // 从本地书仓/webdav导入 router.post("/reader3/importFromLocalPathPreview").coroutineHandler { bookController.importFromLocalPathPreview(it) } // 上传文件到书仓 router.post("/reader3/uploadFileToLocalStore").coroutineHandler { bookController.uploadFileToLocalStore(it) } // 调试书源 router.get("/reader3/bookSourceDebugSSE").coroutineHandlerWithoutRes { bookController.bookSourceDebugSSE(it) } // 缓存书籍章节 router.get("/reader3/cacheBookSSE").coroutineHandlerWithoutRes { bookController.cacheBookSSE(it) } // 获取书籍缓存信息 router.get("/reader3/getShelfBookWithCacheInfo").coroutineHandler { bookController.getShelfBookWithCacheInfo(it) } // 删除书籍章节缓存 router.post("/reader3/deleteBookCache").coroutineHandler { bookController.deleteBookCache(it) } // 导出书籍 router.post("/reader3/exportBook").coroutineHandlerWithoutRes { bookController.exportBook(it) } router.get("/reader3/exportBook").coroutineHandlerWithoutRes { bookController.exportBook(it) } // 全文搜索 router.get("/reader3/searchBookContent").coroutineHandler { bookController.searchBookContent(it) } router.post("/reader3/searchBookContent").coroutineHandler { bookController.searchBookContent(it) } /** 用户模块 */ // 上传文件 router.post("/reader3/uploadFile").coroutineHandler { userController.uploadFile(it) } // 删除文件 router.post("/reader3/deleteFile").coroutineHandler { userController.deleteFile(it) } // 登录 router.post("/reader3/login").coroutineHandler { userController.login(it) } // 注销登录 router.post("/reader3/logout").coroutineHandler { userController.logout(it) } // 获取用户信息 router.get("/reader3/getUserInfo").coroutineHandler { userController.getUserInfo(it) } // 用户备份本地配置 router.post("/reader3/saveUserConfig").coroutineHandler { userController.saveUserConfig(it) } // 用户恢复本地配置 router.get("/reader3/getUserConfig").coroutineHandler { userController.getUserConfig(it) } // 获取用户列表 router.get("/reader3/getUserList").coroutineHandler { userController.getUserList(it) } // 删除用户 router.post("/reader3/deleteUsers").coroutineHandler { userController.deleteUsers(it) } // 添加用户 router.post("/reader3/addUser").coroutineHandler { userController.addUser(it) } // 重置用户密码 router.post("/reader3/resetPassword").coroutineHandler { userController.resetPassword(it) } // 更新用户 router.post("/reader3/updateUser").coroutineHandler { userController.updateUser(it) } /** webdav模块 */ // 获取webdav备份列表 router.get("/reader3/getWebdavFileList").coroutineHandler { webdavController.getWebdavFileList(it) } // 下载webdav文件 router.get("/reader3/getWebdavFile").coroutineHandlerWithoutRes { webdavController.getWebdavFile(it) } // 上传webdav文件 router.post("/reader3/uploadFileToWebdav").coroutineHandler { webdavController.uploadFileToWebdav(it) } // 删除webdav文件 router.get("/reader3/deleteWebdavFile").coroutineHandler { webdavController.deleteWebdavFile(it) } router.post("/reader3/deleteWebdavFile").coroutineHandler { webdavController.deleteWebdavFile(it) } router.post("/reader3/deleteWebdavFileList").coroutineHandler { webdavController.deleteWebdavFileList(it) } // 从webdav备份恢复 router.post("/reader3/restoreFromWebdav").coroutineHandler { webdavController.restoreFromWebdav(it) } // 备份到webdav router.post("/reader3/backupToWebdav").coroutineHandler { webdavController.backupToWebdav(it) } /** rss模块 */ // rss router.get("/reader3/getRssSources").coroutineHandler { rssSourceController.getRssSources(it) } router.post("/reader3/saveRssSource").coroutineHandler { rssSourceController.saveRssSource(it) } router.post("/reader3/saveRssSources").coroutineHandler { rssSourceController.saveRssSources(it) } router.post("/reader3/deleteRssSource").coroutineHandler { rssSourceController.deleteRssSource(it) } // rss 列表 router.get("/reader3/getRssArticles").coroutineHandler { rssSourceController.getRssArticles(it) } router.post("/reader3/getRssArticles").coroutineHandler { rssSourceController.getRssArticles(it) } // rss 内容 router.get("/reader3/getRssContent").coroutineHandler { rssSourceController.getRssContent(it) } router.post("/reader3/getRssContent").coroutineHandler { rssSourceController.getRssContent(it) } /** 替换规则模块 */ router.get("/reader3/getReplaceRules").coroutineHandler { replaceRuleController.getReplaceRules(it) } router.post("/reader3/saveReplaceRule").coroutineHandler { replaceRuleController.saveReplaceRule(it) } router.post("/reader3/saveReplaceRules").coroutineHandler { replaceRuleController.saveReplaceRules(it) } router.post("/reader3/deleteReplaceRule").coroutineHandler { replaceRuleController.deleteReplaceRule(it) } router.post("/reader3/deleteReplaceRules").coroutineHandler { replaceRuleController.deleteReplaceRules(it) } /** 书签模块 */ router.get("/reader3/getBookmarks").coroutineHandler { bookmarkController.getBookmarks(it) } router.post("/reader3/saveBookmark").coroutineHandler { bookmarkController.saveBookmark(it) } router.post("/reader3/saveBookmarks").coroutineHandler { bookmarkController.saveBookmarks(it) } router.post("/reader3/deleteBookmark").coroutineHandler { bookmarkController.deleteBookmark(it) } router.post("/reader3/deleteBookmarks").coroutineHandler { bookmarkController.deleteBookmarks(it) } } suspend fun setupPort() { logger.info("port: {}", port) var serverPort = env.getProperty("reader.server.port", Int::class.java) logger.info("serverPort: {}", serverPort) if (serverPort != null && serverPort > 0) { port = serverPort; } } suspend fun migration() { try { var storageDir = File(getWorkDir("storage")) var dataDir = File(getWorkDir("storage", "data", "default")) if (!storageDir.exists()) { // 直接使用新版本,则创建 default 目录,防止重启之后被迁移 dataDir.mkdirs() } else if (!dataDir.exists()) { // 旧版本不管了 dataDir.mkdirs() // 可能存在旧版本,尝试迁移 // var backupDir = File(getWorkDir("storage-backup")) // storageDir.renameTo(backupDir) // dataDir.parentFile.mkdirs() // backupDir.copyRecursively(dataDir) } } catch(e: Exception) { e.printStackTrace() } } override fun started() { SpringContextUtils.getApplicationContext().publishEvent(SpringEvent(this as java.lang.Object, "READY", "")); } override fun onStartError() { SpringContextUtils.getApplicationContext().publishEvent(SpringEvent(this as java.lang.Object, "START_ERROR", "应用启动失败,请检查" + port + "端口是否被占用")); } override fun onHandlerError(ctx: RoutingContext, error: Exception) { val returnData = ReturnData() logger.error("onHandlerError: ", error) if (!ctx.response().headWritten()) { ctx.success(returnData.setErrorMsg(error.toString())) } else { ctx.response().end(error.toString()) } } private suspend fun getSystemInfo(context: RoutingContext): ReturnData { val returnData = ReturnData() var systemFont = System.getProperty("reader.system.fonts") var freeMemory = "" + (Runtime.getRuntime().freeMemory() / 1024 / 1024) + "M" var totalMemory = "" + (Runtime.getRuntime().totalMemory() / 1024 / 1024) + "M" var maxMemory = "" + (Runtime.getRuntime().maxMemory() / 1024 / 1024) + "M" return returnData.setData(mapOf( "fonts" to systemFont, "freeMemory" to freeMemory, "totalMemory" to totalMemory, "maxMemory" to maxMemory )) } /** * 定时任务 */ /** * 每十分钟检查一次书架书籍更新 */ @Scheduled(cron = "0 0/10 * * * ?") fun shelfUpdateJob() { launch(Dispatchers.IO) { try { val bookController = BookController(coroutineContext) logger.info("开始检查书架书籍更新") // 刷新系统默认书架 bookController.getBookShelfBooks(true, "default") // 刷新用户书架 if (appConfig.secure) { var userMap = mutableMapOf>() var userMapJson: JsonObject? = asJsonObject(getStorage("data", "users")) if (userMapJson != null) { userMap = userMapJson.map as MutableMap> } userMap.forEach{ try { var ns = it.value.getOrDefault("username", "") as String? ?: "" if (ns.isNotEmpty()) { bookController.getBookShelfBooks(true, ns) } } catch (e: Exception) { e.printStackTrace() } } } logger.info("书架书籍更新检查结束") } catch (e: Exception) { e.printStackTrace() } } } /** * 每天清理不活跃用户 */ @Scheduled(cron = "0 59 23 * * ?") fun clearUser() { if (appConfig.autoClearInactiveUser <= 0 || !appConfig.secure) { return } launch(Dispatchers.IO) { try { logger.info("开始清理 {} 天未登录用户", appConfig.autoClearInactiveUser) var userMap = mutableMapOf>() var userMapJson: JsonObject? = asJsonObject(getStorage("data", "users")) if (userMapJson != null) { userMap = userMapJson.map as MutableMap> } val expireTime = System.currentTimeMillis() - appConfig.autoClearInactiveUser * 86400L * 1000L userMap.keys.forEach{ try { var user = userMap.get(it) if (user != null) { var username = user.getOrDefault("username", "") as String? ?: "" var last_login_at = user.getOrDefault("last_login_at", 0) as Long? ?: 0L if (username.isNotEmpty() && last_login_at < expireTime) { logger.info("delete user: {}", user) // 删除用户信息 userMap.remove(username) // 移除用户目录 var userHome = File(getWorkDir("storage", "data", username)) logger.info("delete userHome: {}", userHome) if (userHome.exists()) { userHome.deleteRecursively() } } } } catch (e: Exception) { e.printStackTrace() } } logger.info("不活跃用户自动清理结束") } catch (e: Exception) { e.printStackTrace() } } } } ================================================ FILE: src/main/java/com/htmake/reader/api/controller/BaseController.kt ================================================ package com.htmake.reader.api.controller import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssArticle import io.legado.app.model.webBook.WebBook import io.vertx.ext.web.Route import io.vertx.ext.web.Router import io.vertx.ext.web.RoutingContext import io.vertx.ext.web.handler.StaticHandler; import mu.KotlinLogging import com.htmake.reader.config.AppConfig import com.htmake.reader.config.BookConfig import io.legado.app.constant.DeepinkBookSource import com.htmake.reader.utils.error import com.htmake.reader.utils.success import com.htmake.reader.utils.getStorage import com.htmake.reader.utils.saveStorage import com.htmake.reader.utils.asJsonArray import com.htmake.reader.utils.asJsonObject import com.htmake.reader.utils.toDataClass import com.htmake.reader.utils.toMap import com.htmake.reader.utils.fillData import com.htmake.reader.utils.getWorkDir import com.htmake.reader.utils.getRandomString import com.htmake.reader.utils.genEncryptedPassword import com.htmake.reader.entity.User import com.htmake.reader.utils.SpringContextUtils import com.htmake.reader.utils.deleteRecursively import com.htmake.reader.utils.unzip import com.htmake.reader.utils.zip import com.htmake.reader.utils.jsonEncode import com.htmake.reader.utils.getRelativePath import com.htmake.reader.utils.getFileExtetion import com.htmake.reader.verticle.RestVerticle import com.htmake.reader.SpringEvent import org.springframework.stereotype.Component import io.vertx.core.json.JsonObject import io.vertx.core.json.JsonArray import io.vertx.core.http.HttpMethod import com.htmake.reader.api.ReturnData import io.legado.app.utils.MD5Utils import java.net.URLDecoder; import java.net.URLEncoder; import java.net.URL; import java.util.UUID; import io.vertx.ext.web.client.WebClient import org.springframework.beans.factory.annotation.Autowired import org.springframework.core.env.Environment import java.io.File import java.lang.Runtime import kotlin.collections.mutableMapOf import kotlin.system.measureTimeMillis import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.text.SimpleDateFormat; import io.legado.app.utils.EncoderUtils import io.legado.app.model.rss.Rss import org.springframework.scheduling.annotation.Scheduled import io.legado.app.model.localBook.LocalBook import java.nio.file.Paths import kotlinx.coroutines.withContext import kotlinx.coroutines.async import kotlinx.coroutines.Deferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import io.legado.app.help.coroutine.Coroutine private val logger = KotlinLogging.logger {} open class BaseController(override val coroutineContext: CoroutineContext): CoroutineScope { var loginExpireDays = 7 val appConfig: AppConfig val env: Environment init { appConfig = SpringContextUtils.getBean("appConfig", AppConfig::class.java) env = SpringContextUtils.getBean(Environment::class.java) } suspend fun saveUserSession(context: RoutingContext, userMap: MutableMap>, user: User, regenerateToken: Boolean = true): Map { user.last_login_at = System.currentTimeMillis() if (regenerateToken) { user.token = genEncryptedPassword(user.username, System.currentTimeMillis().toString()) var tokenMap: MutableMap? = null var expire = System.currentTimeMillis() + loginExpireDays * 86400 * 1000 if (user.token_map != null) { tokenMap = user.token_map as? MutableMap } if (tokenMap == null) { tokenMap = mutableMapOf(user.token to expire) } else { tokenMap.put(user.token, expire) } // 删除已过期token tokenMap.values.removeAll { it < user.last_login_at } user.token_map = tokenMap } userMap.put(user.username, user.toMap()) saveStorage("data", "users", value = userMap) val loginData = formatUser(user) context.session().put("username", user.username) context.put("username", user.username) return loginData } suspend fun checkAuth(context: RoutingContext): Boolean { if (!appConfig.secure) { return true } var username = context.session().get("username") as String? ?: "" var userInfo = getUserInfoClass(username) if (userInfo != null) { context.put("username", userInfo.username) context.put("userInfo", userInfo) return true } // 自动登录 var accessToken = context.queryParam("accessToken").firstOrNull() ?: "" if (accessToken.isNotEmpty()) { var userMap = mutableMapOf>() var userMapJson: JsonObject? = asJsonObject(getStorage("data", "users")) if (userMapJson != null) { userMap = userMapJson.map as? MutableMap> ?: mutableMapOf>() } var tmp = accessToken.split(":", limit=2) if (tmp.size >= 2) { var _username = tmp[0] var token = tmp[1] var existedUser: User? = userMap.getOrDefault(_username, null)?.toDataClass() if (existedUser != null && token.isNotEmpty()) { var isLogin = false if (existedUser.token.isNotEmpty() && existedUser.token.equals(token)) { isLogin = true } // 查找历史有效会话 if (!isLogin && existedUser.token_map != null) { var tokenMap = existedUser.token_map as? MutableMap if (tokenMap != null && tokenMap.containsKey(token)) { if (tokenMap.getOrDefault(token, 0L) > System.currentTimeMillis()) { isLogin = true // 延长有效期 tokenMap.put(token, System.currentTimeMillis() + loginExpireDays * 86400 * 1000) } else { // 删除过期token tokenMap.remove(token) } existedUser.token_map = tokenMap } } if (isLogin) { // 保存用户session saveUserSession(context, userMap, existedUser, false) context.put("username", existedUser.username) context.put("userInfo", existedUser) } return isLogin } } } return false } fun checkManagerAuth(context: RoutingContext): Boolean { if (!appConfig.secure) { return true } if (appConfig.secureKey.isEmpty()) { return true } var secureKey = context.queryParam("secureKey").firstOrNull() ?: "" if (secureKey.equals(appConfig.secureKey)) { // 判断是否需要修改 userNameSpace var userNS = context.queryParam("userNS").firstOrNull() if (userNS != null && userNS.isNotEmpty()) { context.put("userNameSpace", userNS) } return true } return false } fun getUserNameSpace(context: RoutingContext): String { if (!appConfig.secure) { return "default" } // 管理权限,可以修改 userNameSpace 来获取任意用户信息 checkManagerAuth(context) var userNS = context.get("userNameSpace") as String? if (userNS != null && userNS.isNotEmpty()) { return userNS } var username = context.get("username") as String? if (username != null) { return username; } return "default" } fun getUserStorage(context: Any, vararg path: String): String? { var userNameSpace = "" when(context) { is RoutingContext -> userNameSpace = getUserNameSpace(context) is String -> userNameSpace = context } if (userNameSpace.isEmpty()) { return getStorage("data", *path) } return getStorage("data", userNameSpace, *path) } fun saveUserStorage(context: Any, path: String, value: Any) { var userNameSpace = "" when(context) { is RoutingContext -> userNameSpace = getUserNameSpace(context) is String -> userNameSpace = context } if (userNameSpace.isEmpty()) { return saveStorage("data", path, value = value) } return saveStorage("data", userNameSpace, path, value = value) } fun getUserInfoClass(username: String): User? { var user: User? = getUserInfoMap(username)?.toDataClass() return user } fun getUserInfoMap(username: String): Map? { if (username.isEmpty()) { return null } var userMap = mutableMapOf>() var userMapJson: JsonObject? = asJsonObject(getStorage("data", "users")) if (userMapJson != null) { userMap = userMapJson.map as MutableMap> } return userMap.getOrDefault(username, null) } fun formatUser(userInfo: Any): MutableMap { var user: User? = null if (userInfo !is User) { var userMap = userInfo as? Map if (userMap != null) { user = userMap.toDataClass() } } else { user = userInfo } if (user == null) { return mutableMapOf() } return mutableMapOf( "username" to user.username, "lastLoginAt" to user.last_login_at, "accessToken" to user.username + ":" + user.token, "enableWebdav" to user.enable_webdav, "enableLocalStore" to user.enable_local_store, "createdAt" to user.created_at ) } fun getUserWebdavHome(context: Any): String { var prefix = getWorkDir("storage", "data") var userNameSpace = "" when(context) { is RoutingContext -> userNameSpace = getUserNameSpace(context) is String -> userNameSpace = context } if (userNameSpace.isNotEmpty()) { prefix = prefix + File.separator + userNameSpace } prefix = prefix + File.separator + "webdav" var file = File(prefix) if (!file.exists()) { file.mkdirs() } return prefix } fun getFileExt(url: String, defaultExt: String=""): String { return getFileExtetion(url, defaultExt) } suspend fun limitConcurrent(concurrentCount: Int, startIndex: Int, endIndex: Int, handler: suspend CoroutineScope.(Int) -> Any) { limitConcurrent(concurrentCount, startIndex, endIndex, handler) {_, _ -> true } } suspend fun limitConcurrent(concurrentCount: Int, startIndex: Int, endIndex: Int, handler: suspend CoroutineScope.(Int) -> Any, needContinue: (ArrayList, Int) -> Boolean) { var lastIndex = startIndex var loopCount = 0 var resultCount = 0 var loopStart = System.currentTimeMillis() var costTime = 0L var deferredList = arrayListOf>() while(true) { var croutineCount = deferredList.size; if (croutineCount < concurrentCount) { for(i in lastIndex until endIndex) { croutineCount += 1; deferredList.add(async { handler(i) }) lastIndex = i if (croutineCount >= concurrentCount) { break; } } } var resultList = arrayListOf() // 等待任何一个完成 while (resultList.size <= 0) { delay(10) var stillDeferredList = arrayListOf>() for (i in 0 until deferredList.size) { try { var deferred = deferredList.get(i) if (deferred.isCompleted) { resultCount++ resultList.add(deferred.getCompleted()) } else if (!deferred.isCancelled) { stillDeferredList.add(deferred) } else { resultCount++ } } catch(e: Exception) { } } deferredList.clear() deferredList.addAll(stillDeferredList) } if (resultCount / concurrentCount > loopCount) { loopCount = resultCount / concurrentCount costTime = System.currentTimeMillis() - loopStart logger.info("Loop: {} concurrentCount: {} lastIndex: {} endIndex: {} costTime: {} ms deferredList size: {}", loopCount, croutineCount, lastIndex, endIndex, costTime, deferredList.size) } if (lastIndex >= endIndex - 1) { // 搞完了,等待所有结束 for (i in 0 until deferredList.size) { try { resultList.add(deferredList.get(i).await()) } catch(e: Exception) { } } deferredList.clear() needContinue(resultList, loopCount) break; } if (resultList.size > 0) { if (!needContinue(resultList, loopCount)) { break; } } lastIndex = lastIndex + 1 } // for (i in 0 until concurrentCount) { // runBlocking(concurrentCount, startIndex + i , endIndex, handler, needContinue) // } } suspend fun runBlocking(concurrentCount: Int, startIndex: Int, endIndex: Int, handler: suspend CoroutineScope.(Int) -> Any, needContinue: (ArrayList, Int) -> Boolean) { var lastIndex = startIndex Coroutine.async(this, coroutineContext) { handler(lastIndex) }.timeout(30000L) .onSuccess(Dispatchers.IO) { if (lastIndex < endIndex - concurrentCount && needContinue(arrayListOf(it), 0)) { lastIndex += concurrentCount runBlocking(concurrentCount, lastIndex, endIndex, handler, needContinue) } } .onError(Dispatchers.IO) { if (lastIndex < endIndex - concurrentCount) { lastIndex += concurrentCount runBlocking(concurrentCount, lastIndex, endIndex, handler, needContinue) } else { needContinue(arrayListOf(), 0) } } } } ================================================ FILE: src/main/java/com/htmake/reader/api/controller/BookController.kt ================================================ package com.htmake.reader.api.controller import io.legado.app.constant.AppPattern import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.SearchResult import io.legado.app.exception.TocEmptyException import io.legado.app.model.webBook.WebBook import io.legado.app.help.DefaultData import io.vertx.ext.web.Route import io.vertx.ext.web.Router import io.vertx.ext.web.RoutingContext import io.vertx.ext.web.handler.StaticHandler; import mu.KotlinLogging import com.htmake.reader.config.AppConfig import com.htmake.reader.config.BookConfig import io.legado.app.constant.DeepinkBookSource import com.htmake.reader.utils.error import com.htmake.reader.utils.success import com.htmake.reader.utils.getStorage import com.htmake.reader.utils.saveStorage import com.htmake.reader.utils.asJsonArray import com.htmake.reader.utils.asJsonObject import com.htmake.reader.utils.toDataClass import com.htmake.reader.utils.toMap import com.htmake.reader.utils.fillData import com.htmake.reader.utils.getWorkDir import com.htmake.reader.utils.getRandomString import com.htmake.reader.utils.genEncryptedPassword import com.htmake.reader.entity.User import com.htmake.reader.utils.SpringContextUtils import com.htmake.reader.utils.deleteRecursively import com.htmake.reader.utils.unzip import com.htmake.reader.utils.zip import com.htmake.reader.utils.jsonEncode import com.htmake.reader.utils.getRelativePath import com.htmake.reader.verticle.RestVerticle import com.htmake.reader.SpringEvent import org.springframework.stereotype.Component import io.vertx.core.json.JsonObject import io.vertx.core.json.JsonArray import io.vertx.core.http.HttpMethod import com.htmake.reader.api.ReturnData import io.legado.app.utils.MD5Utils import io.legado.app.utils.FileUtils import java.net.URLDecoder; import java.net.URLEncoder; import java.net.URL; import java.nio.charset.Charset import java.util.UUID; import io.vertx.ext.web.client.WebClient import org.springframework.beans.factory.annotation.Autowired import org.springframework.core.env.Environment import java.io.File import java.io.FileOutputStream import java.lang.Runtime import kotlin.collections.mutableMapOf import kotlin.system.measureTimeMillis import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.text.SimpleDateFormat; import io.legado.app.utils.EncoderUtils import io.legado.app.utils.ACache import io.legado.app.utils.HtmlFormatter import io.legado.app.utils.NetworkUtils import io.legado.app.model.rss.Rss import io.legado.app.model.Debugger import io.legado.app.help.BookHelp import org.springframework.scheduling.annotation.Scheduled import io.legado.app.model.localBook.LocalBook import io.legado.app.model.analyzeRule.AnalyzeUrl import java.nio.file.Paths import kotlinx.coroutines.withContext import kotlinx.coroutines.async import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.Deferred import kotlinx.coroutines.CoroutineScope import me.ag2s.epublib.domain.* import me.ag2s.epublib.epub.EpubWriter import me.ag2s.epublib.util.ResourceUtil // import io.legado.app.help.coroutine.Coroutine private val logger = KotlinLogging.logger {} class BookController(coroutineContext: CoroutineContext): BaseController(coroutineContext) { var bookInfoCache = ACache.get("bookInfoCache", 1000 * 1000 * 2L, 10000) // 缓存 2M 的书籍信息 val concurrentLoopCount = 8 private var webClient: WebClient init { webClient = SpringContextUtils.getBean("webClient", WebClient::class.java) } private fun getInvalidBookSourceCache(userNameSpace: String): ACache { val cacheDir = File(getWorkDir("storage", "cache", "invalidBookSourceCache", userNameSpace)) // 缓存 5M 的失效书源信息 var invalidBookSourceCache = ACache.get(cacheDir, 1000 * 1000 * 5L, 1000000) return invalidBookSourceCache } private fun isInvalidBookSource(bookSource: BookSource, userNameSpace: String): Boolean { return getInvalidBookSourceCache(userNameSpace).getAsString(bookSource.bookSourceUrl) != null } private fun addInvalidBookSource(sourceUrl: String, invalidInfo: Map, userNameSpace: String) { // 保存600秒时间 getInvalidBookSourceCache(userNameSpace).put(sourceUrl, jsonEncode(invalidInfo), 600) } suspend fun getInvalidBookSources(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var userNameSpace = getUserNameSpace(context) val invalidBookSourceCache = getInvalidBookSourceCache(userNameSpace) val cacheDir = File(getWorkDir("storage", "cache", "invalidBookSourceCache", userNameSpace)) val files = cacheDir.listFiles() val invalidBookSourceList = arrayListOf>() if (files != null) { for (f in files) { invalidBookSourceCache.getByHashCode(f.name)?.let { info -> invalidBookSourceList.add(info.toMap()) } } } return returnData.setData(invalidBookSourceList) } suspend fun getBookInfo(context: RoutingContext): ReturnData { val returnData = ReturnData() var bookUrl: String if (context.request().method() == HttpMethod.POST) { // post 请求 bookUrl = context.bodyAsJson.getString("url") ?: context.bodyAsJson.getJsonObject("searchBook").getString("bookUrl") ?: "" } else { // get 请求 bookUrl = context.queryParam("url").firstOrNull() ?: "" } if (bookUrl.isNullOrEmpty()) { return returnData.setErrorMsg("请输入书籍链接") } logger.info("getBookInfo with bookUrl: {}", bookUrl) var bookInfo: Book? = null if (checkAuth(context)) { bookInfo = getShelfBookByURL(bookUrl, getUserNameSpace(context)) } if (bookInfo == null) { // 看看有没有缓存数据 var bookSource: String? = null var cacheInfo: Book? = bookInfoCache.getAsString(bookUrl)?.toMap()?.toDataClass() if (cacheInfo != null) { // 使用缓存的书籍信息包含的书源 bookSource = getBookSourceString(context, cacheInfo.origin) } else { bookSource = getBookSourceString(context) } if (bookSource.isNullOrEmpty()) { return returnData.setErrorMsg("未配置书源") } bookInfo = mergeBookCacheInfo(WebBook(bookSource, appConfig.debugLog).getBookInfo(bookUrl)) } // 缓存书籍信息 saveBookInfoCache(arrayListOf(bookInfo)) return returnData.setData(bookInfo) } suspend fun getBookCover(context: RoutingContext) { var coverUrl = context.queryParam("path").firstOrNull() ?: "" if (coverUrl.isNullOrEmpty()) { context.response().setStatusCode(404).end() return } var ext = getFileExt(coverUrl, "png") val md5Encode = MD5Utils.md5Encode(coverUrl).toString() var cachePath = getWorkDir("storage", "cache", md5Encode + "." + ext) var cacheFile = File(cachePath) if (cacheFile.exists()) { logger.info("send cache: {}", cacheFile) context.response().putHeader("Cache-Control", "86400").sendFile(cacheFile.toString()) return; } if (!cacheFile.parentFile.exists()) { cacheFile.parentFile.mkdirs() } launch(Dispatchers.IO) { webClient.getAbs(coverUrl).timeout(3000).send { var bodyBytes = it.result()?.bodyAsBuffer()?.getBytes() if (bodyBytes != null) { var res = context.response().putHeader("Cache-Control", "86400") cacheFile.writeBytes(bodyBytes) res.sendFile(cacheFile.toString()) } else { context.response().setStatusCode(404).end() } } } } suspend fun importBookPreview(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (context.fileUploads() == null || context.fileUploads().isEmpty()) { return returnData.setErrorMsg("请上传书籍文件") } var userNameSpace = getUserNameSpace(context) var fileList = arrayListOf>() context.fileUploads().forEach { var file = File(it.uploadedFileName()) logger.info("uploadFile: {} {} {}", it.uploadedFileName(), it.fileName(), file) if (file.exists()) { var fileName = it.fileName() val ext = getFileExt(fileName) if (ext != "txt" && ext != "epub" && ext != "umd" && ext != "cbz") { file.deleteRecursively() return returnData.setErrorMsg("不支持导入" + ext + "格式的书籍文件") } // 文件名格式化 fileName = FileUtils.getNameExcludeExtension(fileName) fileName = fileName.replace(AppPattern.fileNameRegex, "") fileName = fileName.substring(0, Math.min(50, fileName.length)) + "." + ext val localFilePath = Paths.get("storage", "assets", userNameSpace, "book", fileName).toString() val localFileUrl = "/assets/" + userNameSpace + "/book/" + fileName var filePath = localFilePath if (fileName.endsWith(".epub", true)) { filePath = filePath + File.separator + "index.epub" } if (fileName.endsWith(".cbz", true)) { filePath = filePath + File.separator + "index.cbz" } var newFile = File(getWorkDir(filePath)) if (!newFile.parentFile.exists()) { newFile.parentFile.mkdirs() } if (newFile.exists()) { newFile.delete() } logger.info("moveTo: {}", newFile) if (file.copyRecursively(newFile)) { val book = Book.initLocalBook(localFileUrl, localFilePath, getWorkDir()) book.setUserNameSpace(userNameSpace) try { val chapters = LocalBook.getChapterList(book) fileList.add(mapOf("book" to book, "chapters" to chapters)) } catch(e: TocEmptyException) { fileList.add(mapOf("book" to book, "chapters" to arrayListOf())) } } file.deleteRecursively() } } return returnData.setData(fileList) } suspend fun getTxtTocRules(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } return returnData.setData(DefaultData.txtTocRules) } suspend fun getChapterListByRule(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var book = context.bodyAsJson.mapTo(Book::class.java) if (book.origin.isNullOrEmpty()) { return returnData.setErrorMsg("未找到书源信息") } if (!book.isLocalTxt() && !book.isLocalEpub()) { return returnData.setErrorMsg("非本地txt/epub书籍") } book.setRootDir(getWorkDir()) book.setUserNameSpace(getUserNameSpace(context)) val chapters = LocalBook.getChapterList(book) return returnData.setData(mapOf("book" to book, "chapters" to chapters)) } suspend fun refreshLocalBook(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var bookUrl: String if (context.request().method() == HttpMethod.POST) { // post 请求 bookUrl = context.bodyAsJson.getString("bookUrl") } else { // get 请求 bookUrl = context.queryParam("bookUrl").firstOrNull() ?: "" } if (bookUrl.isNullOrEmpty()) { return returnData.setErrorMsg("请输入书籍链接") } // 根据书籍url获取书本信息 var userNameSpace = getUserNameSpace(context) var bookInfo = getShelfBookByURL(bookUrl, userNameSpace) if (bookInfo == null) { return returnData.setErrorMsg("书籍信息错误") } bookInfo.updateFromLocal(true) editShelfBook(bookInfo, userNameSpace) { existBook -> existBook.coverUrl = bookInfo.coverUrl logger.info("refreshLocalBook: {}", existBook) existBook } return returnData.setData(bookInfo) } suspend fun getChapterList(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var bookUrl: String var refresh: Int = 0 if (context.request().method() == HttpMethod.POST) { // post 请求 bookUrl = context.bodyAsJson.getString("url") ?: context.bodyAsJson.getJsonObject("book").getString("bookUrl") ?: "" refresh = context.bodyAsJson.getInteger("refresh", 0) } else { // get 请求 bookUrl = context.queryParam("url").firstOrNull() ?: "" refresh = context.queryParam("refresh").firstOrNull()?.toInt() ?: 0 } if (bookUrl.isNullOrEmpty()) { return returnData.setErrorMsg("请输入书籍链接") } // 根据书籍url获取书本信息 var userNameSpace = getUserNameSpace(context) var bookInfo = getShelfBookByURL(bookUrl, userNameSpace) var bookSource: String? = null if (bookInfo == null) { // 看看有没有缓存数据 var cacheInfo: Book? = bookInfoCache.getAsString(bookUrl)?.toMap()?.toDataClass() if (cacheInfo != null) { // 使用缓存的书籍信息包含的书源 bookSource = getBookSourceString(context, cacheInfo.origin) } else { // 看看有没有传入书源 bookSource = getBookSourceString(context) } if (bookSource.isNullOrEmpty()) { return returnData.setErrorMsg("未配置书源") } bookInfo = mergeBookCacheInfo(WebBook(bookSource, appConfig.debugLog).getBookInfo(bookUrl)) // 缓存书籍信息 saveBookInfoCache(arrayListOf(bookInfo)) } else { bookSource = getBookSourceString(context, bookInfo.origin) } if (!bookInfo.isLocalBook() && bookSource.isNullOrEmpty()) { return returnData.setErrorMsg("未配置书源") } bookInfo.setRootDir(getWorkDir()) bookInfo.setUserNameSpace(userNameSpace) if (bookInfo.isLocalBook()) { val localFile = bookInfo.getLocalFile() if (!localFile.exists()) { logger.info("localFile: {} not exists", localFile) return returnData.setErrorMsg("本地书籍源文件不存在") } } // 缓存章节列表 logger.info("bookInfo: {}", bookInfo) var chapterList = getLocalChapterList(bookInfo, bookSource ?: "", refresh > 0, getUserNameSpace(context)) return returnData.setData(chapterList) } suspend fun saveBookProgress(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var bookUrl: String var chapterIndex: Int if (context.request().method() == HttpMethod.POST) { // post 请求 bookUrl = context.bodyAsJson.getString("url") ?: context.bodyAsJson.getJsonObject("searchBook").getString("bookUrl") ?: "" chapterIndex = context.bodyAsJson.getInteger("index", -1) } else { // get 请求 bookUrl = context.queryParam("url").firstOrNull() ?: "" chapterIndex = context.queryParam("index").firstOrNull()?.toInt() ?: -1 } if (bookUrl.isNullOrEmpty()) { return returnData.setErrorMsg("请输入书籍链接") } var userNameSpace = getUserNameSpace(context) // 看看有没有加入书架 var bookInfo = getShelfBookByURL(bookUrl, userNameSpace) if (bookInfo == null || bookInfo.origin.isNullOrEmpty()) { return returnData.setErrorMsg("书籍未加入书架") } var bookSource = getBookSourceStringBySourceURL(bookInfo.origin, userNameSpace) if (!bookInfo.isLocalBook() && bookSource.isNullOrEmpty()) { return returnData.setErrorMsg("未配置书源") } var chapterList = getLocalChapterList(bookInfo, bookSource ?: "", false, userNameSpace) if (chapterIndex >= chapterList.size) { return returnData.setErrorMsg("章节不存在") } var chapterInfo = chapterList.get(chapterIndex) // 书架书籍保存阅读进度 saveShelfBookProgress(bookInfo, chapterInfo, userNameSpace) // 保存到 webdav saveBookProgressToWebdav(bookInfo, chapterInfo, userNameSpace) return returnData.setData("") } suspend fun getBookContent(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var chapterUrl: String var bookUrl: String var chapterIndex: Int var cache: Int var refresh: Int if (context.request().method() == HttpMethod.POST) { // post 请求 chapterUrl = context.bodyAsJson.getString("chapterUrl") ?: context.bodyAsJson.getJsonObject("bookChapter")?.getString("url") ?: "" bookUrl = context.bodyAsJson.getString("url") ?: context.bodyAsJson.getJsonObject("searchBook")?.getString("bookUrl") ?: "" chapterIndex = context.bodyAsJson.getInteger("index", -1) cache = context.bodyAsJson.getInteger("cache", 0) refresh = context.bodyAsJson.getInteger("refresh", 0) } else { // get 请求 chapterUrl = context.queryParam("chapterUrl").firstOrNull() ?: "" bookUrl = context.queryParam("url").firstOrNull() ?: "" chapterIndex = context.queryParam("index").firstOrNull()?.toInt() ?: -1 cache = context.queryParam("cache").firstOrNull()?.toInt() ?: 0 refresh = context.queryParam("refresh").firstOrNull()?.toInt() ?: 0 } if (bookUrl.isNullOrEmpty()) { return returnData.setErrorMsg("请输入书籍链接") } var bookSource = getBookSourceString(context) var userNameSpace = getUserNameSpace(context) var isInBookShelf = false var bookInfo: Book? = null var chapterInfo: BookChapter? = null var nextChapterUrl: String? = null if (!bookUrl.isNullOrEmpty()) { // 看看有没有加入书架 bookInfo = getShelfBookByURL(bookUrl, userNameSpace) if (bookInfo != null && !bookInfo.origin.isNullOrEmpty()) { isInBookShelf = true bookSource = getBookSourceStringBySourceURL(bookInfo.origin, userNameSpace) } // 看看有没有缓存数据 var cacheInfo: Book? = bookInfoCache.getAsString(bookUrl)?.toMap()?.toDataClass() if (cacheInfo != null) { // 使用缓存的书籍信息包含的书源 bookSource = getBookSourceString(context, cacheInfo.origin) } if (chapterUrl.isNullOrEmpty() && chapterIndex >= 0) { // 根据 url 和 index 获取章节内容 if (bookUrl.isNullOrEmpty()) { return returnData.setErrorMsg("请输入书籍链接") } if (bookInfo != null && !bookInfo.isLocalBook() && bookSource.isNullOrEmpty()) { return returnData.setErrorMsg("未配置书源") } bookInfo = bookInfo ?: mergeBookCacheInfo(WebBook(bookSource ?: "", appConfig.debugLog).getBookInfo(bookUrl)) var chapterList = getLocalChapterList(bookInfo, bookSource ?: "", false, userNameSpace) if (chapterIndex < chapterList.size) { chapterInfo = chapterList.get(chapterIndex) // 书架书籍保存阅读进度 if (isInBookShelf && cache != 1) { saveShelfBookProgress(bookInfo, chapterInfo, userNameSpace) // 保存到 webdav saveBookProgressToWebdav(bookInfo, chapterInfo, userNameSpace) } chapterUrl = chapterInfo.url if (chapterIndex + 1 < chapterList.size) { var nextChapterInfo = chapterList.get(chapterIndex + 1) nextChapterUrl = nextChapterInfo.url } } } } if (bookInfo == null) { return returnData.setErrorMsg("获取书籍信息失败") } if (!bookInfo.isLocalBook() && bookSource.isNullOrEmpty()) { return returnData.setErrorMsg("未配置书源") } if (chapterInfo == null || chapterUrl.isNullOrEmpty()) { return returnData.setErrorMsg("获取章节链接失败") } var content = "" bookInfo.setRootDir(getWorkDir()) bookInfo.setUserNameSpace(userNameSpace) if (bookInfo.isLocalBook()) { val localFile = bookInfo.getLocalFile() if (!localFile.exists()) { return returnData.setErrorMsg("本地源书籍文件不存在") } if (chapterInfo == null) { var chapterList = getLocalChapterList(bookInfo, bookSource ?: "", false, userNameSpace) for(i in 0 until chapterList.size) { if (chapterUrl == chapterList.get(i).url) { chapterInfo = chapterList.get(i) break } } if (chapterInfo == null) { return returnData.setErrorMsg("获取章节信息失败") } } if (bookInfo.isEpub()) { if (!extractEpub(bookInfo)) { return returnData.setErrorMsg("Epub书籍解压失败") } val epubRootDir = bookInfo.getEpubRootDir() var chapterFilePath = getWorkDir(bookInfo.bookUrl, "index", epubRootDir, chapterInfo.url) logger.info("chapterFilePath: {} {}", chapterFilePath, epubRootDir) if (!File(chapterFilePath).exists()) { return returnData.setErrorMsg("章节文件不存在") } // 处理 js 注入脚本 // BookConfig.injectJavascriptToEpubChapter(chapterFilePath); // 直接返回 html访问地址 if (epubRootDir.isEmpty()) { content = bookInfo.bookUrl.replace("storage/data/", "/epub/") + "/index/" + chapterInfo.url } else { content = bookInfo.bookUrl.replace("storage/data/", "/epub/") + "/index/" + epubRootDir + "/" + chapterInfo.url } return returnData.setData(content) } else if (bookInfo.isCbz()) { if (!extractCbz(bookInfo)) { return returnData.setErrorMsg("CBZ书籍解压失败") } var chapterFilePath = getWorkDir(bookInfo.bookUrl, "index", chapterInfo.url) logger.info("chapterFilePath: {}", chapterFilePath) val chapterFile = File(chapterFilePath) if (!chapterFile.exists()) { return returnData.setErrorMsg("章节文件不存在") } val ext = getFileExt(chapterFile.name).lowercase() val imageExt = listOf("jpg", "jpeg", "gif", "png", "bmp", "webp", "svg") val fileUrl = "__API_ROOT__" + bookInfo.bookUrl.replace("storage/data/", "/epub/") + "/index/" + chapterInfo.url if (!imageExt.contains(ext)) { return returnData.setData(fileUrl) } content = "" return returnData.setData(content) } var bookContent = LocalBook.getContent(bookInfo, chapterInfo) if (bookContent == null) { return returnData.setErrorMsg("获取章节内容失败") } content = bookContent } else { // 查找章节缓存 var chapterCacheFile: File? = null if (refresh <= 0 && appConfig.cacheChapterContent) { val localCacheDir = getChapterCacheDir(bookInfo, userNameSpace) chapterCacheFile = File(localCacheDir.absolutePath + File.separator + chapterIndex + ".txt") if (chapterCacheFile.exists()) { content = chapterCacheFile.readText() logger.info("使用缓存的章节内容: {}", chapterCacheFile.toString()) return returnData.setData(content) } } try { content = WebBook(bookSource ?: "", appConfig.debugLog).getBookContent(bookInfo, chapterInfo, nextChapterUrl) if (appConfig.cacheChapterContent && chapterCacheFile != null) { chapterCacheFile.writeText(content) // 保存图片 BookHelp.saveImages( this, BookSource.fromJson(bookSource ?: "").getOrNull() ?: BookSource(), bookInfo, chapterInfo, content ) } } catch(e: Exception) { if (!bookSource.isNullOrEmpty()) { var bookSourceObject = asJsonObject(bookSource)?.mapTo(BookSource::class.java) if (bookSourceObject != null) { // 标记为失败源 val info = mutableMapOf("sourceUrl" to bookSourceObject.bookSourceUrl, "time" to System.currentTimeMillis(), "error" to e.toString()) addInvalidBookSource(bookSourceObject.bookSourceUrl, info, userNameSpace) } } throw e } } return returnData.setData(content) } suspend fun exploreBook(context: RoutingContext): ReturnData { val returnData = ReturnData() // 如果登录了,就使用用户的书源 checkAuth(context) var bookSource = getBookSourceString(context) if (bookSource.isNullOrEmpty()) { return returnData.setErrorMsg("未配置书源") } var page: Int var ruleFindUrl: String if (context.request().method() == HttpMethod.POST) { // post 请求 ruleFindUrl = context.bodyAsJson.getString("ruleFindUrl") page = context.bodyAsJson.getInteger("page", 1) } else { // get 请求 ruleFindUrl = context.queryParam("ruleFindUrl").firstOrNull() ?: "" page = context.queryParam("page").firstOrNull()?.toInt() ?: 1 } var result = WebBook(bookSource, false).exploreBook(ruleFindUrl, page) return returnData.setData(result) } suspend fun searchBook(context: RoutingContext): ReturnData { val returnData = ReturnData() // 如果登录了,就使用用户的书源 checkAuth(context) var bookSource = getBookSourceString(context) if (bookSource.isNullOrEmpty()) { return returnData.setErrorMsg("未配置书源") } val key: String var page: Int if (context.request().method() == HttpMethod.POST) { // post 请求 key = context.bodyAsJson.getString("key") page = context.bodyAsJson.getInteger("page", 1) } else { // get 请求 key = context.queryParam("key").firstOrNull() ?: "" page = context.queryParam("page").firstOrNull()?.toInt() ?: 1 } if (key.isNullOrEmpty()) { return returnData.setErrorMsg("请输入搜索关键字") } logger.info { "searchBook" } var result = WebBook(bookSource, appConfig.debugLog).searchBook(key, page) return returnData.setData(result) } suspend fun searchBookMulti(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var key: String var lastIndex: Int var searchSize: Int var bookSourceGroup: String var concurrentCount: Int if (context.request().method() == HttpMethod.POST) { // post 请求 key = context.bodyAsJson.getString("key", "") bookSourceGroup = context.bodyAsJson.getString("bookSourceGroup", "") lastIndex = context.bodyAsJson.getInteger("lastIndex", -1) searchSize = context.bodyAsJson.getInteger("searchSize", 20) concurrentCount = context.bodyAsJson.getInteger("concurrentCount", 36) } else { // get 请求 key = context.queryParam("key").firstOrNull() ?: "" bookSourceGroup = context.queryParam("bookSourceGroup").firstOrNull() ?: "" lastIndex = context.queryParam("lastIndex").firstOrNull()?.toInt() ?: -1 searchSize = context.queryParam("searchSize").firstOrNull()?.toInt() ?: 20 concurrentCount = context.queryParam("concurrentCount").firstOrNull()?.toInt() ?: 36 } var userNameSpace = getUserNameSpace(context) var userBookSourceList = loadBookSourceStringList(userNameSpace, bookSourceGroup) if (userBookSourceList.size <= 0) { return returnData.setErrorMsg("未配置书源") } if (key.isNullOrEmpty()) { return returnData.setErrorMsg("请输入搜索关键字") } if (lastIndex >= userBookSourceList.size - 1) { return returnData.setErrorMsg("没有更多了") } searchSize = if(searchSize > 0) searchSize else 20 concurrentCount = if(concurrentCount > 0) concurrentCount else 36 logger.info("searchBookMulti from lastIndex: {} searchSize: {}", lastIndex, searchSize) var isEnd = false context.request().connection().closeHandler{ logger.info("客户端已断开链接,停止 searchBookMulti") isEnd = true } var resultList = arrayListOf() var resultMap = mutableMapOf() val book = Book() book.name = key limitConcurrent(concurrentCount, lastIndex + 1, userBookSourceList.size, {it-> lastIndex = it var bookSource = userBookSourceList.get(it) searchBookWithSource(bookSource, book, false, userNameSpace = userNameSpace) }) {list, loopCount -> // logger.info("list: {}", list) list.forEach { val bookList = it as? Collection bookList?.forEach { book -> // 按照 书名 + 作者名 过滤 val bookKey = book.name + '_' + book.author if (!resultMap.containsKey(bookKey)) { resultList.add(book) resultMap.put(bookKey, 1) } } } logger.info("Loog: {} resultList.size: {}", loopCount, resultList.size) if (isEnd || loopCount >= concurrentLoopCount) { // 超过最大轮次,终止执行 false } else { resultList.size < searchSize } } return returnData.setData(mapOf("lastIndex" to lastIndex, "list" to resultList)) } suspend fun searchBookMultiSSE(context: RoutingContext) { val returnData = ReturnData() // 返回 event-stream val response = context.response().putHeader("Content-Type", "text/event-stream") .putHeader("Cache-Control", "no-cache") .setChunked(true); if (!checkAuth(context)) { response.write("event: error\n") response.end("data: " + jsonEncode(returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用"), false) + "\n\n") return } var key: String var lastIndex: Int var searchSize: Int var bookSourceGroup: String var concurrentCount: Int if (context.request().method() == HttpMethod.POST) { // post 请求 key = context.bodyAsJson.getString("key", "") bookSourceGroup = context.bodyAsJson.getString("bookSourceGroup", "") lastIndex = context.bodyAsJson.getInteger("lastIndex", -1) searchSize = context.bodyAsJson.getInteger("searchSize", 50) concurrentCount = context.bodyAsJson.getInteger("concurrentCount", 24) } else { // get 请求 key = context.queryParam("key").firstOrNull() ?: "" bookSourceGroup = context.queryParam("bookSourceGroup").firstOrNull() ?: "" lastIndex = context.queryParam("lastIndex").firstOrNull()?.toInt() ?: -1 searchSize = context.queryParam("searchSize").firstOrNull()?.toInt() ?: 50 concurrentCount = context.queryParam("concurrentCount").firstOrNull()?.toInt() ?: 24 } var userNameSpace = getUserNameSpace(context) var userBookSourceList = loadBookSourceStringList(userNameSpace, bookSourceGroup) if (userBookSourceList.size <= 0) { response.write("event: error\n") response.end("data: " + jsonEncode(returnData.setErrorMsg("未配置书源"), false) + "\n\n") return } if (key.isNullOrEmpty()) { response.write("event: error\n") response.end("data: " + jsonEncode(returnData.setErrorMsg("请输入搜索关键字"), false) + "\n\n") return } if (lastIndex >= userBookSourceList.size - 1) { response.write("event: error\n") response.end("data: " + jsonEncode(returnData.setErrorMsg("没有更多了"), false) + "\n\n") return } searchSize = if(searchSize > 0) searchSize else 50 concurrentCount = if(concurrentCount > 0) concurrentCount else 24 logger.info("searchBookMulti from lastIndex: {} concurrentCount: {} searchSize: {}", lastIndex, concurrentCount, searchSize) var isEnd = false context.request().connection().closeHandler{ logger.info("客户端已断开链接,停止 searchBookMultiSSE") isEnd = true } var resultList = arrayListOf() var resultMap = mutableMapOf() val book = Book() book.name = key limitConcurrent(concurrentCount, lastIndex + 1, userBookSourceList.size, {it-> lastIndex = it var bookSource = userBookSourceList.get(it) searchBookWithSource(bookSource, book, false, userNameSpace = userNameSpace) }) {list, loopCount -> // logger.info("list: {}", list) val loopResult = arrayListOf() list.forEach { val bookList = it as? Collection bookList?.forEach { book -> // 按照 书名 + 作者名 过滤 val bookKey = book.name + '_' + book.author if (!resultMap.containsKey(bookKey)) { resultList.add(book) loopResult.add(book) resultMap.put(bookKey, 1) } } } // 返回本轮数据 response.write("data: " + jsonEncode(mapOf("lastIndex" to lastIndex, "data" to loopResult), false) + "\n\n") logger.info("Loog: {} resultList.size: {}", loopCount, resultList.size) if (isEnd || loopCount >= concurrentLoopCount) { // 超过最大轮次,终止执行 false } else { resultList.size < searchSize } } response.write("event: end\n") response.end("data: " + jsonEncode(mapOf("lastIndex" to lastIndex), false) + "\n\n") } suspend fun searchBookSource(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var bookUrl: String var lastIndex: Int var searchSize: Int var bookSourceGroup: String if (context.request().method() == HttpMethod.POST) { // post 请求 bookUrl = context.bodyAsJson.getString("url") lastIndex = context.bodyAsJson.getInteger("lastIndex", -1) searchSize = context.bodyAsJson.getInteger("searchSize", 5) bookSourceGroup = context.bodyAsJson.getString("bookSourceGroup", "") } else { // get 请求 bookUrl = context.queryParam("url").firstOrNull() ?: "" lastIndex = context.queryParam("lastIndex").firstOrNull()?.toInt() ?: -1 searchSize = context.queryParam("searchSize").firstOrNull()?.toInt() ?: 5 bookSourceGroup = context.queryParam("bookSourceGroup").firstOrNull() ?: "" } var userNameSpace = getUserNameSpace(context) var userBookSourceList = loadBookSourceStringList(userNameSpace, bookSourceGroup) if (userBookSourceList.size <= 0) { return returnData.setErrorMsg("未配置书源") } if (bookUrl.isNullOrEmpty()) { return returnData.setErrorMsg("请输入书籍链接") } if (lastIndex >= userBookSourceList.size - 1) { return returnData.setErrorMsg("没有更多了") } var book = getShelfBookByURL(bookUrl, userNameSpace) if (book == null) { book = bookInfoCache.getAsString(bookUrl)?.toMap()?.toDataClass() } if (book == null) { return returnData.setErrorMsg("书籍信息错误") } // 校正 lastIndex var bookSourceList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, book.name + "_" + book.author, "bookSource")) if (bookSourceList != null && bookSourceList.size() > 0) { try { val lastBookSourceUrl = bookSourceList.getJsonObject(bookSourceList.size() - 1).getString("origin") lastIndex = Math.max(lastIndex, getBookSourceBySourceURL(lastBookSourceUrl, userNameSpace, userBookSourceList).second) } catch(e: Exception) { e.printStackTrace() } } logger.info("searchBookSource from lastIndex: {}", lastIndex) var isEnd = false context.request().connection().closeHandler{ logger.info("客户端已断开链接,停止 searchBookSource") isEnd = true } searchSize = if(searchSize > 0) searchSize else 5 var resultList = arrayListOf() var concurrentCount = Math.max(searchSize * 2, 24) limitConcurrent(concurrentCount, lastIndex + 1, userBookSourceList.size, {it-> lastIndex = it var bookSource = userBookSourceList.get(it) searchBookWithSource(bookSource, book, userNameSpace = userNameSpace) }) {list, loopCount -> // logger.info("list: {}", list) list.forEach { val bookList = it as? Collection bookList?.let { resultList.addAll(it) } } if (isEnd || loopCount >= concurrentLoopCount) { // 超过最大轮次,终止执行 false } else { resultList.size < searchSize } } saveBookSources(book, resultList, userNameSpace) return returnData.setData(mapOf("lastIndex" to lastIndex, "list" to resultList)) } suspend fun searchBookSourceSSE(context: RoutingContext) { val returnData = ReturnData() // 返回 event-stream val response = context.response().putHeader("Content-Type", "text/event-stream") .putHeader("Cache-Control", "no-cache") .setChunked(true); if (!checkAuth(context)) { response.write("event: error\n") response.end("data: " + jsonEncode(returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用"), false) + "\n\n") return } var bookUrl: String var lastIndex: Int var searchSize: Int var bookSourceGroup: String var refresh: Int = 0 if (context.request().method() == HttpMethod.POST) { // post 请求 bookUrl = context.bodyAsJson.getString("url") lastIndex = context.bodyAsJson.getInteger("lastIndex", -1) searchSize = context.bodyAsJson.getInteger("searchSize", 30) bookSourceGroup = context.bodyAsJson.getString("bookSourceGroup", "") refresh = context.bodyAsJson.getInteger("refresh", 0) } else { // get 请求 bookUrl = context.queryParam("url").firstOrNull() ?: "" lastIndex = context.queryParam("lastIndex").firstOrNull()?.toInt() ?: -1 searchSize = context.queryParam("searchSize").firstOrNull()?.toInt() ?: 30 bookSourceGroup = context.queryParam("bookSourceGroup").firstOrNull() ?: "" refresh = context.queryParam("refresh").firstOrNull()?.toInt() ?: 0 } var userNameSpace = getUserNameSpace(context) var userBookSourceList = loadBookSourceStringList(userNameSpace, bookSourceGroup) if (userBookSourceList.size <= 0) { response.write("event: error\n") response.end("data: " + jsonEncode(returnData.setErrorMsg("未配置书源"), false) + "\n\n") return } if (bookUrl.isNullOrEmpty()) { response.write("event: error\n") response.end("data: " + jsonEncode(returnData.setErrorMsg("请输入书籍链接"), false) + "\n\n") return } var book = getShelfBookByURL(bookUrl, userNameSpace) if (book == null) { book = bookInfoCache.getAsString(bookUrl)?.toMap()?.toDataClass() } if (book == null) { response.write("event: error\n") response.end("data: " + jsonEncode(returnData.setErrorMsg("书籍信息错误"), false) + "\n\n") return } // 校正 lastIndex var bookSourceList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, book.name + "_" + book.author, "bookSource")) // refresh > 0 校验书源最后一个书源作为lastIndex的可选项 if (refresh <= 0 && bookSourceList != null && bookSourceList.size() > 0) { try { val lastBookSourceUrl = bookSourceList.getJsonObject(bookSourceList.size() - 1).getString("origin") lastIndex = Math.max(lastIndex, getBookSourceBySourceURL(lastBookSourceUrl, userNameSpace, userBookSourceList).second) } catch(e: Exception) { e.printStackTrace() } } if (lastIndex >= userBookSourceList.size - 1) { response.write("event: error\n") response.end("data: " + jsonEncode(returnData.setData(mapOf("lastIndex" to lastIndex)).setErrorMsg("没有更多了"), false) + "\n\n") return } searchSize = if(searchSize > 0) searchSize else 30 var resultList = arrayListOf() var concurrentCount = Math.max(searchSize * 2, 24) logger.info("searchBookMulti from lastIndex: {} concurrentCount: {} searchSize: {}", lastIndex, concurrentCount, searchSize) var isEnd = false context.request().connection().closeHandler{ logger.info("客户端已断开链接,停止 searchBookSourceSSE") isEnd = true } limitConcurrent(concurrentCount, lastIndex + 1, userBookSourceList.size, {it-> lastIndex = it var bookSource = userBookSourceList.get(it) searchBookWithSource(bookSource, book, userNameSpace = userNameSpace) }) {list, loopCount -> // logger.info("list: {}", list) val loopResult = arrayListOf() list.forEach { val bookList = it as? Collection bookList?.let { resultList.addAll(it) loopResult.addAll(it) } } // 返回本轮数据 response.write("data: " + jsonEncode(mapOf("lastIndex" to lastIndex, "data" to loopResult), false) + "\n\n") logger.info("Loog: {} resultList.size: {}", loopCount, resultList.size) if (isEnd || loopCount >= concurrentLoopCount) { // 超过最大轮次,终止执行 false } else { resultList.size < searchSize } } saveBookSources(book, resultList, userNameSpace) response.write("event: end\n") response.end("data: " + jsonEncode(mapOf("lastIndex" to lastIndex), false) + "\n\n") } suspend fun searchBookWithSource(bookSourceString: String, book: Book, accurate: Boolean = true, userNameSpace: String = "default"): ArrayList { var resultList = arrayListOf() var bookSource = asJsonObject(bookSourceString)?.mapTo(BookSource::class.java) if (bookSource == null) { return resultList; } if (isInvalidBookSource(bookSource, userNameSpace)) { return resultList; } withContext(Dispatchers.IO) { // val costTime = measureTimeMillis { try { val start = System.currentTimeMillis() var result = WebBook(bookSourceString, false).searchBook(book.name, 1) val end = System.currentTimeMillis() if (result.size > 0) { for (j in 0 until result.size) { var _book = result.get(j) if (accurate && _book.name.equals(book.name) && _book.author.equals(book.author)) { _book.time = end - start resultList.add(_book) } else if (!accurate && (_book.name.indexOf(book.name, ignoreCase=true) >= 0 || _book.author.indexOf(book.name, ignoreCase=true) >= 0)) { _book.time = end - start resultList.add(_book) } } } } catch(e: Exception) { // 标记为失败源 val info = mutableMapOf("sourceUrl" to bookSource.bookSourceUrl, "time" to System.currentTimeMillis(), "error" to e.toString()) addInvalidBookSource(bookSource.bookSourceUrl, info, userNameSpace) e.printStackTrace() } // } // logger.info("searchBookWithSource in Thread: {} Cost: {}", Thread.currentThread().name, costTime) } return resultList; } suspend fun getAvailableBookSource(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var bookUrl: String var refresh: Int = 0 if (context.request().method() == HttpMethod.POST) { // post 请求 bookUrl = context.bodyAsJson.getString("url") refresh = context.bodyAsJson.getInteger("refresh", 0) } else { // get 请求 bookUrl = context.queryParam("url").firstOrNull() ?: "" refresh = context.queryParam("refresh").firstOrNull()?.toInt() ?: 0 } if (bookUrl.isNullOrEmpty()) { return returnData.setErrorMsg("请输入书籍链接") } var userNameSpace = getUserNameSpace(context) var book = getShelfBookByURL(bookUrl, userNameSpace) if (book == null) { book = bookInfoCache.getAsString(bookUrl)?.toMap()?.toDataClass() } if (book == null) { return returnData.setErrorMsg("书籍信息错误") } var bookSourceList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, book.name + "_" + book.author, "bookSource")) if (bookSourceList != null && bookSourceList.size() > 0) { if (refresh <= 0) { return returnData.setData(bookSourceList.getList()) } // 刷新源 var resultList = arrayListOf() val concurrentCount = 16 val userBookSourceStringList = loadBookSourceStringList(userNameSpace) limitConcurrent(concurrentCount, 0, bookSourceList.size(), {it -> var searchBook = bookSourceList.getJsonObject(it).mapTo(SearchBook::class.java) if (searchBook.origin.equals("loc_book")) { arrayListOf(searchBook) } else { var bookSource = getBookSourceStringBySourceURL(searchBook.origin, userNameSpace, userBookSourceStringList) if (bookSource != null) { searchBookWithSource(bookSource, book, userNameSpace = userNameSpace) } else { arrayListOf() } } }) {list, _-> // logger.info("list: {}", list) list.forEach { val bookList = it as? Collection bookList?.let { resultList.addAll(it) } } true } // logger.info("refreshed bookSourceList: {}", resultList) saveBookSources(book, resultList, userNameSpace, true) return returnData.setData(resultList) } return returnData.setData(arrayListOf()) } suspend fun getBookshelf(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var refresh: Int = 0 if (context.request().method() == HttpMethod.POST) { // post 请求 refresh = context.bodyAsJson.getInteger("refresh", 0) } else { // get 请求 refresh = context.queryParam("refresh").firstOrNull()?.toInt() ?: 0 } var bookList = getBookShelfBooks(refresh > 0, getUserNameSpace(context)) return returnData.setData(bookList) } suspend fun getShelfBook(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var url: String if (context.request().method() == HttpMethod.POST) { // post 请求 url = context.bodyAsJson.getString("url") } else { // get 请求 url = context.queryParam("url").firstOrNull() ?: "" } if (url.isNullOrEmpty()) { return returnData.setErrorMsg("书源链接不能为空") } var book = getShelfBookByURL(url, getUserNameSpace(context)) if (book == null) { return returnData.setErrorMsg("书籍不存在") } return returnData.setData(book) } suspend fun saveBook(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var book = context.bodyAsJson.mapTo(Book::class.java) if (book.origin.isNullOrEmpty()) { return returnData.setErrorMsg("未找到书源信息") } if (book.bookUrl.isNullOrEmpty()) { return returnData.setErrorMsg("书籍链接不能为空") } var userNameSpace = getUserNameSpace(context) var bookshelf: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "bookshelf")) if (bookshelf == null) { bookshelf = JsonArray() } // 遍历判断书本是否存在 var existIndex: Int = -1 for (i in 0 until bookshelf.size()) { var _book = bookshelf.getJsonObject(i).mapTo(Book::class.java) if (_book.name.equals(book.name) && _book.author.equals(book.author)) { existIndex = i break; } } if (existIndex < 0) { // 判断书籍是否超过限制 if (bookshelf.size() >= appConfig.userBookLimit) { return returnData.setErrorMsg("超过用户书籍数上限") } } // 导入本地书籍 if (book.isLocalBook()) { if (book.bookUrl.startsWith("/assets/")) { // 临时文件,移动到书籍目录 // storage/assets/default/book/《极道天魔》(校对版全本)作者:滚开/《极道天魔》(校对版全本)作者:滚开.txt val tempFile = File(getWorkDir("storage" + book.bookUrl)) if (!tempFile.exists()) { return returnData.setErrorMsg("上传书籍不存在") } val relativeLocalFilePath = Paths.get("storage", "data", userNameSpace, book.name + "_" + book.author, tempFile.name).toString() val localFilePath = getWorkDir(relativeLocalFilePath) logger.info("localFilePath: {}", localFilePath) var localFile = File(localFilePath) localFile.deleteRecursively() if (!localFile.parentFile.exists()) { localFile.parentFile.mkdirs() } if (!tempFile.copyRecursively(localFile)) { return returnData.setErrorMsg("导入本地书籍失败") } tempFile.deleteRecursively() // 修改书籍信息 book.bookUrl = relativeLocalFilePath book.originName = relativeLocalFilePath if (book.isEpub()) { // 解压文件 index.epub if (!extractEpub(book)) { return returnData.setErrorMsg("导入本地Epub书籍失败") } } else if (book.isCbz()) { // 解压文件 index.cbz if (!extractCbz(book)) { return returnData.setErrorMsg("导入本地CBZ书籍失败") } } } else if (book.bookUrl.indexOf("storage/localStore") >= 0) { // 本地书仓,不用移动书籍 val tempFile = File(getWorkDir(book.bookUrl)) if (!tempFile.exists()) { return returnData.setErrorMsg("本地书仓书籍不存在") } val relativeLocalFilePath = Paths.get("storage", "data", userNameSpace, book.name + "_" + book.author, tempFile.name).toString() book.bookUrl = relativeLocalFilePath if (book.isEpub()) { // 解压文件 index.epub if (!extractEpub(book)) { return returnData.setErrorMsg("导入本地Epub书籍失败") } } else if (book.isCbz()) { // 解压文件 index.cbz if (!extractCbz(book)) { return returnData.setErrorMsg("导入本地CBZ书籍失败") } } } else if (book.bookUrl.indexOf("webdav/") >= 0) { // webdav书仓,不用移动书籍 val tempFile = File(getWorkDir(book.bookUrl)) if (!tempFile.exists()) { return returnData.setErrorMsg("webdav书仓书籍不存在") } val relativeLocalFilePath = Paths.get("storage", "data", userNameSpace, book.name + "_" + book.author, tempFile.name).toString() book.bookUrl = relativeLocalFilePath if (book.isEpub()) { // 解压文件 index.epub if (!extractEpub(book)) { return returnData.setErrorMsg("导入本地Epub书籍失败") } } else if (book.isCbz()) { // 解压文件 index.cbz if (!extractCbz(book)) { return returnData.setErrorMsg("导入本地CBZ书籍失败") } } } } else if (book.tocUrl.isNullOrEmpty()) { // 补全书籍信息 var bookSource = getBookSourceStringBySourceURL(book.origin, userNameSpace) if (bookSource == null) { return returnData.setErrorMsg("书源信息错误") } var newBook = WebBook(bookSource, appConfig.debugLog).getBookInfo(book.bookUrl) book.fillData(newBook, listOf("name", "author", "coverUrl", "tocUrl", "intro", "latestChapterTitle", "wordCount")) } book = mergeBookCacheInfo(book) if (existIndex >= 0) { var bookList = bookshelf.getList() var existBook = bookshelf.getJsonObject(existIndex).mapTo(Book::class.java) book.durChapterIndex = existBook.durChapterIndex book.durChapterTitle = existBook.durChapterTitle book.durChapterTime = existBook.durChapterTime bookList.set(existIndex, JsonObject.mapFrom(book)) bookshelf = JsonArray(bookList) } else { bookshelf.add(JsonObject.mapFrom(book)) } // 保存书源信息 val sourceList = listOf(book.toSearchBook()) saveBookSources(book, sourceList, userNameSpace) // logger.info("bookshelf: {}", bookshelf) saveUserStorage(userNameSpace, "bookshelf", bookshelf) return returnData.setData(book) } suspend fun setBookSource(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var bookUrl: String var newBookUrl: String var bookSourceUrl: String if (context.request().method() == HttpMethod.POST) { // post 请求 bookUrl = context.bodyAsJson.getString("bookUrl") newBookUrl = context.bodyAsJson.getString("newUrl") bookSourceUrl = context.bodyAsJson.getString("bookSourceUrl") } else { // get 请求 bookUrl = context.queryParam("bookUrl").firstOrNull() ?: "" newBookUrl = context.queryParam("newUrl").firstOrNull() ?: "" bookSourceUrl = context.queryParam("bookSourceUrl").firstOrNull() ?: "" } if (bookUrl.isNullOrEmpty()) { return returnData.setErrorMsg("书籍链接不能为空") } if (newBookUrl.isNullOrEmpty()) { return returnData.setErrorMsg("新源书籍链接不能为空") } if (bookSourceUrl.isNullOrEmpty()) { return returnData.setErrorMsg("书源链接不能为空") } var userNameSpace = getUserNameSpace(context) var book = getShelfBookByURL(bookUrl, userNameSpace) if (book == null) { return returnData.setErrorMsg("书籍信息错误") } // 查找是否存在该书源 var bookSourceString = getBookSourceStringBySourceURL(bookSourceUrl, userNameSpace) var searchBook: Book? = null if (bookSourceString.isNullOrEmpty()) { // 判断是不是本地书籍 val localBookSourceList = asJsonArray(getUserStorage(userNameSpace, book.name + "_" + book.author, "bookSource")) // 遍历判断书本是否存在 if (localBookSourceList != null) { for (i in 0 until localBookSourceList.size()) { var _searchBook = localBookSourceList.getJsonObject(i).mapTo(SearchBook::class.java) if (_searchBook.bookUrl.equals(newBookUrl)) { searchBook = _searchBook.toBook() break; } } } if (searchBook == null) { return returnData.setErrorMsg("书源信息错误") } } var newBookInfo = if (searchBook != null) { searchBook } else { if (bookSourceString.isNullOrEmpty()) { return returnData.setErrorMsg("书源信息错误") } WebBook(bookSourceString, appConfig.debugLog).getBookInfo(newBookUrl) } editShelfBook(book, userNameSpace) { existBook -> existBook.origin = newBookInfo.origin existBook.originName = newBookInfo.originName existBook.bookUrl = newBookInfo.bookUrl existBook.tocUrl = newBookInfo.tocUrl if (existBook.coverUrl.isNullOrEmpty() && !newBookInfo.coverUrl.isNullOrEmpty()) { existBook.coverUrl = newBookInfo.coverUrl } logger.info("setBookSource: {}", existBook) newBookInfo = existBook existBook } // 更新目录 getLocalChapterList(newBookInfo, bookSourceString ?: "", true, userNameSpace) return returnData.setData(newBookInfo) } suspend fun saveBookGroupId(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var bookUrl: String var groupId: Int if (context.request().method() == HttpMethod.POST) { // post 请求 bookUrl = context.bodyAsJson.getString("bookUrl") groupId = context.bodyAsJson.getInteger("groupId", 0) } else { // get 请求 bookUrl = context.queryParam("bookUrl").firstOrNull() ?: "" groupId = context.queryParam("groupId").firstOrNull()?.toInt() ?: 0 } if (bookUrl.isNullOrEmpty()) { return returnData.setErrorMsg("书籍链接不能为空") } var userNameSpace = getUserNameSpace(context) var book = getShelfBookByURL(bookUrl, userNameSpace) if (book == null) { return returnData.setErrorMsg("书籍信息错误") } if (groupId <= 0) { return returnData.setErrorMsg("分组信息错误") } editShelfBook(book, userNameSpace) { existBook -> existBook.group = groupId logger.info("saveBookGroupId: {}", existBook) existBook } book.group = groupId return returnData.setData(book) } suspend fun addBookGroupMulti(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val groupId: Int = context.bodyAsJson.getInteger("groupId", 0) if (groupId <= 0) { return returnData.setErrorMsg("分组信息错误") } var userNameSpace = getUserNameSpace(context) val bookJsonArray = context.bodyAsJson.getJsonArray("bookList", JsonArray()) for (k in 0 until bookJsonArray.size()) { var book = bookJsonArray.getJsonObject(k).mapTo(Book::class.java) editShelfBook(book, userNameSpace) { existBook -> existBook.group = existBook.group or groupId logger.info("saveBookGroupId: {}", existBook) existBook } } return returnData.setData("") } suspend fun removeBookGroupMulti(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val groupId: Int = context.bodyAsJson.getInteger("groupId", 0) if (groupId <= 0) { return returnData.setErrorMsg("分组信息错误") } var userNameSpace = getUserNameSpace(context) val bookJsonArray = context.bodyAsJson.getJsonArray("bookList", JsonArray()) for (k in 0 until bookJsonArray.size()) { var book = bookJsonArray.getJsonObject(k).mapTo(Book::class.java) editShelfBook(book, userNameSpace) { existBook -> existBook.group = existBook.group xor groupId logger.info("saveBookGroupId: {}", existBook) existBook } } return returnData.setData("") } suspend fun deleteBook(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var book = context.bodyAsJson.mapTo(Book::class.java) var userNameSpace = getUserNameSpace(context) var bookshelf: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "bookshelf")) if (bookshelf == null) { bookshelf = JsonArray() } // 遍历判断书本是否存在 var existIndex: Int = -1 for (i in 0 until bookshelf.size()) { var _book = bookshelf.getJsonObject(i).mapTo(Book::class.java) if (_book.bookUrl.equals(book.bookUrl)) { existIndex = i book = _book break; } if (_book.name.equals(book.name) && _book.author.equals(book.author)) { existIndex = i book = _book break; } } if (existIndex < 0) { return returnData.setErrorMsg("书架书籍不存在") } bookshelf.remove(existIndex) // logger.info("bookshelf: {}", bookshelf) saveUserStorage(userNameSpace, "bookshelf", bookshelf) // 删除书籍目录 val localBookPath = File(getWorkDir("storage", "data", userNameSpace, book.name + "_" + book.author)) localBookPath.deleteRecursively() return returnData.setData("删除书籍成功") } suspend fun deleteBooks(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val bookJsonArray = context.bodyAsJsonArray var userNameSpace = getUserNameSpace(context) var bookshelf: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "bookshelf")) if (bookshelf == null) { bookshelf = JsonArray() } for (k in 0 until bookJsonArray.size()) { var book = bookJsonArray.getJsonObject(k).mapTo(Book::class.java) // 遍历判断书本是否存在 var existIndex: Int = -1 for (i in 0 until bookshelf.size()) { var _book = bookshelf.getJsonObject(i).mapTo(Book::class.java) if (_book.bookUrl.equals(book.bookUrl)) { existIndex = i book = _book break; } if (_book.name.equals(book.name) && _book.author.equals(book.author)) { existIndex = i book = _book break; } } if (existIndex >= 0) { bookshelf.remove(existIndex) } // 删除书籍目录 val localBookPath = File(getWorkDir("storage", "data", userNameSpace, book.name + "_" + book.author)) localBookPath.deleteRecursively() } saveUserStorage(userNameSpace, "bookshelf", bookshelf) return returnData.setData("") } suspend fun getBookGroups(context: RoutingContext): ReturnData { val returnData = ReturnData() checkAuth(context) if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var userNameSpace = getUserNameSpace(context) var bookGroupList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "bookGroup")) if (bookGroupList == null) { bookGroupList = asJsonArray(""" [{"groupId":-1,"groupName":"全部","order":-10,"show":true},{"groupId":-2,"groupName":"本地","order":-9,"show":true},{"groupId":-3,"groupName":"音频","order":-8,"show":true},{"groupId":-4,"groupName":"未分组","order":-7,"show":true}] """) if (bookGroupList == null) { return returnData.setData(arrayListOf()) } saveUserStorage(userNameSpace, "bookGroup", bookGroupList) } return returnData.setData(bookGroupList.getList()) } suspend fun saveBookGroup(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val bookGroup = context.bodyAsJson.mapTo(BookGroup::class.java) if (bookGroup.groupName.isEmpty()) { return returnData.setErrorMsg("分组名称不能为空") } var userNameSpace = getUserNameSpace(context) var bookGroupList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "bookGroup")) if (bookGroupList == null) { bookGroupList = JsonArray() } // 遍历判断书本是否存在 var existIndex: Int = -1 for (i in 0 until bookGroupList.size()) { var _bookGroup = bookGroupList.getJsonObject(i).mapTo(BookGroup::class.java) if (_bookGroup.groupId.equals(bookGroup.groupId)) { existIndex = i break; } } if (existIndex >= 0) { var groupList = bookGroupList.getList() groupList.set(existIndex, JsonObject.mapFrom(bookGroup)) bookGroupList = JsonArray(groupList) } else { // 新增分组 if (bookGroup.groupId >= 0) { var maxOrder = 0; val idsSum = bookGroupList.sumBy{ val id = asJsonObject(it)?.getInteger("groupId", 0) ?: 0 val order = asJsonObject(it)?.getInteger("order", 0) ?: 0 maxOrder = if (order > maxOrder) order else maxOrder if (id > 0) id else 0 } var id = 1 while (id and idsSum != 0) { id = id.shl(1) } bookGroup.groupId = id bookGroup.order = maxOrder + 1 } bookGroupList.add(JsonObject.mapFrom(bookGroup)) } // logger.info("bookGroupList: {}", bookGroupList) saveUserStorage(userNameSpace, "bookGroup", bookGroupList) return returnData.setData("") } suspend fun saveBookGroupOrder(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val bookGroupOrder = context.bodyAsJson.getJsonArray("order", null) if (bookGroupOrder == null) { return returnData.setErrorMsg("参数错误") } var userNameSpace = getUserNameSpace(context) var bookGroupList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "bookGroup")) if (bookGroupList == null) { bookGroupList = JsonArray() } var orderMap: MutableMap = mutableMapOf() for (i in 0 until bookGroupOrder.size()) { orderMap.put(bookGroupOrder.getJsonObject(i).getInteger("groupId"), bookGroupOrder.getJsonObject(i).getInteger("order")) } // 遍历判断书本是否存在 var groupList = bookGroupList.getList() for (i in 0 until bookGroupList.size()) { var bookGroup = bookGroupList.getJsonObject(i).mapTo(BookGroup::class.java) if (orderMap.containsKey(bookGroup.groupId)) { bookGroup.order = orderMap.get(bookGroup.groupId) as? Int ?: bookGroup.order groupList.set(i, JsonObject.mapFrom(bookGroup)) } } bookGroupList = JsonArray(groupList) // logger.info("bookGroupList: {}", bookGroupList) saveUserStorage(userNameSpace, "bookGroup", bookGroupList) return returnData.setData("") } suspend fun deleteBookGroup(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val bookgroup = context.bodyAsJson.mapTo(BookGroup::class.java) var userNameSpace = getUserNameSpace(context) var bookGroupList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "bookGroup")) if (bookGroupList == null) { bookGroupList = JsonArray() } // 遍历判断分组是否存在 var existIndex: Int = -1 for (i in 0 until bookGroupList.size()) { var _bookGroup = bookGroupList.getJsonObject(i).mapTo(BookGroup::class.java) if (_bookGroup.groupId.equals(bookgroup.groupId)) { existIndex = i break; } } if (existIndex >= 0) { bookGroupList.remove(existIndex) } // logger.info("bookGroup: {}", bookGroup) saveUserStorage(userNameSpace, "bookGroup", bookGroupList) return returnData.setData("") } suspend fun saveBookInfoCache(bookList: List): List { if (bookList.size > 0) { for (i in 0 until bookList.size) { var book = bookList.get(i) bookInfoCache.put(book.bookUrl, jsonEncode(JsonObject.mapFrom(book).map)) } saveStorage("cache", "bookInfoCache", value = bookInfoCache) } return bookList } suspend fun mergeBookCacheInfo(book: Book): Book { var cacheInfo: Book? = bookInfoCache.getAsString(book.bookUrl)?.toMap()?.toDataClass() if (cacheInfo != null) { return book.fillData(cacheInfo, listOf("name", "author", "coverUrl", "tocUrl", "intro", "latestChapterTitle", "wordCount")) } return book } suspend fun getBookShelfBooks(refresh: Boolean = false, userNameSpace: String): List { var bookshelf: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "bookshelf")) if (bookshelf == null) { return arrayListOf() } if (bookshelf.size() == 0) { return arrayListOf() } var bookList = arrayListOf() val concurrentCount = 16 val userBookSourceStringList = loadBookSourceStringList(userNameSpace) val mutex = Mutex() limitConcurrent(concurrentCount, 0, bookshelf.size()) { var book = bookshelf.getJsonObject(it).mapTo(Book::class.java) if (!book.isLocalBook() && book.canUpdate && refresh) { try { var bookSource = getBookSourceStringBySourceURL(book.origin, userNameSpace, userBookSourceStringList) if (bookSource != null) { withContext(Dispatchers.IO) { var bookChapterList = getLocalChapterList(book, bookSource, refresh, userNameSpace, false, mutex) if (bookChapterList.size > 0) { var bookChapter = bookChapterList.last() book.latestChapterTitle = bookChapter.title } if (bookChapterList.size - book.totalChapterNum > 0) { book.lastCheckTime = System.currentTimeMillis() book.lastCheckCount = bookChapterList.size - book.totalChapterNum } book.totalChapterNum = bookChapterList.size } } } catch(e: Exception) { e.printStackTrace() } } bookList.add(book) } return bookList } suspend fun getLocalChapterList(book: Book, bookSource: String, refresh: Boolean = false, userNameSpace: String, debugLog: Boolean = true, mutex: Mutex? = null): List { val md5Encode = MD5Utils.md5Encode(book.bookUrl).toString() var chapterList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, book.name + "_" + book.author, md5Encode)) if (chapterList == null || refresh) { var newChapterList: List book.setRootDir(getWorkDir()) book.setUserNameSpace(userNameSpace) if (book.isLocalBook()) { // 重新解压epub文件 if (book.isEpub() && !extractEpub(book, refresh)) { throw Exception("Epub书籍解压失败") } // 重新解压cbz文件 if (book.isCbz() && !extractCbz(book, refresh)) { throw Exception("CBZ书籍解压失败") } newChapterList = LocalBook.getChapterList(book) } else { try { newChapterList = WebBook(bookSource, debugLog).getChapterList(book) } catch(e: Exception) { if (!bookSource.isNullOrEmpty()) { var bookSourceObject = asJsonObject(bookSource)?.mapTo(BookSource::class.java) if (bookSourceObject != null) { // 标记为失败源 val info = mutableMapOf("sourceUrl" to bookSourceObject.bookSourceUrl, "time" to System.currentTimeMillis(), "error" to e.toString()) addInvalidBookSource(bookSourceObject.bookSourceUrl, info, userNameSpace) } } throw e } } saveUserStorage(userNameSpace, getRelativePath(book.name + "_" + book.author, md5Encode), newChapterList) saveShelfBookLatestChapter(book, newChapterList, userNameSpace, mutex) return newChapterList } var localChapterList = arrayListOf() for (i in 0 until chapterList.size()) { var _chapter = chapterList.getJsonObject(i).mapTo(BookChapter::class.java) localChapterList.add(_chapter) } return localChapterList } suspend fun getBookSourceString(context: RoutingContext, sourceUrl: String = ""): String? { var bookSourceString: String? = null if (context.request().method() == HttpMethod.POST) { var bookSource = context.bodyAsJson.getJsonObject("bookSource") if (bookSource != null) { bookSourceString = bookSource.toString() } } var userNameSpace = getUserNameSpace(context) if (bookSourceString.isNullOrEmpty()) { var bookSourceUrl: String if (context.request().method() == HttpMethod.POST) { bookSourceUrl = context.bodyAsJson.getString("bookSourceUrl", "") } else { bookSourceUrl = context.queryParam("bookSourceUrl").firstOrNull() ?: "" } bookSourceString = getBookSourceStringBySourceURL(bookSourceUrl, userNameSpace) } if (bookSourceString.isNullOrEmpty() && !sourceUrl.isNullOrEmpty()) { bookSourceString = getBookSourceStringBySourceURL(sourceUrl, userNameSpace) } return bookSourceString } fun loadBookSourceStringList(userNameSpace: String, bookSourceGroup: String = ""): List { var bookSourceList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "bookSource")) var userBookSourceList = arrayListOf() if (bookSourceList != null) { for (i in 0 until bookSourceList.size()) { var isAdd = true if (!bookSourceGroup.isEmpty()) { val bookSource = bookSourceList.getJsonObject(i).mapTo(BookSource::class.java) if (!bookSource.bookSourceGroup.equals(bookSourceGroup)) { isAdd = false } } if (isAdd) { userBookSourceList.add(bookSourceList.getJsonObject(i).toString()) } } } return userBookSourceList } fun getBookSourceStringBySourceURL(sourceUrl: String, userNameSpace: String, bookSourceList: List? = null): String? { var bookSourcePair = getBookSourceBySourceURL(sourceUrl, userNameSpace, bookSourceList) return bookSourcePair.first } fun getBookSourceBySourceURL(sourceUrl: String, userNameSpace: String, bookSourceList: List? = null): Pair { var bookSourceString: String? = null var index: Int = -1 if (sourceUrl.isNullOrEmpty()) { return Pair(bookSourceString, index) } // 优先查找用户的书源 var userBookSourceList = bookSourceList ?: loadBookSourceStringList(userNameSpace) for (i in 0 until userBookSourceList.size) { val sourceMap = userBookSourceList.get(i).toMap() if (sourceUrl.equals(sourceMap.get("bookSourceUrl") as String)) { bookSourceString = userBookSourceList.get(i) index = i break; } } return Pair(bookSourceString, index) } fun getShelfBookByURL(url: String, userNameSpace: String): Book? { if (url.isEmpty()) { return null } var bookshelf: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "bookshelf")) if (bookshelf == null) { return null } for (i in 0 until bookshelf.size()) { var _book = bookshelf.getJsonObject(i).mapTo(Book::class.java) if (_book.bookUrl.equals(url)) { _book.setRootDir(getWorkDir()) _book.setUserNameSpace(userNameSpace) return _book } } return null } fun saveShelfBookProgress(book: Book, bookChapter: BookChapter, userNameSpace: String) { editShelfBook(book, userNameSpace) { existBook -> existBook.durChapterIndex = bookChapter.index existBook.durChapterTitle = bookChapter.title existBook.durChapterTime = System.currentTimeMillis() // logger.info("saveShelfBookProgress: {}", existBook) existBook } } suspend fun saveShelfBookLatestChapter(book: Book, bookChapterList: List, userNameSpace: String, mutex: Mutex? = null) { try { mutex?.lock() editShelfBook(book, userNameSpace) { existBook -> if (bookChapterList.size > 0) { var bookChapter = bookChapterList.last() existBook.latestChapterTitle = bookChapter.title } if (bookChapterList.size - existBook.totalChapterNum > 0) { existBook.lastCheckCount = bookChapterList.size - existBook.totalChapterNum existBook.lastCheckTime = System.currentTimeMillis() } existBook.totalChapterNum = bookChapterList.size // TODO 最新章节更新时间 // existBook.latestChapterTime = System.currentTimeMillis() // logger.info("saveShelfBookLatestChapter: {}", existBook) existBook } } finally { mutex?.unlock() } } fun editShelfBook(book: Book, userNameSpace: String, handler: (Book)->Book) { var bookshelf: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "bookshelf")) if (bookshelf == null) { bookshelf = JsonArray() } // 遍历判断书本是否存在 var existIndex: Int = -1 for (i in 0 until bookshelf.size()) { var _book = bookshelf.getJsonObject(i).mapTo(Book::class.java) // 根据书籍链接查找 if (book.bookUrl.isNotEmpty() && _book.bookUrl.equals(book.bookUrl)) { existIndex = i break; } // 根据作者和书名查找 if (book.name.isNotEmpty() && _book.name.equals(book.name) && book.author.isNotEmpty() && _book.author.equals(book.author)) { existIndex = i break; } } if (existIndex >= 0) { var bookList = bookshelf.getList() var existBook = bookshelf.getJsonObject(existIndex).mapTo(Book::class.java) existBook = handler(existBook) // logger.info("editShelfBook: {}", existBook) bookList.set(existIndex, JsonObject.mapFrom(existBook)) bookshelf = JsonArray(bookList) saveUserStorage(userNameSpace, "bookshelf", bookshelf) } } fun saveBookSources(book: Book, sourceList: List, userNameSpace: String, replace: Boolean = false) { if (book.name.isEmpty()) { return; } var bookSourceList = JsonArray() if (!replace) { val localBookSourceList = asJsonArray(getUserStorage(userNameSpace, book.name + "_" + book.author, "bookSource")) if (localBookSourceList != null) { bookSourceList = localBookSourceList } } for (k in 0 until sourceList.size) { var searchBook = sourceList.get(k) // 遍历判断书本是否存在 var existIndex: Int = -1 for (i in 0 until bookSourceList.size()) { var _searchBook = bookSourceList.getJsonObject(i).mapTo(SearchBook::class.java) if (_searchBook.bookUrl.equals(searchBook.bookUrl)) { existIndex = i break; } } if (existIndex >= 0) { var _sourceList = bookSourceList.getList() _sourceList.set(existIndex, JsonObject.mapFrom(searchBook)) bookSourceList = JsonArray(_sourceList) } else { bookSourceList.add(JsonObject.mapFrom(searchBook)) } } // logger.info("bookSourceList: {}", bookSourceList) saveUserStorage(userNameSpace, getRelativePath(book.name + "_" + book.author, "bookSource"), bookSourceList) } fun extractEpub(book: Book, force: Boolean = false): Boolean { val epubExtractDir = File(getWorkDir(book.bookUrl + File.separator + "index")) if (force || !epubExtractDir.exists()) { epubExtractDir.deleteRecursively() var localEpubFile = File(getWorkDir(book.originName + File.separator + "index.epub")) if (book.originName.indexOf("localStore") > 0) { // 本地书仓的源文件 localEpubFile = File(getWorkDir(book.originName)) } if (book.originName.indexOf("webdav") > 0) { // webdav 书仓的源文件 localEpubFile = File(getWorkDir(book.originName)) } if (!localEpubFile.unzip(epubExtractDir.toString())) { return false } } return true } fun extractCbz(book: Book, force: Boolean = false): Boolean { val extractDir = File(getWorkDir(book.bookUrl + File.separator + "index")) if (force || !extractDir.exists()) { extractDir.deleteRecursively() var localFile = File(getWorkDir(book.originName + File.separator + "index.cbz")) if (book.originName.indexOf("localStore") > 0) { // 本地书仓的源文件 localFile = File(getWorkDir(book.originName)) } if (book.originName.indexOf("webdav") > 0) { // webdav 书仓的源文件 localFile = File(getWorkDir(book.originName)) } if (!localFile.unzip(extractDir.toString())) { return false } } return true } suspend fun syncBookProgressFromWebdav(progressFilePath: Any, userNameSpace: String) { var progressFile: File? = null when (progressFilePath) { is File -> progressFile = progressFilePath is String -> progressFile = File(progressFilePath) } if (progressFile == null) { return } var book = asJsonObject(progressFile.readText())?.mapTo(Book::class.java) if (book != null) { editShelfBook(book, userNameSpace) { existBook -> existBook.durChapterIndex = book.durChapterIndex existBook.durChapterPos = book.durChapterPos existBook.durChapterTime = book.durChapterTime existBook.durChapterTitle = book.durChapterTitle logger.info("syncShelfBookProgress: {}", existBook) existBook } } } suspend fun saveBookProgressToWebdav(book: Book, bookChapter: BookChapter, userNameSpace: String) { val userHome = getUserWebdavHome(userNameSpace) var bookProgressDir = File(userHome + File.separator + "bookProgress") if (!bookProgressDir.exists()) { bookProgressDir = File(userHome + File.separator + "legado" + File.separator + "bookProgress") if (!bookProgressDir.exists()) { return } } var progressFile = File(bookProgressDir.toString() + File.separator + book.name + "_" + book.author + ".json") progressFile.writeText(jsonEncode(mapOf( "name" to book.name, "author" to book.author, "durChapterIndex" to bookChapter.index, "durChapterPos" to 0, "durChapterTime" to System.currentTimeMillis(), "durChapterTitle" to bookChapter.title ), true)) } suspend fun syncFromWebdav(zipFilePath: String, userNameSpace: String): Boolean { var descDir = getWorkDir("storage", "data", userNameSpace, "tmp") var descDirFile = File(descDir) try { val userHome = getUserWebdavHome(userNameSpace) var zipFile = File(zipFilePath) if (!zipFile.exists()) { return false } descDirFile.deleteRecursively() if (zipFile.unzip(descDir)) { // 同步 书源 val bookSourceFile = File(descDir + File.separator + "bookSource.json") if (bookSourceFile.exists()) { val userBookSourceFile = File(getWorkDir("storage", "data", userNameSpace, "bookSource.json")) userBookSourceFile.deleteRecursively() bookSourceFile.renameTo(userBookSourceFile) } // 同步 书架 val bookshelfFile = File(descDir + File.separator + "bookshelf.json") if (bookshelfFile.exists()) { val userBookSourceFile = File(getWorkDir("storage", "data", userNameSpace, "bookshelf.json")) userBookSourceFile.deleteRecursively() bookshelfFile.renameTo(userBookSourceFile) } // 同步 书籍分组 val bookGroupFile = File(descDir + File.separator + "bookGroup.json") if (bookGroupFile.exists()) { val userBookGroupFile = File(getWorkDir("storage", "data", userNameSpace, "bookGroup.json")) userBookGroupFile.deleteRecursively() bookGroupFile.renameTo(userBookGroupFile) } // 同步 RSS订阅 val rssSourcesFile = File(descDir + File.separator + "rssSources.json") if (rssSourcesFile.exists()) { val userRssSourcesFile = File(getWorkDir("storage", "data", userNameSpace, "rssSources.json")) userRssSourcesFile.deleteRecursively() rssSourcesFile.renameTo(userRssSourcesFile) } // 同步 替换规则 val replaceRuleFile = File(descDir + File.separator + "replaceRule.json") if (replaceRuleFile.exists()) { val userReplaceRuleFile = File(getWorkDir("storage", "data", userNameSpace, "replaceRule.json")) userReplaceRuleFile.deleteRecursively() replaceRuleFile.renameTo(userReplaceRuleFile) } // 同步 书签 val bookmarkFile = File(descDir + File.separator + "bookmark.json") if (bookmarkFile.exists()) { val userBookmarkFile = File(getWorkDir("storage", "data", userNameSpace, "bookmark.json")) userBookmarkFile.deleteRecursively() bookmarkFile.renameTo(userBookmarkFile) } // 同步阅读进度 var bookProgressDir = File(userHome + File.separator + "bookProgress") if (!bookProgressDir.exists()) { bookProgressDir = File(userHome + File.separator + "legado" + File.separator + "bookProgress") } if (bookProgressDir.exists() && bookProgressDir.isDirectory()) { bookProgressDir.listFiles().forEach{ syncBookProgressFromWebdav(it, userNameSpace) } } return true } } catch(e: Exception) { e.printStackTrace() } finally { descDirFile.deleteRecursively() } return true; } suspend fun saveToWebdav(latestZipFilePath: String, userNameSpace: String): Boolean { var descDir = getWorkDir("storage", "data", userNameSpace, "tmp") var descDirFile = File(descDir) descDirFile.deleteRecursively() try { val userHome = getUserWebdavHome(userNameSpace) var legadoHome = userHome if (latestZipFilePath.indexOf("legado") > 0) { legadoHome = userHome + File.separator + "legado" } var zipFile = File(latestZipFilePath) if (zipFile.unzip(descDir)) { // 同步 书源 val userBookSourceFile = File(getWorkDir("storage", "data", userNameSpace, "bookSource.json")) if (userBookSourceFile.exists()) { val bookSourceFile = File(descDir + File.separator + "bookSource.json") bookSourceFile.deleteRecursively() userBookSourceFile.copyRecursively(bookSourceFile) } // 同步 书架 val userBookshelfFile = File(getWorkDir("storage", "data", userNameSpace, "bookshelf.json")) if (userBookshelfFile.exists()) { val bookshelfFile = File(descDir + File.separator + "bookshelf.json") bookshelfFile.deleteRecursively() userBookshelfFile.copyRecursively(bookshelfFile) } // 同步 书籍分组 val userBookGroupFile = File(getWorkDir("storage", "data", userNameSpace, "bookGroup.json")) if (userBookGroupFile.exists()) { val bookGroupFile = File(descDir + File.separator + "bookGroup.json") bookGroupFile.deleteRecursively() userBookGroupFile.renameTo(bookGroupFile) } // 同步 RSS订阅 val userRssSourcesFile = File(getWorkDir("storage", "data", userNameSpace, "rssSources.json")) if (userRssSourcesFile.exists()) { val rssSourcesFile = File(descDir + File.separator + "rssSources.json") rssSourcesFile.deleteRecursively() userRssSourcesFile.renameTo(rssSourcesFile) } // 同步 替换规则 val userReplaceRuleFile = File(getWorkDir("storage", "data", userNameSpace, "replaceRule.json")) if (userReplaceRuleFile.exists()) { val replaceRuleFile = File(descDir + File.separator + "replaceRule.json") replaceRuleFile.deleteRecursively() userReplaceRuleFile.renameTo(replaceRuleFile) } // 同步 书签 val userBookmarkFile = File(getWorkDir("storage", "data", userNameSpace, "bookmark.json")) if (userBookmarkFile.exists()) { val bookmarkFile = File(descDir + File.separator + "bookmark.json") bookmarkFile.deleteRecursively() userBookmarkFile.renameTo(bookmarkFile) } // 压缩 val today = SimpleDateFormat("yyyy-MM-dd").format(System.currentTimeMillis()) return descDirFile.zip(legadoHome + File.separator + "backup" + today + ".zip") } } catch(e: Exception) { e.printStackTrace() } finally { descDirFile.deleteRecursively() } return false; } suspend fun getLastBackFileFromWebdav(userNameSpace: String): String? { val userHome = getUserWebdavHome(userNameSpace) var legadoHome = File(userHome + File.separator + "legado") if (!legadoHome.exists()) { legadoHome = File(userHome) } if (!legadoHome.exists()) { return null } var latestZipFile: String? = null val zipFileReg = Regex("^backup[0-9-]+.zip$", RegexOption.IGNORE_CASE) //忽略大小写 legadoHome.listFiles().also{ it.sortByDescending { it.lastModified() } }.forEach { if (zipFileReg.matches(it.name)) { latestZipFile = it.toString() return@forEach } } return latestZipFile } // 从本地导入文件预览 suspend fun importFromLocalPathPreview(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var paths = context.bodyAsJson.getJsonArray("path") if (paths == null) { return returnData.setErrorMsg("参数错误") } var webdav = context.bodyAsJson.getBoolean("webdav", false) if (appConfig.secure) { var userInfo = context.get("userInfo") as User? if (userInfo == null) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (webdav && !userInfo.enable_webdav) { return returnData.setErrorMsg("未开启 Webdav 功能") } else if (!userInfo.enable_local_store) { return returnData.setErrorMsg("未开启本地书仓功能") } } var userNameSpace = getUserNameSpace(context) var home = if (webdav) { getUserWebdavHome(context) } else { getWorkDir("storage", "localStore") } var fileList = arrayListOf>() paths.forEach { var path = it as String? ?: "" if (path.isNotEmpty()) { path = home + path var file = File(path) logger.info("localFile: {} {}", path, file) if (file.exists()) { val fileName = file.name val ext = getFileExt(fileName) if (ext != "txt" && ext != "epub" && ext != "umd" && ext != "cbz") { return returnData.setErrorMsg("不支持导入" + ext + "格式的书籍文件") } val book = Book.initLocalBook(path, path, getWorkDir()) book.setUserNameSpace(userNameSpace) try { val chapters = LocalBook.getChapterList(book) fileList.add(mapOf("book" to book, "chapters" to chapters)) } catch(e: TocEmptyException) { fileList.add(mapOf("book" to book, "chapters" to arrayListOf())) } } } } return returnData.setData(fileList) } suspend fun uploadFileToLocalStore(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (context.fileUploads() == null || context.fileUploads().isEmpty()) { return returnData.setErrorMsg("请上传文件") } if (appConfig.secure) { var userInfo = context.get("userInfo") as User? if (userInfo == null) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (!userInfo.enable_local_store) { return returnData.setErrorMsg("未开启本地书仓功能") } } if (!checkManagerAuth(context)) { return returnData.setData("NEED_SECURE_KEY").setErrorMsg("请输入管理密码") } var path = context.request().getParam("path") if (path.isNullOrEmpty()) { path = "" } var fileList = arrayListOf>() var home = getWorkDir("storage", "localStore") // logger.info("type: {}", type) context.fileUploads().forEach { var file = File(it.uploadedFileName()) logger.info("uploadFile: {} {} {}", it.uploadedFileName(), it.fileName(), file) if (file.exists()) { var fileName = it.fileName() var newFile = File(getWorkDir("storage", "localStore", path, fileName)) if (!newFile.parentFile.exists()) { newFile.parentFile.mkdirs() } if (newFile.exists()) { newFile.delete() } logger.info("moveTo: {}", newFile) if (file.copyRecursively(newFile)) { fileList.add(mapOf( "name" to newFile.name, "size" to newFile.length(), "path" to newFile.toString().replace(home, ""), "lastModified" to newFile.lastModified(), "isDirectory" to newFile.isDirectory() )) } file.deleteRecursively() } } return returnData.setData(fileList) } suspend fun getLocalStoreFileList(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (appConfig.secure) { var userInfo = context.get("userInfo") as User? if (userInfo == null) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (!userInfo.enable_local_store) { return returnData.setErrorMsg("未开启本地书仓功能") } } var path: String if (context.request().method() == HttpMethod.POST) { // post 请求 path = context.bodyAsJson.getString("path") ?: "" } else { // get 请求 path = context.queryParam("path").firstOrNull() ?: "" } if (path.isEmpty()) { path = "/" } var home = getWorkDir("storage", "localStore") var file = File(home + path) logger.info("file: {} {}", path, file) if (!file.exists()) { return returnData.setErrorMsg("路径不存在") } if (!file.isDirectory()) { return returnData.setErrorMsg("路径不是目录") } var fileList = arrayListOf>() file.listFiles().forEach{ if (!it.name.startsWith(".")) { fileList.add(mapOf( "name" to it.name, "size" to it.length(), "path" to it.toString().replace(home, ""), "lastModified" to it.lastModified(), "isDirectory" to it.isDirectory() )) } } return returnData.setData(fileList) } suspend fun getLocalStoreFile(context: RoutingContext) { val returnData = ReturnData() if (!checkAuth(context)) { context.success(returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用")) return } if (appConfig.secure) { var userInfo = context.get("userInfo") as User? if (userInfo == null) { context.success(returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用")) return } if (!userInfo.enable_local_store) { context.success(returnData.setErrorMsg("未开启本地书仓功能")) return } } var path: String if (context.request().method() == HttpMethod.POST) { // post 请求 path = context.bodyAsJson.getString("path") ?: "" } else { // get 请求 path = context.queryParam("path").firstOrNull() ?: "" } if (path.isEmpty()) { context.success(returnData.setErrorMsg("参数错误")) return } var home = getWorkDir("storage", "localStore") var file = File(home + path) logger.info("file: {} {}", path, file) if (!file.exists()) { context.success(returnData.setErrorMsg("路径不存在")) return } context.response().putHeader("Cache-Control", "86400") .putHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(file.name, "UTF-8")) .sendFile(file.toString()) } suspend fun deleteLocalStoreFile(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (appConfig.secure) { var userInfo = context.get("userInfo") as User? if (userInfo == null) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (!userInfo.enable_local_store) { return returnData.setErrorMsg("未开启本地书仓功能") } } if (!checkManagerAuth(context)) { return returnData.setData("NEED_SECURE_KEY").setErrorMsg("请输入管理密码") } var path: String if (context.request().method() == HttpMethod.POST) { // post 请求 path = context.bodyAsJson.getString("path") ?: "" } else { // get 请求 path = context.queryParam("path").firstOrNull() ?: "" } if (path.isEmpty()) { return returnData.setErrorMsg("参数错误") } var home = getWorkDir("storage", "localStore") var file = File(home + path) logger.info("file: {} {}", path, file) if (!file.exists()) { return returnData.setErrorMsg("路径不存在") } file.deleteRecursively() return returnData.setData("") } suspend fun deleteLocalStoreFileList(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (appConfig.secure) { var userInfo = context.get("userInfo") as User? if (userInfo == null) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (!userInfo.enable_local_store) { return returnData.setErrorMsg("未开启本地书仓功能") } } if (!checkManagerAuth(context)) { return returnData.setData("NEED_SECURE_KEY").setErrorMsg("请输入管理密码") } var path = context.bodyAsJson.getJsonArray("path") if (path == null) { return returnData.setErrorMsg("参数错误") } var home = getWorkDir("storage", "localStore") path.forEach { var filePath = it as String? ?: "" if (filePath.isNotEmpty()) { var file = File(home + filePath) file.deleteRecursively() } } return returnData.setData("") } suspend fun bookSourceDebugSSE(context: RoutingContext) { val returnData = ReturnData() // 返回 event-stream val response = context.response().putHeader("Content-Type", "text/event-stream") .putHeader("Cache-Control", "no-cache") .setChunked(true); if (!checkAuth(context)) { response.write("event: error\n") response.end("data: " + jsonEncode(returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用"), false) + "\n\n") return } var bookSourceUrl = context.queryParam("bookSourceUrl").firstOrNull() ?: "" var keyword = context.queryParam("keyword").firstOrNull() ?: "" if (bookSourceUrl.isNullOrEmpty()) { response.write("event: error\n") response.end("data: " + jsonEncode(returnData.setErrorMsg("未配置书源"), false) + "\n\n") return } if (keyword.isNullOrEmpty()) { response.write("event: error\n") response.end("data: " + jsonEncode(returnData.setErrorMsg("请输入搜索关键词"), false) + "\n\n") return } var userNameSpace = getUserNameSpace(context) var bookSourceString = getBookSourceBySourceURL(bookSourceUrl, userNameSpace).first if (bookSourceString.isNullOrEmpty()) { response.write("event: error\n") response.end("data: " + jsonEncode(returnData.setErrorMsg("未配置书源"), false) + "\n\n") return } logger.info("bookSourceDebugSSE bookSource: {} keyword: {}", bookSourceString, keyword) val debugger = Debugger { msg -> response.write("data: " + jsonEncode(mapOf("msg" to msg), false) + "\n\n") } val webBook = WebBook(bookSourceString) debugger.startDebug(webBook, keyword) response.write("event: end\n") response.end("data: " + jsonEncode(mapOf("end" to true), false) + "\n\n") } suspend fun cacheBookSSE(context: RoutingContext) { val returnData = ReturnData() // 返回 event-stream val response = context.response().putHeader("Content-Type", "text/event-stream") .putHeader("Cache-Control", "no-cache") .setChunked(true); if (!checkAuth(context)) { response.write("event: error\n") response.end("data: " + jsonEncode(returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用"), false) + "\n\n") return } var bookUrl: String var refresh: Int var concurrentCount: Int if (context.request().method() == HttpMethod.POST) { // post 请求 bookUrl = context.bodyAsJson.getString("url") ?: context.bodyAsJson.getString("bookUrl") ?: "" refresh = context.bodyAsJson.getInteger("refresh", 0) concurrentCount = context.bodyAsJson.getInteger("concurrentCount", 24) } else { // get 请求 bookUrl = context.queryParam("url").firstOrNull() ?: "" refresh = context.queryParam("refresh").firstOrNull()?.toInt() ?: 0 concurrentCount = context.queryParam("concurrentCount").firstOrNull()?.toInt() ?: 24 } if (bookUrl.isNullOrEmpty()) { response.write("event: error\n") response.end("data: " + jsonEncode(returnData.setErrorMsg("请输入书籍链接"), false) + "\n\n") return } var userNameSpace = getUserNameSpace(context) val bookInfo = getShelfBookByURL(bookUrl, userNameSpace) if (bookInfo == null) { response.write("event: error\n") response.end("data: " + jsonEncode(returnData.setErrorMsg("请先加入书架"), false) + "\n\n") return } if (bookInfo.isLocalBook()) { response.write("event: error\n") response.end("data: " + jsonEncode(returnData.setErrorMsg("本地书籍无需缓存"), false) + "\n\n") return } var bookSource = getBookSourceString(context, bookInfo.origin) if (bookSource.isNullOrEmpty()) { response.write("event: error\n") response.end("data: " + jsonEncode(returnData.setErrorMsg("未配置书源"), false) + "\n\n") return } var chapterList = getLocalChapterList(bookInfo, bookSource, false, userNameSpace) var cachedChapterContentSet = mutableSetOf() if (refresh <= 0) { cachedChapterContentSet = getCachedChapterContentSet(bookInfo, userNameSpace) } val localCacheDir = getChapterCacheDir(bookInfo, userNameSpace) var isEnd = false var successCount = 0; var failedCount = 0; context.request().connection().closeHandler{ logger.info("客户端已断开链接,停止 cacheBookSSE") isEnd = true } concurrentCount = if(concurrentCount > 0) concurrentCount else 24 logger.info("cacheBookSSE concurrentCount: {} refresh: {}", concurrentCount, refresh) limitConcurrent(concurrentCount, 0, chapterList.size, {it-> if (!cachedChapterContentSet.contains(it)) { val chapterIndex = it var chapterInfo = chapterList.get(it) try { var nextChapterUrl: String? = null if (chapterIndex + 1 < chapterList.size) { var nextChapterInfo = chapterList.get(chapterIndex + 1) nextChapterUrl = nextChapterInfo.url } var content = WebBook(bookSource, appConfig.debugLog).getBookContent(bookInfo, chapterInfo, nextChapterUrl) var chapterCacheFile = File(localCacheDir.absolutePath + File.separator + chapterIndex + ".txt") chapterCacheFile.writeText(content) // 保存图片 BookHelp.saveImages( this, BookSource.fromJson(bookSource).getOrNull() ?: BookSource(), bookInfo, chapterInfo, content ) successCount++; cachedChapterContentSet.add(chapterIndex) } catch(e: Exception) { isEnd = true failedCount++ } } it }) {list, loopCount -> if (isEnd) { false } else { // 返回本轮数据 val result = mapOf( "cachedCount" to cachedChapterContentSet.size, "successCount" to successCount, "failedCount" to failedCount ) response.write("data: " + jsonEncode(result, false) + "\n\n") logger.info("Loog: {} list.size: {} result: {}", loopCount, list.size, result) true } } response.write("event: end\n") response.end("data: " + jsonEncode(mapOf( "cachedCount" to cachedChapterContentSet.size, "successCount" to successCount, "failedCount" to failedCount ), false) + "\n\n") } suspend fun deleteBookCache(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var bookUrl: String if (context.request().method() == HttpMethod.POST) { // post 请求 bookUrl = context.bodyAsJson.getString("url") ?: context.bodyAsJson.getString("bookUrl") ?: "" } else { // get 请求 bookUrl = context.queryParam("url").firstOrNull() ?: "" } if (bookUrl.isNullOrEmpty()) { return returnData.setErrorMsg("请输入书籍链接") } var userNameSpace = getUserNameSpace(context) val bookInfo = getShelfBookByURL(bookUrl, userNameSpace) if (bookInfo == null) { return returnData.setErrorMsg("请先加入书架") } if (bookInfo.isLocalBook()) { return returnData.setErrorMsg("本地书籍无需删除缓存") } val localCacheDir = getChapterCacheDir(bookInfo, userNameSpace) localCacheDir.deleteRecursively() return returnData.setData("") } fun getChapterCacheDir(bookInfo: Book, userNameSpace: String): File { val md5Encode = MD5Utils.md5Encode(bookInfo.bookUrl).toString() val localCacheDirPath = getWorkDir("storage", "data", userNameSpace, bookInfo.name + "_" + bookInfo.author, md5Encode) val localCacheDir = File(localCacheDirPath) if (!localCacheDir.exists()) { localCacheDir.mkdirs() } return localCacheDir } fun getCachedChapterContentSet(bookInfo: Book, userNameSpace: String): MutableSet { val localCacheDir = getChapterCacheDir(bookInfo, userNameSpace) val cachedChapterContentSet = mutableSetOf() localCacheDir.listFiles().forEach{ if (!it.name.startsWith(".") && it.name.endsWith(".txt")) { cachedChapterContentSet.add(it.name.replace(".txt", "").toInt()) } } return cachedChapterContentSet } suspend fun getShelfBookWithCacheInfo(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val userNameSpace = getUserNameSpace(context) var bookList = getBookShelfBooks(false, userNameSpace) var result = mutableListOf() for (i in 0 until bookList.size) { val bookInfo = bookList.get(i) if (!bookInfo.isLocalBook()) { val cachedSet = getCachedChapterContentSet(bookInfo, userNameSpace) val bookInfoMap = bookInfo.toMap() as MutableMap bookInfoMap.put("cachedChapterCount", cachedSet.size) result.add(bookInfoMap) } else { result.add(bookInfo) } } return returnData.setData(result) } suspend fun exportBook(context: RoutingContext) { val returnData = ReturnData() if (!checkAuth(context)) { context.success(returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用")) return } var bookUrl: String var isEpub: Int if (context.request().method() == HttpMethod.POST) { // post 请求 bookUrl = context.bodyAsJson.getString("url") ?: context.bodyAsJson.getString("bookUrl") ?: "" isEpub = context.bodyAsJson.getInteger("isEpub", 0) } else { // get 请求 bookUrl = context.queryParam("url").firstOrNull() ?: "" isEpub = context.queryParam("isEpub").firstOrNull()?.toInt() ?: 0 } if (bookUrl.isNullOrEmpty()) { context.success(returnData.setErrorMsg("请输入书籍链接")) return } var userNameSpace = getUserNameSpace(context) val bookInfo = getShelfBookByURL(bookUrl, userNameSpace) if (bookInfo == null) { context.success(returnData.setErrorMsg("请先加入书架")) return } if (bookInfo.isLocalBook()) { val localFile = bookInfo.getLocalFile() context.response().putHeader("Cache-Control", "300") .putHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(localFile.name, "UTF-8")) .sendFile(localFile.toString()) return } var bookSource = getBookSourceString(context, bookInfo.origin) if (bookSource.isNullOrEmpty()) { context.success(returnData.setErrorMsg("未配置书源")) return } var exportDir = File(getWorkDir("storage", "assets", userNameSpace, "export")) val bookFile = if (isEpub > 0) { exportToEpub(exportDir, bookInfo, bookSource, userNameSpace) } else { exportToTxt(exportDir, bookInfo, bookSource, userNameSpace) } context.response().putHeader("Cache-Control", "300") .putHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(bookFile.name, "UTF-8")) .sendFile(bookFile.toString()) } suspend fun exportToTxt(exportDir: File, bookInfo: Book, bookSource: String, userNameSpace: String): File { val filename = "《${bookInfo.name}》作者:${bookInfo.getRealAuthor()}.txt" val bookPath = FileUtils.getPath(exportDir, filename) val bookFile = FileUtils.createFileWithReplace(bookPath) // val stringBuilder = StringBuilder() getAllContents(bookInfo, bookSource, userNameSpace) { text, srcList -> bookFile.appendText(text, Charset.forName(appConfig.exportCharset)) // stringBuilder.append(text) // srcList?.forEach { // val vFile = BookHelp.getImage(bookInfo, it.third) // if (vFile.exists()) { // FileUtils.createFileIfNotExist( // exportDir, // "${book.name}_${book.author}", // "images", // it.first, // "${it.second}-${MD5Utils.md5Encode16(it.third)}.jpg" // ).writeBytes(vFile.readBytes()) // } // } } return bookFile } private suspend fun getAllContents( book: Book, bookSourceString: String, userNameSpace: String, append: (text: String, srcList: ArrayList>?) -> Unit ) { // val useReplace = appConfig.exportUseReplace && book.getUseReplaceRule() // val contentProcessor = ContentProcessor.get(book.name, book.origin) val qy = "${book.name}\n作者:${ book.getRealAuthor() }\n简介:${ HtmlFormatter.format(book.getDisplayIntro()) }" append(qy, null) var chapterList = getLocalChapterList(book, bookSourceString, false, userNameSpace) val localCacheDir = getChapterCacheDir(book, userNameSpace) chapterList.forEachIndexed { index, chapter -> var chapterCacheFile = File(localCacheDir.absolutePath + File.separator + index + ".txt") var content = "" if (!appConfig.exportNoChapterName) { content += chapter.title + "\n" } if (chapterCacheFile.exists()) { content += chapterCacheFile.readText() + "\n" } else { content += "暂无缓存内容。\n" } append.invoke("\n\n$content", null) // BookHelp.getContent(book, chapter).let { content -> // val content1 = contentProcessor // .getContent( // book, // chapter, // content ?: "null", // includeTitle = !appConfig.exportNoChapterName, // useReplace = useReplace, // chineseConvert = false, // reSegment = false // ).joinToString("\n") // if (appConfig.exportPictureFile) { // //txt导出图片文件 // val srcList = arrayListOf>() // content?.split("\n")?.forEachIndexed { index, text -> // val matcher = AppPattern.imgPattern.matcher(text) // while (matcher.find()) { // matcher.group(1)?.let { // val src = NetworkUtils.getAbsoluteURL(chapter.url, it) // srcList.add(Triple(chapter.title, index, src)) // } // } // } // append.invoke("\n\n$content1", srcList) // } else { // append.invoke("\n\n$content1", null) // } // } } } private suspend fun exportToEpub(exportDir: File, book: Book, bookSource: String, userNameSpace: String): File { val filename = "《${book.name}》作者:${book.getRealAuthor()}.epub" val bookPath = FileUtils.getPath(exportDir, filename) val bookFile = FileUtils.createFileWithReplace(bookPath) val epubBook = EpubBook() epubBook.version = "2.0" //set metadata setEpubMetadata(book, epubBook) //set cover setCover(book, epubBook, bookSource) //set css val contentModel = setAssets(book, epubBook) //设置正文 setEpubContent(contentModel, book, epubBook, bookSource, userNameSpace) EpubWriter().write(epubBook, FileOutputStream(bookFile)) return bookFile } private fun setAssets(book: Book, epubBook: EpubBook): String { epubBook.resources.add( Resource( BookController::class.java.getResource("/epub/fonts.css").readBytes(), "Styles/fonts.css" ) ) epubBook.resources.add( Resource( BookController::class.java.getResource("/epub/main.css").readBytes(), "Styles/main.css" ) ) epubBook.resources.add( Resource( BookController::class.java.getResource("/epub/logo.png").readBytes(), "Images/logo.png" ) ) epubBook.addSection( "封面", ResourceUtil.createPublicResource( book.name, book.getRealAuthor(), book.getDisplayIntro(), book.kind, book.wordCount, String(BookController::class.java.getResource("/epub/cover.html").readBytes()), "Text/cover.html" ) ) epubBook.addSection( "简介", ResourceUtil.createPublicResource( book.name, book.getRealAuthor(), book.getDisplayIntro(), book.kind, book.wordCount, String(BookController::class.java.getResource("/epub/intro.html").readBytes()), "Text/intro.html" ) ) return String(BookController::class.java.getResource("/epub/chapter.html").readBytes()) } private suspend fun setCover(book: Book, epubBook: EpubBook, bookSourceString: String) { val coverUrl = book.getDisplayCover() if (coverUrl == null) { // TODO 默认封面 } else if (coverUrl.startsWith("/")) { // 本地 /assets 封面 val coverFile = File(getWorkDir("storage", coverUrl.substring(1))) val byteArray: ByteArray = coverFile.readBytes() epubBook.coverImage = Resource(byteArray, "Images/cover.jpg") } else { var ext = getFileExt(coverUrl, "jpg") val md5Encode = MD5Utils.md5Encode(coverUrl).toString() var cachePath = getWorkDir("storage", "cache", md5Encode + "." + ext) var cacheFile = File(cachePath) if (cacheFile.exists()) { val byteArray: ByteArray = cacheFile.readBytes() epubBook.coverImage = Resource(byteArray, "Images/cover.jpg") return; } val analyzeUrl = AnalyzeUrl(coverUrl, source = BookSource.fromJson(bookSourceString).getOrNull()) try { analyzeUrl.getByteArrayAwait().let { epubBook.coverImage = Resource(it, "Images/cover.jpg") } } catch (e: Exception) { e.printStackTrace() } finally { } // webClient.getAbs(coverUrl).timeout(3000).send // webClient.getAbs(coverUrl).timeout(3000).send { // var bodyBytes = it.result()?.bodyAsBuffer()?.getBytes() // if (bodyBytes != null) { // epubBook.coverImage = Resource(bodyBytes, "Images/cover.jpg") // } // } } } private suspend fun setEpubContent( contentModel: String, book: Book, epubBook: EpubBook, bookSourceString: String, userNameSpace: String ) { //正文 var chapterList = getLocalChapterList(book, bookSourceString, false, userNameSpace) val localCacheDir = getChapterCacheDir(book, userNameSpace) chapterList.forEachIndexed { index, chapter -> var chapterCacheFile = File(localCacheDir.absolutePath + File.separator + index + ".txt") var content = "" if (!appConfig.exportNoChapterName) { content += chapter.title + "\n" } if (chapterCacheFile.exists()) { content += chapterCacheFile.readText() + "\n" } else { content += "暂无缓存内容。\n" } var content1 = fixPic(epubBook, book, content, chapter) // content1 = contentProcessor // .getContent( // book, // chapter, // content1, // includeTitle = false, // useReplace = useReplace, // chineseConvert = false, // reSegment = false // ) // .joinToString("\n") val title = chapter.title epubBook.addSection( title, ResourceUtil.createChapterResource( title.replace("\uD83D\uDD12", ""), content1, contentModel, "Text/chapter_${index}.html" ) ) } } private fun fixPic( epubBook: EpubBook, book: Book, content: String, chapter: BookChapter ): String { val data = StringBuilder("") content.split("\n").forEach { text -> var text1 = text val matcher = AppPattern.imgPattern.matcher(text) while (matcher.find()) { matcher.group(1)?.let { val src = NetworkUtils.getAbsoluteURL(chapter.url, it) val originalHref = "${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}" val href = "Images/${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}" val vFile = BookHelp.getImage(book, src) val fp = FileResourceProvider(vFile.parent) if (vFile.exists()) { val img = LazyResource(fp, href, originalHref) epubBook.resources.add(img) } text1 = text1.replace(src, "../${href}") } } data.append(text1).append("\n") } return data.toString() } private fun setEpubMetadata(book: Book, epubBook: EpubBook) { val metadata = Metadata() metadata.titles.add(book.name)//书籍的名称 metadata.authors.add(Author(book.getRealAuthor()))//书籍的作者 metadata.language = "zh"//数据的语言 metadata.dates.add(Date())//数据的创建日期 metadata.publishers.add("Legado")//数据的创建者 metadata.descriptions.add(book.getDisplayIntro())//书籍的简介 //metadata.subjects.add("")//书籍的主题,在静读天下里面有使用这个分类书籍 epubBook.metadata = metadata } suspend fun searchBookContent(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var bookUrl: String var keyword: String var lastIndex: Int var size: Int if (context.request().method() == HttpMethod.POST) { // post 请求 bookUrl = context.bodyAsJson.getString("url") ?: context.bodyAsJson.getString("bookUrl") ?: "" keyword = context.bodyAsJson.getString("keyword") ?: "" lastIndex = context.bodyAsJson.getInteger("lastIndex", 0) size = context.bodyAsJson.getInteger("size", 20) } else { // get 请求 bookUrl = context.queryParam("url").firstOrNull() ?: "" keyword = context.queryParam("keyword").firstOrNull() ?: "" lastIndex = context.queryParam("lastIndex").firstOrNull()?.toInt() ?: 0 size = context.queryParam("size").firstOrNull()?.toInt() ?: 20 } if (bookUrl.isNullOrEmpty()) { return returnData.setErrorMsg("请输入书籍链接") } if (keyword.isNullOrEmpty()) { return returnData.setErrorMsg("请输入搜索关键词") } var userNameSpace = getUserNameSpace(context) val bookInfo = getShelfBookByURL(bookUrl, userNameSpace) if (bookInfo == null) { return returnData.setErrorMsg("请先加入书架") } var bookSource: String? = null if (!bookInfo.isLocalBook()) { bookSource = getBookSourceString(context, bookInfo.origin) if (bookSource.isNullOrEmpty()) { return returnData.setErrorMsg("未配置书源") } } var chapterList = getLocalChapterList(bookInfo, bookSource ?: "", false, userNameSpace) if (lastIndex >= chapterList.size) { return returnData.setErrorMsg("没有更多了") } var isEnd = false context.request().connection().closeHandler{ logger.info("客户端已断开链接,停止 searchBookContent") isEnd = true } logger.info("searchBookContent keyword: {} lastIndex: {}", keyword, lastIndex) var resultList = mutableListOf(); lastIndex += 1 var currentIndex = lastIndex for (chapterIndex in lastIndex until chapterList.size) { currentIndex = chapterIndex var chapter = chapterList.get(chapterIndex) var chapterResult = searchChapter(bookInfo, chapter, keyword) if (chapterResult.size > 0) { resultList.addAll(chapterResult) } if (resultList.size >= size || isEnd) { break; } } return returnData.setData(mapOf("list" to resultList, "lastIndex" to currentIndex)) } suspend fun searchChapter(book: Book, chapter: BookChapter, query: String): List { val searchResultsWithinChapter: MutableList = mutableListOf() val chapterContent = BookHelp.getContent(book, chapter) if (chapterContent != null) { // withContext(Dispatchers.IO) { // chapter.title = when (AppConfig.chineseConverterType) { // 1 -> ChineseUtils.t2s(chapter.title) // 2 -> ChineseUtils.s2t(chapter.title) // else -> chapter.title // } // mContent = contentProcessor!!.getContent( // book, chapter, chapterContent, // chineseConvert = true, // reSegment = false, // useReplace = false // ).joinToString("") // } val positions = searchPosition(chapterContent, query) logger.info("positions: {}", positions) positions.forEachIndexed { index, position -> val construct = getResultAndQueryIndex(chapterContent, position, query) val result = SearchResult( resultCountWithinChapter = index, resultText = construct.second, chapterTitle = chapter.title, query = query, chapterIndex = chapter.index, queryIndexInResult = construct.first, queryIndexInChapter = position ) searchResultsWithinChapter.add(result) } } return searchResultsWithinChapter } private suspend fun searchPosition(mContent: String, pattern: String): List { val position: MutableList = mutableListOf() var index = mContent.indexOf(pattern) if (index >= 0) { //搜索到内容允许净化 // if (book!!.getUseReplaceRule()) { // mContent = contentProcessor!!.replaceContent(mContent) // index = mContent.indexOf(pattern) // } while (index >= 0) { position.add(index) index = mContent.indexOf(pattern, index + 1) } } return position } private fun getResultAndQueryIndex( content: String, queryIndexInContent: Int, query: String ): Pair { // 左右移动20个字符,构建关键词周边文字,在搜索结果里显示 // todo: 判断段落,只在关键词所在段落内分割 // todo: 利用标点符号分割完整的句 // todo: length和设置结合,自由调整周边文字长度 val length = 20 var po1 = queryIndexInContent - length var po2 = queryIndexInContent + query.length + length if (po1 < 0) { po1 = 0 } if (po2 > content.length) { po2 = content.length } val queryIndexInResult = queryIndexInContent - po1 val newText = content.substring(po1, po2) return queryIndexInResult to newText } } ================================================ FILE: src/main/java/com/htmake/reader/api/controller/BookSourceController.kt ================================================ package com.htmake.reader.api.controller import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssArticle import io.legado.app.model.webBook.WebBook import io.vertx.ext.web.Route import io.vertx.ext.web.Router import io.vertx.ext.web.RoutingContext import io.vertx.ext.web.handler.StaticHandler; import mu.KotlinLogging import com.htmake.reader.config.AppConfig import com.htmake.reader.config.BookConfig import io.legado.app.constant.DeepinkBookSource import com.htmake.reader.utils.error import com.htmake.reader.utils.success import com.htmake.reader.utils.getStorage import com.htmake.reader.utils.saveStorage import com.htmake.reader.utils.asJsonArray import com.htmake.reader.utils.asJsonObject import com.htmake.reader.utils.toDataClass import com.htmake.reader.utils.toMap import com.htmake.reader.utils.fillData import com.htmake.reader.utils.getWorkDir import com.htmake.reader.utils.getRandomString import com.htmake.reader.utils.genEncryptedPassword import com.htmake.reader.entity.User import com.htmake.reader.utils.SpringContextUtils import com.htmake.reader.utils.deleteRecursively import com.htmake.reader.utils.unzip import com.htmake.reader.utils.zip import com.htmake.reader.utils.jsonEncode import com.htmake.reader.utils.getRelativePath import com.htmake.reader.verticle.RestVerticle import com.htmake.reader.SpringEvent import org.springframework.stereotype.Component import io.vertx.core.json.JsonObject import io.vertx.core.json.JsonArray import io.vertx.core.http.HttpMethod import com.htmake.reader.api.ReturnData import io.legado.app.utils.MD5Utils import java.net.URLDecoder; import java.net.URLEncoder; import java.net.URL; import java.util.UUID; import io.vertx.ext.web.client.WebClient import org.springframework.beans.factory.annotation.Autowired import org.springframework.core.env.Environment import java.io.File import java.lang.Runtime import kotlin.collections.mutableMapOf import kotlin.system.measureTimeMillis import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.text.SimpleDateFormat; import io.legado.app.utils.EncoderUtils import io.legado.app.model.rss.Rss import org.springframework.scheduling.annotation.Scheduled import io.legado.app.model.localBook.LocalBook import java.nio.file.Paths import kotlinx.coroutines.withContext import kotlinx.coroutines.async import kotlinx.coroutines.Deferred import kotlinx.coroutines.CoroutineScope private val logger = KotlinLogging.logger {} class BookSourceController(coroutineContext: CoroutineContext): BaseController(coroutineContext) { private var webClient: WebClient init { webClient = SpringContextUtils.getBean("webClient", WebClient::class.java) } suspend fun getUserBookSourceJson(userNameSpace: String): JsonArray? { var bookSourceList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "bookSource")) if (bookSourceList == null && !userNameSpace.equals("default")) { // 用户书源文件不存在,拷贝系统书源 var systemBookSourceList: JsonArray? = asJsonArray(getUserStorage("default", "bookSource")) if (systemBookSourceList != null) { saveUserStorage(userNameSpace, "bookSource", systemBookSourceList.getList()) bookSourceList = systemBookSourceList } } return bookSourceList } suspend fun saveBookSource(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val bookSource = BookSource.fromJson(context.bodyAsString).getOrNull() if (bookSource == null) { return returnData.setErrorMsg("参数错误") } // val bookSource = context.bodyAsJson.mapTo(BookSource::class.java) var userNameSpace = getUserNameSpace(context) var bookSourceList = getUserBookSourceJson(userNameSpace) if (bookSourceList == null) { bookSourceList = JsonArray() } // 遍历判断书本是否存在 var existIndex: Int = -1 for (i in 0 until bookSourceList.size()) { var _bookSource = bookSourceList.getJsonObject(i).mapTo(BookSource::class.java) if (_bookSource.bookSourceUrl.equals(bookSource.bookSourceUrl)) { existIndex = i break; } } if (existIndex >= 0) { var sourceList = bookSourceList.getList() sourceList.set(existIndex, JsonObject.mapFrom(bookSource)) bookSourceList = JsonArray(sourceList) } else { bookSourceList.add(JsonObject.mapFrom(bookSource)) } // logger.info("bookSourceList: {}", bookSourceList) saveUserStorage(userNameSpace, "bookSource", bookSourceList) return returnData.setData("") } suspend fun saveBookSources(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val bookSourceJsonArray = context.bodyAsJsonArray if (bookSourceJsonArray == null) { return returnData.setErrorMsg("参数错误") } var userNameSpace = getUserNameSpace(context) var bookSourceList = getUserBookSourceJson(userNameSpace) if (bookSourceList == null) { bookSourceList = JsonArray() } for (k in 0 until bookSourceJsonArray.size()) { val bookSource = BookSource.fromJson(bookSourceJsonArray.getJsonObject(k).toString()).getOrNull() if (bookSource == null) { continue } // var bookSource = bookSourceJsonArray.getJsonObject(k).mapTo(BookSource::class.java) // 遍历判断书本是否存在 var existIndex: Int = -1 for (i in 0 until bookSourceList!!.size()) { var _bookSource = bookSourceList.getJsonObject(i).mapTo(BookSource::class.java) if (_bookSource.bookSourceUrl.equals(bookSource.bookSourceUrl)) { existIndex = i break; } } if (existIndex >= 0) { var sourceList = bookSourceList.getList() sourceList.set(existIndex, JsonObject.mapFrom(bookSource)) bookSourceList = JsonArray(sourceList) } else { bookSourceList.add(JsonObject.mapFrom(bookSource)) } } // logger.info("bookSourceList: {}", bookSourceList) saveUserStorage(userNameSpace, "bookSource", bookSourceList!!) return returnData.setData("") } suspend fun getBookSource(context: RoutingContext): ReturnData { val returnData = ReturnData() checkAuth(context) var bookSourceUrl: String if (context.request().method() == HttpMethod.POST) { // post 请求 bookSourceUrl = context.bodyAsJson.getString("bookSourceUrl") } else { // get 请求 bookSourceUrl = context.queryParam("bookSourceUrl").firstOrNull() ?: "" } if (bookSourceUrl.isNullOrEmpty()) { return returnData.setErrorMsg("书源链接不能为空") } var userNameSpace = getUserNameSpace(context) var bookSourceList = getUserBookSourceJson(userNameSpace) if (bookSourceList == null) { bookSourceList = JsonArray() } // 遍历判断书本是否存在 var existIndex: Int = -1 for (i in 0 until bookSourceList.size()) { var _bookSource = bookSourceList.getJsonObject(i).mapTo(BookSource::class.java) if (_bookSource.bookSourceUrl.equals(bookSourceUrl)) { existIndex = i break; } } if (existIndex < 0) { return returnData.setErrorMsg("书源信息不存在") } return returnData.setData(bookSourceList.getJsonObject(existIndex).map) } suspend fun getBookSources(context: RoutingContext): ReturnData { val returnData = ReturnData() checkAuth(context) var simple: Int = 0 if (context.request().method() == HttpMethod.POST) { // post 请求 simple = context.bodyAsJson.getInteger("simple", 0) } else { // get 请求 simple = context.queryParam("simple").firstOrNull()?.toInt() ?: 0 } var userNameSpace = getUserNameSpace(context) var bookSourceList = getUserBookSourceJson(userNameSpace) if (bookSourceList != null) { if (simple > 0) { var list = arrayListOf>() for (i in 0 until bookSourceList.size()) { var bookSource = bookSourceList.getJsonObject(i).mapTo(BookSource::class.java) list.add(mapOf( "bookSourceGroup" to bookSource.bookSourceGroup, "bookSourceName" to bookSource.bookSourceName, "bookSourceUrl" to bookSource.bookSourceUrl, "exploreUrl" to bookSource.exploreUrl )) } return returnData.setData(list) } return returnData.setData(bookSourceList.getList()) } return returnData.setData(arrayListOf()) } suspend fun deleteBookSource(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val bookSource = context.bodyAsJson.mapTo(BookSource::class.java) var userNameSpace = getUserNameSpace(context) var bookSourceList = getUserBookSourceJson(userNameSpace) if (bookSourceList == null) { bookSourceList = JsonArray() } // 遍历判断书本是否存在 var existIndex: Int = -1 for (i in 0 until bookSourceList.size()) { var _bookSource = bookSourceList.getJsonObject(i).mapTo(BookSource::class.java) if (_bookSource.bookSourceUrl.equals(bookSource.bookSourceUrl)) { existIndex = i break; } } if (existIndex >= 0) { bookSourceList.remove(existIndex) } // logger.info("bookSourceList: {}", bookSourceList) saveUserStorage(userNameSpace, "bookSource", bookSourceList) return returnData.setData("") } suspend fun deleteBookSources(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val bookSourceJsonArray = context.bodyAsJsonArray var userNameSpace = getUserNameSpace(context) var bookSourceList = getUserBookSourceJson(userNameSpace) if (bookSourceList == null) { bookSourceList = JsonArray() } for (k in 0 until bookSourceJsonArray.size()) { var bookSource = bookSourceJsonArray.getJsonObject(k).mapTo(BookSource::class.java) // 遍历判断书本是否存在 var existIndex: Int = -1 for (i in 0 until bookSourceList.size()) { var _bookSource = bookSourceList.getJsonObject(i).mapTo(BookSource::class.java) if (_bookSource.bookSourceUrl.equals(bookSource.bookSourceUrl)) { existIndex = i break; } } if (existIndex >= 0) { bookSourceList.remove(existIndex) } } // logger.info("bookSourceList: {}", bookSourceList) saveUserStorage(userNameSpace, "bookSource", bookSourceList) return returnData.setData("") } suspend fun deleteAllBookSources(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var userNameSpace = getUserNameSpace(context) saveUserStorage(userNameSpace, "bookSource", JsonArray()) return returnData.setData("") } suspend fun setAsDefaultBookSources(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (!checkManagerAuth(context)) { return returnData.setData("NEED_SECURE_KEY").setErrorMsg("请输入管理密码") } var username = context.bodyAsJson.getString("username") var bookSourceList: JsonArray? = asJsonArray(getUserStorage(username, "bookSource")) if (bookSourceList == null) { return returnData.setErrorMsg("用户书源不存在") } // 保存为默认书源 saveUserStorage("default", "bookSource", bookSourceList.getList()) return returnData.setData("设置默认书源成功") } suspend fun readSourceFile(context: RoutingContext): ReturnData { val returnData = ReturnData() if (context.fileUploads() == null || context.fileUploads().isEmpty()) { return returnData.setErrorMsg("请上传文件") } var sourceList = JsonArray() context.fileUploads().forEach { // logger.info("readSourceFile: {}", it.uploadedFileName()) var file = File(it.uploadedFileName()) if (file.exists()) { sourceList.add(file.readText()) file.delete() } } return returnData.setData(sourceList.getList()) } suspend fun readRemoteSourceFile(context: RoutingContext) { val returnData = ReturnData() var url: String if (context.request().method() == HttpMethod.POST) { // post 请求 url = context.bodyAsJson.getString("url") ?: "" } else { // get 请求 url = context.queryParam("url").firstOrNull() ?: "" } if (url.isNullOrEmpty()) { context.success(returnData.setErrorMsg("请输入远程书源链接")) return } launch(Dispatchers.IO) { webClient.getAbs(url).timeout(3000).send { var body = it.result()?.bodyAsString() if (body != null) { context.success(returnData.setData(arrayListOf(body))) } else { context.success(returnData.setErrorMsg("远程书源链接错误")) } } } } suspend fun deleteUserBookSource(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (!checkManagerAuth(context)) { return returnData.setData("NEED_SECURE_KEY").setErrorMsg("请输入管理密码") } val userJsonArray = context.bodyAsJsonArray for (i in 0 until userJsonArray.size()) { var username = userJsonArray.getString(i) var userBookSourceFile = File(getWorkDir("storage", "data", username, "bookSource.json")) // 删除用户书源文件,恢复默认书源 if (userBookSourceFile.exists()) { userBookSourceFile.deleteRecursively() } } return returnData.setData("删除书源成功") } suspend fun deleteBookSourcesFile(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var userNameSpace = getUserNameSpace(context) var userBookSourceFile = File(getWorkDir("storage", "data", userNameSpace, "bookSource.json")) // 删除用户书源文件,恢复默认书源 if (userBookSourceFile.exists()) { userBookSourceFile.deleteRecursively() } return returnData.setData("") } } ================================================ FILE: src/main/java/com/htmake/reader/api/controller/BookmarkController.kt ================================================ package com.htmake.reader.api.controller import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.Bookmark import io.legado.app.model.webBook.WebBook import io.vertx.ext.web.Route import io.vertx.ext.web.Router import io.vertx.ext.web.RoutingContext import io.vertx.ext.web.handler.StaticHandler; import mu.KotlinLogging import com.htmake.reader.config.AppConfig import com.htmake.reader.config.BookConfig import io.legado.app.constant.DeepinkBookSource import com.htmake.reader.utils.error import com.htmake.reader.utils.success import com.htmake.reader.utils.getStorage import com.htmake.reader.utils.saveStorage import com.htmake.reader.utils.asJsonArray import com.htmake.reader.utils.asJsonObject import com.htmake.reader.utils.toDataClass import com.htmake.reader.utils.toMap import com.htmake.reader.utils.fillData import com.htmake.reader.utils.getWorkDir import com.htmake.reader.utils.getRandomString import com.htmake.reader.utils.genEncryptedPassword import com.htmake.reader.entity.User import com.htmake.reader.utils.SpringContextUtils import com.htmake.reader.utils.deleteRecursively import com.htmake.reader.utils.unzip import com.htmake.reader.utils.zip import com.htmake.reader.utils.jsonEncode import com.htmake.reader.utils.getRelativePath import com.htmake.reader.verticle.RestVerticle import com.htmake.reader.SpringEvent import org.springframework.stereotype.Component import io.vertx.core.json.JsonObject import io.vertx.core.json.JsonArray import io.vertx.core.http.HttpMethod import com.htmake.reader.api.ReturnData import io.legado.app.utils.MD5Utils import java.net.URLDecoder; import java.net.URLEncoder; import java.net.URL; import java.util.UUID; import io.vertx.ext.web.client.WebClient import org.springframework.beans.factory.annotation.Autowired import org.springframework.core.env.Environment import java.io.File import java.lang.Runtime import kotlin.collections.mutableMapOf import kotlin.system.measureTimeMillis import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.text.SimpleDateFormat; import io.legado.app.utils.EncoderUtils import io.legado.app.model.rss.Rss import org.springframework.scheduling.annotation.Scheduled import io.legado.app.model.localBook.LocalBook import java.nio.file.Paths import kotlinx.coroutines.withContext import kotlinx.coroutines.async import kotlinx.coroutines.Deferred import kotlinx.coroutines.CoroutineScope private val logger = KotlinLogging.logger {} class BookmarkController(coroutineContext: CoroutineContext): BaseController(coroutineContext) { suspend fun getBookmarks(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var userNameSpace = getUserNameSpace(context) var list: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "bookmark")) if (list != null) { return returnData.setData(list.getList()) } return returnData.setData(arrayListOf()) } suspend fun saveBookmark(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val bookmark = context.bodyAsJson.mapTo(Bookmark::class.java) if (bookmark.bookName.isEmpty() && bookmark.bookAuthor.isEmpty()) { return returnData.setErrorMsg("书籍信息错误") } var userNameSpace = getUserNameSpace(context) var bookmarkList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "bookmark")) if (bookmarkList == null) { bookmarkList = JsonArray() } // 遍历判断是否存在 var existIndex: Int = -1 for (i in 0 until bookmarkList.size()) { var _bookmark = bookmarkList.getJsonObject(i).mapTo(Bookmark::class.java) if (_bookmark.bookName.equals(bookmark.bookName) && _bookmark.bookAuthor.equals(bookmark.bookAuthor)) { existIndex = i break; } } if (existIndex >= 0) { var list = bookmarkList.getList() list.set(existIndex, JsonObject.mapFrom(bookmark)) bookmarkList = JsonArray(list) } else { // 新增书签 bookmarkList.add(JsonObject.mapFrom(bookmark)) } // logger.info("bookmarkList: {}", bookmarkList) saveUserStorage(userNameSpace, "bookmark", bookmarkList) return returnData.setData("") } suspend fun saveBookmarks(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val bookmarkJsonArray = context.bodyAsJsonArray if (bookmarkJsonArray == null) { return returnData.setErrorMsg("参数错误") } var userNameSpace = getUserNameSpace(context) var bookmarkList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "bookmark")) if (bookmarkList == null) { bookmarkList = JsonArray() } for (k in 0 until bookmarkJsonArray.size()) { var bookmark = bookmarkJsonArray.getJsonObject(k).mapTo(Bookmark::class.java) if (bookmark.bookName.isEmpty() && bookmark.bookAuthor.isEmpty()) { continue } // 遍历判断是否存在 var existIndex: Int = -1 for (i in 0 until bookmarkList!!.size()) { var _bookmark = bookmarkList.getJsonObject(i).mapTo(Bookmark::class.java) if (_bookmark.bookName.equals(bookmark.bookName) && _bookmark.bookAuthor.equals(bookmark.bookAuthor)) { existIndex = i break; } } if (existIndex >= 0) { var list = bookmarkList.getList() list.set(existIndex, JsonObject.mapFrom(bookmark)) bookmarkList = JsonArray(list) } else { // 新增书签 bookmarkList.add(JsonObject.mapFrom(bookmark)) } } // logger.info("bookmarkList: {}", bookmarkList) saveUserStorage(userNameSpace, "bookmark", bookmarkList!!) return returnData.setData("") } suspend fun deleteBookmark(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val bookmark = context.bodyAsJson.mapTo(Bookmark::class.java) var userNameSpace = getUserNameSpace(context) var bookmarkList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "bookmark")) if (bookmarkList == null) { bookmarkList = JsonArray() } // 遍历判断是否存在 var existIndex: Int = -1 for (i in 0 until bookmarkList.size()) { var _bookmark = bookmarkList.getJsonObject(i).mapTo(Bookmark::class.java) if (_bookmark.bookName.equals(bookmark.bookName) && _bookmark.bookAuthor.equals(bookmark.bookAuthor)) { existIndex = i break; } } if (existIndex >= 0) { bookmarkList.remove(existIndex) } // logger.info("bookmark: {}", bookmark) saveUserStorage(userNameSpace, "bookmark", bookmarkList) return returnData.setData("") } suspend fun deleteBookmarks(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val bookmarkJsonArray = context.bodyAsJsonArray var userNameSpace = getUserNameSpace(context) var bookmarkList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "bookmark")) if (bookmarkList == null) { bookmarkList = JsonArray() } for (k in 0 until bookmarkJsonArray.size()) { var bookmark = bookmarkJsonArray.getJsonObject(k).mapTo(Bookmark::class.java) // 遍历判断书本是否存在 var existIndex: Int = -1 for (i in 0 until bookmarkList.size()) { var _bookmark = bookmarkList.getJsonObject(i).mapTo(Bookmark::class.java) if (_bookmark.bookName.equals(bookmark.bookName) && _bookmark.bookAuthor.equals(bookmark.bookAuthor)) { existIndex = i break; } } if (existIndex >= 0) { bookmarkList.remove(existIndex) } } // logger.info("bookmark: {}", bookmark) saveUserStorage(userNameSpace, "bookmark", bookmarkList) return returnData.setData("") } } ================================================ FILE: src/main/java/com/htmake/reader/api/controller/ReplaceRuleController.kt ================================================ package com.htmake.reader.api.controller import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.ReplaceRule import io.legado.app.model.webBook.WebBook import io.vertx.ext.web.Route import io.vertx.ext.web.Router import io.vertx.ext.web.RoutingContext import io.vertx.ext.web.handler.StaticHandler; import mu.KotlinLogging import com.htmake.reader.config.AppConfig import com.htmake.reader.config.BookConfig import io.legado.app.constant.DeepinkBookSource import com.htmake.reader.utils.error import com.htmake.reader.utils.success import com.htmake.reader.utils.getStorage import com.htmake.reader.utils.saveStorage import com.htmake.reader.utils.asJsonArray import com.htmake.reader.utils.asJsonObject import com.htmake.reader.utils.toDataClass import com.htmake.reader.utils.toMap import com.htmake.reader.utils.fillData import com.htmake.reader.utils.getWorkDir import com.htmake.reader.utils.getRandomString import com.htmake.reader.utils.genEncryptedPassword import com.htmake.reader.entity.User import com.htmake.reader.utils.SpringContextUtils import com.htmake.reader.utils.deleteRecursively import com.htmake.reader.utils.unzip import com.htmake.reader.utils.zip import com.htmake.reader.utils.jsonEncode import com.htmake.reader.utils.getRelativePath import com.htmake.reader.verticle.RestVerticle import com.htmake.reader.SpringEvent import org.springframework.stereotype.Component import io.vertx.core.json.JsonObject import io.vertx.core.json.JsonArray import io.vertx.core.http.HttpMethod import com.htmake.reader.api.ReturnData import io.legado.app.utils.MD5Utils import java.net.URLDecoder; import java.net.URLEncoder; import java.net.URL; import java.util.UUID; import io.vertx.ext.web.client.WebClient import org.springframework.beans.factory.annotation.Autowired import org.springframework.core.env.Environment import java.io.File import java.lang.Runtime import kotlin.collections.mutableMapOf import kotlin.system.measureTimeMillis import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.text.SimpleDateFormat; import io.legado.app.utils.EncoderUtils import io.legado.app.model.rss.Rss import org.springframework.scheduling.annotation.Scheduled import io.legado.app.model.localBook.LocalBook import java.nio.file.Paths import kotlinx.coroutines.withContext import kotlinx.coroutines.async import kotlinx.coroutines.Deferred import kotlinx.coroutines.CoroutineScope private val logger = KotlinLogging.logger {} class ReplaceRuleController(coroutineContext: CoroutineContext): BaseController(coroutineContext) { suspend fun getReplaceRules(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var userNameSpace = getUserNameSpace(context) var list: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "replaceRule")) if (list != null) { return returnData.setData(list.getList()) } return returnData.setData(arrayListOf()) } suspend fun saveReplaceRule(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val replaceRule = context.bodyAsJson.mapTo(ReplaceRule::class.java) if (replaceRule.name.isEmpty()) { return returnData.setErrorMsg("名称不能为空") } if (replaceRule.pattern.isEmpty()) { return returnData.setErrorMsg("替换规则不能为空") } var userNameSpace = getUserNameSpace(context) var replaceRuleList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "replaceRule")) if (replaceRuleList == null) { replaceRuleList = JsonArray() } // 遍历判断是否存在 var existIndex: Int = -1 for (i in 0 until replaceRuleList.size()) { var _replaceRule = replaceRuleList.getJsonObject(i).mapTo(ReplaceRule::class.java) if (_replaceRule.name.equals(replaceRule.name)) { existIndex = i break; } } if (existIndex >= 0) { var list = replaceRuleList.getList() list.set(existIndex, JsonObject.mapFrom(replaceRule)) replaceRuleList = JsonArray(list) } else { // 新增替换规则 replaceRuleList.add(JsonObject.mapFrom(replaceRule)) } // logger.info("replaceRuleList: {}", replaceRuleList) saveUserStorage(userNameSpace, "replaceRule", replaceRuleList) return returnData.setData("") } suspend fun saveReplaceRules(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val replaceRuleJsonArray = context.bodyAsJsonArray if (replaceRuleJsonArray == null) { return returnData.setErrorMsg("参数错误") } var userNameSpace = getUserNameSpace(context) var replaceRuleList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "replaceRule")) if (replaceRuleList == null) { replaceRuleList = JsonArray() } for (k in 0 until replaceRuleJsonArray.size()) { var replaceRule = replaceRuleJsonArray.getJsonObject(k).mapTo(ReplaceRule::class.java) if (replaceRule.name.isEmpty()) { continue } if (replaceRule.pattern.isEmpty()) { continue } // 遍历判断是否存在 var existIndex: Int = -1 for (i in 0 until replaceRuleList!!.size()) { var _replaceRule = replaceRuleList.getJsonObject(i).mapTo(ReplaceRule::class.java) if (_replaceRule.name.equals(replaceRule.name)) { existIndex = i break; } } if (existIndex >= 0) { var list = replaceRuleList.getList() list.set(existIndex, JsonObject.mapFrom(replaceRule)) replaceRuleList = JsonArray(list) } else { // 新增替换规则 replaceRuleList.add(JsonObject.mapFrom(replaceRule)) } } // logger.info("replaceRuleList: {}", replaceRuleList) saveUserStorage(userNameSpace, "replaceRule", replaceRuleList!!) return returnData.setData("") } suspend fun deleteReplaceRule(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val replaceRule = context.bodyAsJson.mapTo(ReplaceRule::class.java) var userNameSpace = getUserNameSpace(context) var replaceRuleList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "replaceRule")) if (replaceRuleList == null) { replaceRuleList = JsonArray() } // 遍历判断是否存在 var existIndex: Int = -1 for (i in 0 until replaceRuleList.size()) { var _replaceRule = replaceRuleList.getJsonObject(i).mapTo(ReplaceRule::class.java) if (_replaceRule.name.equals(replaceRule.name)) { existIndex = i break; } } if (existIndex >= 0) { replaceRuleList.remove(existIndex) } // logger.info("replaceRule: {}", replaceRule) saveUserStorage(userNameSpace, "replaceRule", replaceRuleList) return returnData.setData("") } suspend fun deleteReplaceRules(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val replaceRuleJsonArray = context.bodyAsJsonArray var userNameSpace = getUserNameSpace(context) var replaceRuleList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "replaceRule")) if (replaceRuleList == null) { replaceRuleList = JsonArray() } for (k in 0 until replaceRuleJsonArray.size()) { var replaceRule = replaceRuleJsonArray.getJsonObject(k).mapTo(ReplaceRule::class.java) // 遍历判断书本是否存在 var existIndex: Int = -1 for (i in 0 until replaceRuleList.size()) { var _replaceRule = replaceRuleList.getJsonObject(i).mapTo(ReplaceRule::class.java) if (_replaceRule.name.equals(replaceRule.name)) { existIndex = i break; } } if (existIndex >= 0) { replaceRuleList.remove(existIndex) } } // logger.info("replaceRule: {}", replaceRule) saveUserStorage(userNameSpace, "replaceRule", replaceRuleList) return returnData.setData("") } } ================================================ FILE: src/main/java/com/htmake/reader/api/controller/RssSourceController.kt ================================================ package com.htmake.reader.api.controller import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssArticle import io.legado.app.model.webBook.WebBook import io.vertx.ext.web.Route import io.vertx.ext.web.Router import io.vertx.ext.web.RoutingContext import io.vertx.ext.web.handler.StaticHandler; import mu.KotlinLogging import com.htmake.reader.config.AppConfig import com.htmake.reader.config.BookConfig import io.legado.app.constant.DeepinkBookSource import com.htmake.reader.utils.error import com.htmake.reader.utils.success import com.htmake.reader.utils.getStorage import com.htmake.reader.utils.saveStorage import com.htmake.reader.utils.asJsonArray import com.htmake.reader.utils.asJsonObject import com.htmake.reader.utils.toDataClass import com.htmake.reader.utils.toMap import com.htmake.reader.utils.fillData import com.htmake.reader.utils.getWorkDir import com.htmake.reader.utils.getRandomString import com.htmake.reader.utils.genEncryptedPassword import com.htmake.reader.entity.User import com.htmake.reader.utils.SpringContextUtils import com.htmake.reader.utils.deleteRecursively import com.htmake.reader.utils.unzip import com.htmake.reader.utils.zip import com.htmake.reader.utils.jsonEncode import com.htmake.reader.utils.getRelativePath import com.htmake.reader.verticle.RestVerticle import com.htmake.reader.SpringEvent import org.springframework.stereotype.Component import io.vertx.core.json.JsonObject import io.vertx.core.json.JsonArray import io.vertx.core.http.HttpMethod import com.htmake.reader.api.ReturnData import io.legado.app.utils.MD5Utils import java.net.URLDecoder; import java.net.URLEncoder; import java.net.URL; import java.util.UUID; import io.vertx.ext.web.client.WebClient import org.springframework.beans.factory.annotation.Autowired import org.springframework.core.env.Environment import java.io.File import java.lang.Runtime import kotlin.collections.mutableMapOf import kotlin.system.measureTimeMillis import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.text.SimpleDateFormat; import io.legado.app.utils.EncoderUtils import io.legado.app.model.rss.Rss import org.springframework.scheduling.annotation.Scheduled import io.legado.app.model.localBook.LocalBook import io.legado.app.model.Debug import java.nio.file.Paths import kotlinx.coroutines.withContext import kotlinx.coroutines.async import kotlinx.coroutines.Deferred import kotlinx.coroutines.CoroutineScope private val logger = KotlinLogging.logger {} class RssSourceController(coroutineContext: CoroutineContext): BaseController(coroutineContext) { suspend fun getRssSources(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var userNameSpace = getUserNameSpace(context) var list: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "rssSources")) if (list != null) { return returnData.setData(list.getList()) } return returnData.setData(arrayListOf()) } suspend fun saveRssSource(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val rssSource = context.bodyAsJson.mapTo(RssSource::class.java) if (rssSource.sourceUrl.isEmpty()) { return returnData.setErrorMsg("RSS链接不能为空") } if (rssSource.sourceName.isEmpty()) { return returnData.setErrorMsg("RSS名称不能为空") } var userNameSpace = getUserNameSpace(context) var rssSourceList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "rssSources")) if (rssSourceList == null) { rssSourceList = JsonArray() } // 遍历判断是否存在 var existIndex: Int = -1 for (i in 0 until rssSourceList.size()) { var _rssSource = rssSourceList.getJsonObject(i).mapTo(RssSource::class.java) if (_rssSource.sourceUrl.equals(rssSource.sourceUrl)) { existIndex = i break; } } if (existIndex >= 0) { var list = rssSourceList.getList() list.set(existIndex, JsonObject.mapFrom(rssSource)) rssSourceList = JsonArray(list) } else { // 新增rss源 rssSourceList.add(JsonObject.mapFrom(rssSource)) } // logger.info("rssSourceList: {}", rssSourceList) saveUserStorage(userNameSpace, "rssSources", rssSourceList) return returnData.setData("") } suspend fun saveRssSources(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val rssSourceJsonArray = context.bodyAsJsonArray if (rssSourceJsonArray == null) { return returnData.setErrorMsg("参数错误") } var userNameSpace = getUserNameSpace(context) var rssSourceList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "rssSources")) if (rssSourceList == null) { rssSourceList = JsonArray() } for (k in 0 until rssSourceJsonArray.size()) { var rssSource = rssSourceJsonArray.getJsonObject(k).mapTo(RssSource::class.java) if (rssSource.sourceUrl.isEmpty()) { continue } if (rssSource.sourceName.isEmpty()) { continue } // 遍历判断是否存在 var existIndex: Int = -1 for (i in 0 until rssSourceList!!.size()) { var _rssSource = rssSourceList.getJsonObject(i).mapTo(RssSource::class.java) if (_rssSource.sourceUrl.equals(rssSource.sourceUrl)) { existIndex = i break; } } if (existIndex >= 0) { var list = rssSourceList.getList() list.set(existIndex, JsonObject.mapFrom(rssSource)) rssSourceList = JsonArray(list) } else { // 新增rss源 rssSourceList.add(JsonObject.mapFrom(rssSource)) } } // logger.info("rssSourceList: {}", rssSourceList) saveUserStorage(userNameSpace, "rssSources", rssSourceList!!) return returnData.setData("") } suspend fun deleteRssSource(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val rssSource = context.bodyAsJson.mapTo(RssSource::class.java) var userNameSpace = getUserNameSpace(context) var rssSourceList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "rssSources")) if (rssSourceList == null) { rssSourceList = JsonArray() } // 遍历判断是否存在 var existIndex: Int = -1 for (i in 0 until rssSourceList.size()) { var _rssSource = rssSourceList.getJsonObject(i).mapTo(RssSource::class.java) if (_rssSource.sourceUrl.equals(rssSource.sourceUrl)) { existIndex = i break; } } if (existIndex >= 0) { rssSourceList.remove(existIndex) } // logger.info("rssSource: {}", rssSource) saveUserStorage(userNameSpace, "rssSources", rssSourceList) return returnData.setData("") } fun getRssSourceByURL(url: String, userNameSpace: String): RssSource? { if (url.isEmpty()) { return null } var list: JsonArray? = asJsonArray(getUserStorage(userNameSpace, "rssSources")) if (list == null) { return null } for (i in 0 until list.size()) { var _rssSource = list.getJsonObject(i).mapTo(RssSource::class.java) if (_rssSource.sourceUrl.equals(url)) { return _rssSource } } return null } suspend fun getRssArticles(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var sourceUrl: String var sortName: String var sortUrl: String var page: Int if (context.request().method() == HttpMethod.POST) { // post 请求 sourceUrl = context.bodyAsJson.getString("sourceUrl") sortName = context.bodyAsJson.getString("sortName", "") sortUrl = context.bodyAsJson.getString("sortUrl", "") page = context.bodyAsJson.getInteger("page", 1) } else { // get 请求 sourceUrl = context.queryParam("sourceUrl").firstOrNull() ?: "" sortName = context.queryParam("sortName").firstOrNull() ?: "" sortUrl = context.queryParam("sortUrl").firstOrNull() ?: "" page = context.queryParam("page").firstOrNull()?.toInt() ?: 1 } if (sourceUrl.isEmpty()) { return returnData.setErrorMsg("RSS源链接不能为空") } if (sortUrl.isEmpty()) { sortUrl = sourceUrl } var userNameSpace = getUserNameSpace(context) var rssSource = getRssSourceByURL(sourceUrl, userNameSpace) if (rssSource == null) { return returnData.setErrorMsg("RSS源不存在") } val rssArtcles = Rss.getArticles(sortName, sortUrl, rssSource, page, Debug) return returnData.setData(rssArtcles) } suspend fun getRssContent(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var sourceUrl: String var link: String var origin: String if (context.request().method() == HttpMethod.POST) { // post 请求 sourceUrl = context.bodyAsJson.getString("sourceUrl") link = context.bodyAsJson.getString("link") origin = context.bodyAsJson.getString("origin") } else { // get 请求 sourceUrl = context.queryParam("sourceUrl").firstOrNull() ?: "" link = context.queryParam("link").firstOrNull() ?: "" origin = context.queryParam("origin").firstOrNull() ?: "" } if (sourceUrl.isEmpty()) { return returnData.setErrorMsg("RSS链接不能为空") } if (link.isEmpty()) { return returnData.setErrorMsg("RSS文章链接不能为空") } if (origin.isEmpty()) { return returnData.setErrorMsg("RSS文章来源不能为空") } var userNameSpace = getUserNameSpace(context) var rssSource = getRssSourceByURL(sourceUrl, userNameSpace) if (rssSource == null) { return returnData.setErrorMsg("RSS源不存在") } val rssArticle = RssArticle(origin = origin, link = link) var content = "" if (rssSource.ruleContent != null) { content = Rss.getContent(rssArticle, rssSource.ruleContent as String, rssSource, Debug) } return returnData.setData(content) } } ================================================ FILE: src/main/java/com/htmake/reader/api/controller/UserController.kt ================================================ package com.htmake.reader.api.controller import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssArticle import io.legado.app.model.webBook.WebBook import io.vertx.ext.web.Route import io.vertx.ext.web.Router import io.vertx.ext.web.RoutingContext import io.vertx.ext.web.handler.StaticHandler; import mu.KotlinLogging import com.htmake.reader.config.AppConfig import com.htmake.reader.config.BookConfig import io.legado.app.constant.DeepinkBookSource import com.htmake.reader.utils.error import com.htmake.reader.utils.success import com.htmake.reader.utils.getStorage import com.htmake.reader.utils.saveStorage import com.htmake.reader.utils.asJsonArray import com.htmake.reader.utils.asJsonObject import com.htmake.reader.utils.toDataClass import com.htmake.reader.utils.toMap import com.htmake.reader.utils.fillData import com.htmake.reader.utils.getWorkDir import com.htmake.reader.utils.getRandomString import com.htmake.reader.utils.genEncryptedPassword import com.htmake.reader.entity.User import com.htmake.reader.utils.SpringContextUtils import com.htmake.reader.utils.deleteRecursively import com.htmake.reader.utils.unzip import com.htmake.reader.utils.zip import com.htmake.reader.utils.jsonEncode import com.htmake.reader.utils.getRelativePath import com.htmake.reader.verticle.RestVerticle import com.htmake.reader.SpringEvent import org.springframework.stereotype.Component import io.vertx.core.json.JsonObject import io.vertx.core.json.JsonArray import io.vertx.core.http.HttpMethod import com.htmake.reader.api.ReturnData import io.legado.app.utils.MD5Utils import java.net.URLDecoder; import java.net.URLEncoder; import java.net.URL; import java.util.UUID; import io.vertx.ext.web.client.WebClient import org.springframework.beans.factory.annotation.Autowired import org.springframework.core.env.Environment import java.io.File import java.lang.Runtime import kotlin.collections.mutableMapOf import kotlin.system.measureTimeMillis import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.text.SimpleDateFormat; import io.legado.app.utils.EncoderUtils import io.legado.app.model.rss.Rss import org.springframework.scheduling.annotation.Scheduled import io.legado.app.model.localBook.LocalBook import java.nio.file.Paths import kotlinx.coroutines.withContext import kotlinx.coroutines.async import kotlinx.coroutines.Deferred import kotlinx.coroutines.CoroutineScope private val logger = KotlinLogging.logger {} class UserController(coroutineContext: CoroutineContext): BaseController(coroutineContext) { val userMaxCount = 50 private fun getUserLimit(context: RoutingContext): Int { if (context.request().host().equals("reader.htmake.com")) { return 500; } return Math.min(Math.max(appConfig.userLimit, 1), userMaxCount) } suspend fun login(context: RoutingContext): ReturnData { val returnData = ReturnData() val username = context.bodyAsJson.getString("username", "") ?: "" val password = context.bodyAsJson.getString("password", "") ?: "" val isLogin = context.bodyAsJson.getBoolean("isLogin", false) ?: false if (username.isNullOrEmpty()) { return returnData.setErrorMsg("请输入用户名") } if (password.isNullOrEmpty()) { return returnData.setErrorMsg("请输入密码") } var userMap = mutableMapOf>() var userMapJson: JsonObject? = asJsonObject(getStorage("data", "users")) if (userMapJson != null) { userMap = userMapJson.map as MutableMap> } var existedUser = userMap.getOrDefault(username, null) if (existedUser == null) { if (isLogin) { // 登录返回用户不存在 return returnData.setErrorMsg("用户不存在") } if (username.length < 5) { return returnData.setErrorMsg("用户名不能低于5位") } if (password.length < 8) { return returnData.setErrorMsg("密码不能低于8位") } if (username.equals("default")) { return returnData.setErrorMsg("用户名不能为非法字符") } val usernameReg = Regex("[a-z0-9]+", RegexOption.IGNORE_CASE) //忽略大小写 if (!usernameReg.matches(username)) { return returnData.setErrorMsg("用户名只能由字母和数字组成") } if (appConfig.inviteCode.isNotEmpty()) { // 需要填入邀请码才能注册 val code = context.bodyAsJson.getString("code") ?: "" if (code.isNullOrEmpty()) { return returnData.setErrorMsg("请输入邀请码") } if (!appConfig.inviteCode.equals(code)) { return returnData.setErrorMsg("邀请码错误") } } val userLimit = getUserLimit(context) if (userMap.keys.size >= userLimit) { return returnData.setErrorMsg("超过用户数上限") } // 自动注册 var salt = getRandomString(8) var passwordEncrypted = genEncryptedPassword(password, salt) var newUser = User(username, passwordEncrypted, salt) val loginData = saveUserSession(context, userMap, newUser) return returnData.setData(loginData) } else { if (!isLogin) { // 注册时返回用户名已被占用 return returnData.setErrorMsg("用户名已被占用") } // 登录 var userInfo: User? = existedUser.toDataClass() if (userInfo == null) { return returnData.setErrorMsg("用户信息错误") } var passwordEncrypted = genEncryptedPassword(password, userInfo.salt) if (passwordEncrypted != userInfo.password) { return returnData.setErrorMsg("密码错误") } val loginData = saveUserSession(context, userMap, userInfo) return returnData.setData(loginData) } } suspend fun logout(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (!appConfig.secure) { return returnData.setErrorMsg("不支持的操作") } var username = context.session().get("username") as String? ?: "" context.session().destroy() // 清除自动登录token var accessToken = context.queryParam("accessToken").firstOrNull() ?: "" if (accessToken.isNotEmpty()) { var tmp = accessToken.split(":", limit=2) if (tmp.size >= 2) { accessToken = tmp[1] var userMap = mutableMapOf>() var userMapJson: JsonObject? = asJsonObject(getStorage("data", "users")) if (userMapJson != null) { userMap = userMapJson.map as MutableMap> } var currentUser = userMap.getOrDefault(username, null) if (currentUser == null) { return returnData.setErrorMsg("系统错误") } var tokenMapVal = currentUser.getOrDefault("token_map", null) if (tokenMapVal != null) { var tokenMap: MutableMap? = tokenMapVal as MutableMap? if (tokenMap != null) { tokenMap.remove(accessToken) currentUser.put("token_map", tokenMap) } } if (currentUser.getOrDefault("token", "").equals(accessToken)) { currentUser.put("token", "") } userMap.put(username, currentUser) saveStorage("data", "users", value = userMap) } } return returnData.setErrorMsg("请重新登录").setData("NEED_LOGIN") } suspend fun getUserList(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (!appConfig.secure || appConfig.secureKey.isEmpty()) { return returnData.setErrorMsg("不支持的操作") } if (!checkManagerAuth(context)) { return returnData.setData("NEED_SECURE_KEY").setErrorMsg("请输入管理密码") } var userMap = mutableMapOf>() var userMapJson: JsonObject? = asJsonObject(getStorage("data", "users")) if (userMapJson != null) { userMap = userMapJson.map as MutableMap> } var userList = arrayListOf>() userMap.forEach{ userList.add(formatUser(it.value)) } return returnData.setData(userList) } suspend fun addUser(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (!appConfig.secure || appConfig.secureKey.isEmpty()) { return returnData.setErrorMsg("不支持的操作") } val username = context.bodyAsJson.getString("username") ?: "" val password = context.bodyAsJson.getString("password") ?: "" if (username.isNullOrEmpty()) { return returnData.setErrorMsg("请输入用户名") } if (password.isNullOrEmpty()) { return returnData.setErrorMsg("请输入密码") } if (username.length < 5) { return returnData.setErrorMsg("用户名不能低于5位") } if (password.length < 8) { return returnData.setErrorMsg("密码不能低于8位") } if (username.equals("default")) { return returnData.setErrorMsg("用户名不能为非法字符") } if (!checkManagerAuth(context)) { return returnData.setData("NEED_SECURE_KEY").setErrorMsg("请输入管理密码") } val usernameReg = Regex("[a-z0-9]+", RegexOption.IGNORE_CASE) //忽略大小写 if (!usernameReg.matches(username)) { return returnData.setErrorMsg("用户名只能由字母和数字组成") } var userMap = mutableMapOf>() var userMapJson: JsonObject? = asJsonObject(getStorage("data", "users")) if (userMapJson != null) { userMap = userMapJson.map as MutableMap> } var existedUser = userMap.getOrDefault(username, null) if (existedUser != null) { return returnData.setErrorMsg("用户已存在") } val userLimit = getUserLimit(context) if (userMap.keys.size >= userLimit) { return returnData.setErrorMsg("超过用户数上限") } // 自动注册 var salt = getRandomString(8) var passwordEncrypted = genEncryptedPassword(password, salt) var newUser = User(username, passwordEncrypted, salt) userMap.put(newUser.username, newUser.toMap()) saveStorage("data", "users", value = userMap) var userList = arrayListOf>() userMap.forEach{ userList.add(formatUser(it.value)) } return returnData.setData(userList) } suspend fun resetPassword(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (!appConfig.secure || appConfig.secureKey.isEmpty()) { return returnData.setErrorMsg("不支持的操作") } val username = context.bodyAsJson.getString("username") ?: "" val password = context.bodyAsJson.getString("password") ?: "" if (username.isNullOrEmpty()) { return returnData.setErrorMsg("请输入用户名") } if (password.isNullOrEmpty()) { return returnData.setErrorMsg("请输入密码") } if (password.length < 8) { return returnData.setErrorMsg("密码不能低于8位") } if (username.equals("default")) { return returnData.setErrorMsg("用户不存在") } if (!checkManagerAuth(context)) { return returnData.setData("NEED_SECURE_KEY").setErrorMsg("请输入管理密码") } var userMap = mutableMapOf>() var userMapJson: JsonObject? = asJsonObject(getStorage("data", "users")) if (userMapJson != null) { userMap = userMapJson.map as MutableMap> } var existedUser = userMap.getOrDefault(username, null) if (existedUser == null) { return returnData.setErrorMsg("用户不存在") } var salt = getRandomString(8) var passwordEncrypted = genEncryptedPassword(password, salt) existedUser.put("salt", salt) existedUser.put("password", passwordEncrypted) userMap.put(username, existedUser) saveStorage("data", "users", value = userMap as MutableMap>) return returnData.setData("") } suspend fun deleteUsers(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (!appConfig.secure || appConfig.secureKey.isEmpty()) { return returnData.setErrorMsg("不支持的操作") } if (!checkManagerAuth(context)) { return returnData.setData("NEED_SECURE_KEY").setErrorMsg("请输入管理密码") } var userMap = mutableMapOf>() var userMapJson: JsonObject? = asJsonObject(getStorage("data", "users")) if (userMapJson != null) { val userJsonArray = context.bodyAsJsonArray for (i in 0 until userJsonArray.size()) { var username = userJsonArray.getString(i) if (username != null && userMapJson.containsKey(username)) { // 删除用户信息 userMapJson.remove(username) // 移除用户目录 var userHome = File(getWorkDir("storage", "data", username)) logger.info("delete userHome: {}", userHome) if (userHome.exists()) { userHome.deleteRecursively() } } } userMap = userMapJson.map as MutableMap> saveStorage("data", "users", value = userMap) } var userList = arrayListOf>() userMap.forEach{ userList.add(formatUser(it.value)) } return returnData.setData(userList) } suspend fun updateUser(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (!appConfig.secure || appConfig.secureKey.isEmpty()) { return returnData.setErrorMsg("不支持的操作") } if (!checkManagerAuth(context)) { return returnData.setData("NEED_SECURE_KEY").setErrorMsg("请输入管理密码") } val username = context.bodyAsJson.getString("username") ?: "" val enableWebdav = context.bodyAsJson.getBoolean("enableWebdav") val enableLocalStore = context.bodyAsJson.getBoolean("enableLocalStore") if (username.isEmpty()) { return returnData.setErrorMsg("参数错误") } var userMap = mutableMapOf>() var userMapJson: JsonObject? = asJsonObject(getStorage("data", "users")) if (userMapJson != null) { userMap = userMapJson.map as MutableMap> var existedUser = userMap.getOrDefault(username, null) if (existedUser == null) { return returnData.setErrorMsg("用户不存在") } if (enableWebdav != null) { existedUser.put("enable_webdav", enableWebdav) } if (enableLocalStore != null) { existedUser.put("enable_local_store", enableLocalStore) } userMap.put(username, existedUser) saveStorage("data", "users", value = userMap) } var userList = arrayListOf>() userMap.forEach{ userList.add(formatUser(it.value)) } return returnData.setData(userList) } suspend fun getUserInfo(context: RoutingContext): ReturnData { val returnData = ReturnData() checkAuth(context) var username = context.session().get("username") as String? var secure = env.getProperty("reader.app.secure", Boolean::class.java) var secureKey = env.getProperty("reader.app.secureKey") var userInfo: Any? = null if (username != null) { var user = getUserInfoClass(username) if (user != null) { userInfo = formatUser(user) } } return returnData.setData(mapOf( "userInfo" to userInfo, "secure" to secure, "secureKey" to secureKey?.isNotEmpty() )) } suspend fun saveUserConfig(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val content = context.bodyAsJson if (content == null) { return returnData.setErrorMsg("参数错误") } content.put("@updateTime", System.currentTimeMillis()) val userNameSpace = getUserNameSpace(context) saveUserStorage(userNameSpace, "userConfig", content) return returnData.setData("") } suspend fun getUserConfig(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } val userNameSpace = getUserNameSpace(context) val userConfig = asJsonObject(getUserStorage(userNameSpace, "userConfig")) if (userConfig == null) { return returnData.setErrorMsg("没有备份文件") } return returnData.setData(userConfig.map) } suspend fun uploadFile(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (context.fileUploads() == null || context.fileUploads().isEmpty()) { return returnData.setErrorMsg("请上传文件") } var userNameSpace = getUserNameSpace(context) var fileList = JsonArray() var type = context.request().getParam("type") if (type.isNullOrEmpty()) { type = "images" } // logger.info("type: {}", type) context.fileUploads().forEach { var file = File(it.uploadedFileName()) logger.info("uploadFile: {} {} {}", it.uploadedFileName(), it.fileName(), file) if (file.exists()) { var fileName = it.fileName() var newFile = File(getWorkDir("storage", "assets", userNameSpace, type, fileName)) if (!newFile.parentFile.exists()) { newFile.parentFile.mkdirs() } if (newFile.exists()) { newFile.delete() } logger.info("moveTo: {}", newFile) if (file.copyRecursively(newFile)) { fileList.add("/assets/" + userNameSpace + "/" + type + "/" + fileName) } file.deleteRecursively() } } return returnData.setData(fileList.getList()) } suspend fun deleteFile(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } var url: String if (context.request().method() == HttpMethod.POST) { // post 请求 url = context.bodyAsJson.getString("url") ?: "" } else { // get 请求 url = context.queryParam("url").firstOrNull() ?: "" } if (url.isNullOrEmpty()) { return returnData.setErrorMsg("请输入文件链接") } var userNameSpace = getUserNameSpace(context) if (!url.startsWith("/assets/" + userNameSpace + "/")) { return returnData.setErrorMsg("文件链接错误") } var file = File(getWorkDir("storage" + url)) logger.info("delete file: {}", file) file.deleteRecursively() return returnData.setData("") } } ================================================ FILE: src/main/java/com/htmake/reader/api/controller/WebdavController.kt ================================================ package com.htmake.reader.api.controller import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssArticle import io.legado.app.model.webBook.WebBook import io.vertx.ext.web.Route import io.vertx.ext.web.Router import io.vertx.ext.web.RoutingContext import io.vertx.ext.web.handler.StaticHandler; import mu.KotlinLogging import com.htmake.reader.config.AppConfig import com.htmake.reader.config.BookConfig import io.legado.app.constant.DeepinkBookSource import com.htmake.reader.utils.error import com.htmake.reader.utils.success import com.htmake.reader.utils.getStorage import com.htmake.reader.utils.saveStorage import com.htmake.reader.utils.asJsonArray import com.htmake.reader.utils.asJsonObject import com.htmake.reader.utils.toDataClass import com.htmake.reader.utils.toMap import com.htmake.reader.utils.fillData import com.htmake.reader.utils.getWorkDir import com.htmake.reader.utils.getRandomString import com.htmake.reader.utils.genEncryptedPassword import com.htmake.reader.entity.User import com.htmake.reader.utils.SpringContextUtils import com.htmake.reader.utils.deleteRecursively import com.htmake.reader.utils.unzip import com.htmake.reader.utils.zip import com.htmake.reader.utils.jsonEncode import com.htmake.reader.utils.getRelativePath import com.htmake.reader.verticle.RestVerticle import com.htmake.reader.SpringEvent import org.springframework.stereotype.Component import io.vertx.core.json.JsonObject import io.vertx.core.json.JsonArray import io.vertx.core.http.HttpMethod import com.htmake.reader.api.ReturnData import io.legado.app.utils.MD5Utils import java.net.URLDecoder; import java.net.URLEncoder; import java.net.URL; import java.util.UUID; import io.vertx.ext.web.client.WebClient import org.springframework.beans.factory.annotation.Autowired import org.springframework.core.env.Environment import java.io.File import java.lang.Runtime import kotlin.collections.mutableMapOf import kotlin.system.measureTimeMillis import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.text.SimpleDateFormat; import io.legado.app.utils.EncoderUtils import io.legado.app.model.rss.Rss import org.springframework.scheduling.annotation.Scheduled import io.legado.app.model.localBook.LocalBook import java.nio.file.Paths import kotlinx.coroutines.withContext import kotlinx.coroutines.async import kotlinx.coroutines.Deferred import kotlinx.coroutines.CoroutineScope // import io.legado.app.help.coroutine.Coroutine private val logger = KotlinLogging.logger {} class WebdavController(coroutineContext: CoroutineContext, router: Router, onHandlerError: (RoutingContext, Exception) -> Unit): BaseController(coroutineContext) { init { // webdav 服务 router.route("/reader3/webdav*").handler { it.addHeadersEndHandler { _ -> var res = it.response() res.putHeader("DAV", "1,2") res.putHeader("Access-Control-Allow-Origin", "*") res.putHeader("Access-Control-Allow-Credentials", "true") res.putHeader("Access-Control-Expose-Headers", "DAV, content-length, Allow") res.putHeader("MS-Author-Via", "DAV") res.putHeader("Allow", "OPTIONS,DELETE,GET,PUT,PROPFIND,MKCOL,MOVE,COPY,LOCK,UNLOCK") if (appConfig.secure) { res.putHeader("WWW-Authenticate", "Basic realm=\"Default realm\"") } } val rawMethod = it.request().rawMethod() if (!checkAuthorization(it)) { if ( rawMethod.equals("PROPFIND") || rawMethod.equals("MKCOL") || rawMethod.equals("PUT") || rawMethod.equals("GET") || rawMethod.equals("DELETE") || rawMethod.equals("MOVE") || rawMethod.equals("COPY") || rawMethod.equals("LOCK") || rawMethod.equals("UNLOCK") ) { it.response().setStatusCode(401).end() return@handler } else if(rawMethod.equals("OPTIONS")) { var authorization = it.request().getHeader("Authorization") if (authorization != null) { it.response().setStatusCode(401).end() return@handler } } } when (rawMethod) { "PROPFIND" -> launch(Dispatchers.IO) { try { webdavList(it) } catch (e: Exception) { onHandlerError(it, e) } } "MKCOL" -> launch(Dispatchers.IO) { try { webdavMkdir(it) } catch (e: Exception) { onHandlerError(it, e) } } "PUT" -> launch(Dispatchers.IO) { try { webdavUpload(it) } catch (e: Exception) { onHandlerError(it, e) } } "GET" -> launch(Dispatchers.IO) { try { webdavDownload(it) } catch (e: Exception) { onHandlerError(it, e) } } "DELETE" -> launch(Dispatchers.IO) { try { webdavDelete(it) } catch (e: Exception) { onHandlerError(it, e) } } "MOVE" -> launch(Dispatchers.IO) { try { webdavMove(it) } catch (e: Exception) { onHandlerError(it, e) } } "COPY" -> launch(Dispatchers.IO) { try { webdavCopy(it) } catch (e: Exception) { onHandlerError(it, e) } } "LOCK" -> launch(Dispatchers.IO) { try { webdavLock(it) } catch (e: Exception) { onHandlerError(it, e) } } "UNLOCK" -> launch(Dispatchers.IO) { try { webdavUnLock(it) } catch (e: Exception) { onHandlerError(it, e) } } "OPTIONS" -> it.response().setStatusCode(200).end() else -> it.response().setStatusCode(405).end() } } } fun checkAuthorization(context: RoutingContext): Boolean { if (!appConfig.secure) { return true } var authorization = context.request().getHeader("Authorization") logger.info("authorization: {}", authorization) if (authorization == null || authorization.isEmpty()) { return false } // Basic YTox val auth = EncoderUtils.base64Decode(authorization.replace("Basic ", "", true)).split(":", limit=2) if (auth.size < 2) { return false } val username = auth[0] val password = auth[1] var userMap = mutableMapOf>() var userMapJson: JsonObject? = asJsonObject(getStorage("data", "users")) if (userMapJson != null) { userMap = userMapJson.map as MutableMap> } var existedUser = userMap.getOrDefault(username, null) if (existedUser == null) { return false } var userInfo: User? = existedUser.toDataClass() if (userInfo == null) { return false } var passwordEncrypted = genEncryptedPassword(password, userInfo.salt) if (passwordEncrypted != userInfo.password) { logger.info("user: {} password error", userInfo.username) return false } if (!userInfo.enable_webdav) { logger.info("user: {} enable_webdav: false", userInfo.username) return false } context.put("username", userInfo.username) return true } suspend fun webdavList(context: RoutingContext) { var home = getUserWebdavHome(context) var path = context.request().path().replace("/reader3/webdav/", "/", true) path = URLDecoder.decode(path, "UTF-8") var file = File(home + path) if (!file.exists()) { context.response().setStatusCode(404).end() return } var xml = """ %s """ var dirResponse = """ %s HTTP/1.1 200 OK %s %s %s """ var fileResponse = """ %s HTTP/1.1 200 OK %s %s %s %s %s """ var fileUrl = context.request().absoluteURI() // 只支持一级 var formatter = { f: File, url: String, showName: Boolean -> var name = if(showName) f.name else "" var modifiedDate = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(f.lastModified()) if (f.isFile()) { String.format(fileResponse, url, modifiedDate, modifiedDate, name, f.length(), "") } else { String.format(dirResponse, url, modifiedDate, modifiedDate, name) } } var response = "" if (file.isFile()) { response = String.format(xml, formatter(file, fileUrl, true)) context.response().setStatusCode(207).end(response) return } if (file.isDirectory()) { fileUrl = if (fileUrl.endsWith("/")) fileUrl else fileUrl + "/" response = formatter(file, fileUrl, false) file.listFiles().forEach { val fileName = URLEncoder.encode(it.name, "UTF-8") response = response + formatter(it, fileUrl + fileName, true) } response = String.format(xml, response) context.response().setStatusCode(207).end(response) return } context.response().setStatusCode(404).end() } suspend fun webdavMkdir(context: RoutingContext) { var home = getUserWebdavHome(context) var path = context.request().path().replace("/reader3/webdav/", "/", true) path = URLDecoder.decode(path, "UTF-8") var file = File(home + path) if (file.exists()) { // 文件夹存在时,返回成功 context.response().setStatusCode(201).end() return } try { file.mkdirs() context.response().setStatusCode(201).end() } catch(e: Exception) { context.response().setStatusCode(500).end() } } suspend fun webdavUpload(context: RoutingContext) { var home = getUserWebdavHome(context) var path = context.request().path().replace("/reader3/webdav/", "/", true) path = URLDecoder.decode(path, "UTF-8") var file = File(home + path) if (!file.parentFile.exists()) { context.response().setStatusCode(409).end() return } if (file.isDirectory()) { context.response().setStatusCode(405).end() return } if (file.exists()) { file.delete(); } try { file.writeBytes(context.getBody().getBytes()) // 同步用户进度 if (file.toString().indexOf("/bookProgress/") > 0 && file.toString().indexOf(".json") > 0) { val userNameSpace = getUserNameSpace(context) BookController(coroutineContext).syncBookProgressFromWebdav(file, userNameSpace) } context.response().setStatusCode(201).end() } catch(e: Exception) { context.response().setStatusCode(500).end() } } suspend fun webdavDownload(context: RoutingContext) { var home = getUserWebdavHome(context) var path = context.request().path().replace("/reader3/webdav/", "/", true) path = URLDecoder.decode(path, "UTF-8") var file = File(home + path) if (!file.exists()) { context.response().setStatusCode(404).end() return } if (file.isDirectory()) { context.response().setStatusCode(405).end() return } context.response().putHeader("Cache-Control", "86400") .putHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(file.name, "UTF-8")) .sendFile(file.toString()) } suspend fun webdavDelete(context: RoutingContext) { var home = getUserWebdavHome(context) var path = context.request().path().replace("/reader3/webdav/", "/", true) path = URLDecoder.decode(path, "UTF-8") var file = File(home + path) if (!file.exists()) { context.response().setStatusCode(404).end() return } file.deleteRecursively() context.response().setStatusCode(200).end() } suspend fun webdavMove(context: RoutingContext) { var home = getUserWebdavHome(context) var path = context.request().path().replace("/reader3/webdav/", "/", true) path = URLDecoder.decode(path, "UTF-8") var file = File(home + path) if (!file.exists()) { context.response().setStatusCode(412).end() return } var destination = context.request().getHeader("Destination") if (destination == null) { context.response().setStatusCode(400).end() return } var destinationUrl = URL(destination) destination = destinationUrl.path?.replace("/reader3/webdav/", "/", true) if (destination == null) { context.response().setStatusCode(400).end() return } var overwrite = context.request().getHeader("Overwrite") var destinationFile = File(home + URLDecoder.decode(destination, "UTF-8")) if (destinationFile.exists()) { if (overwrite == null || overwrite.isEmpty()) { context.response().setStatusCode(412).end() return } destinationFile.deleteRecursively() } file.renameTo(destinationFile) context.response().setStatusCode(201).end() } suspend fun webdavCopy(context: RoutingContext) { var home = getUserWebdavHome(context) var path = context.request().path().replace("/reader3/webdav/", "/", true) path = URLDecoder.decode(path, "UTF-8") var file = File(home + path) if (!file.exists()) { context.response().setStatusCode(412).end() return } var destination = context.request().getHeader("Destination") if (destination == null) { context.response().setStatusCode(400).end() return } var destinationUrl = URL(destination) destination = destinationUrl.path?.replace("/reader3/webdav/", "/", true) if (destination == null) { context.response().setStatusCode(400).end() return } var overwrite = context.request().getHeader("Overwrite") var destinationFile = File(home + URLDecoder.decode(destination, "UTF-8")) if (destinationFile.exists()) { if (overwrite == null || overwrite.isEmpty()) { context.response().setStatusCode(412).end() return } destinationFile.deleteRecursively() } file.copyRecursively(destinationFile) context.response().setStatusCode(201).end() } suspend fun webdavLock(context: RoutingContext) { var response = """ %s %s infinity http://www.apple.com/webdav_fs/ %s """ var lockToken = "urn:uuid:" + UUID.randomUUID().toString() var timeout = context.request().getHeader("Timeout") if (timeout == null) { timeout = "Second-3600" } var fileUrl = context.request().absoluteURI() context.response().putHeader("Lock-Token", lockToken).setStatusCode(200).end(String.format(response, lockToken, fileUrl, timeout)) } suspend fun webdavUnLock(context: RoutingContext) { var lockToken = context.request().getHeader("Lock-Token") if (lockToken == null) { context.response().setStatusCode(400).end() return } context.response().putHeader("Lock-Token", lockToken).setStatusCode(204).end() } suspend fun getWebdavFileList(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (appConfig.secure) { var userInfo = context.get("userInfo") as User? if (userInfo == null) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (!userInfo.enable_webdav) { return returnData.setErrorMsg("未开启webdav功能") } } var path: String if (context.request().method() == HttpMethod.POST) { // post 请求 path = context.bodyAsJson.getString("path") ?: "" } else { // get 请求 path = context.queryParam("path").firstOrNull() ?: "" path = URLDecoder.decode(path, "UTF-8") } if (path.isEmpty()) { path = "/" } var home = getUserWebdavHome(context) var file = File(home + path) logger.info("file: {} {}", path, file) if (!file.exists()) { return returnData.setErrorMsg("路径不存在") } if (!file.isDirectory()) { return returnData.setErrorMsg("路径不是目录") } var fileList = arrayListOf>() file.listFiles().forEach{ if (!it.name.startsWith(".")) { fileList.add(mapOf( "name" to it.name, "size" to it.length(), "path" to it.toString().replace(home, ""), "lastModified" to it.lastModified(), "isDirectory" to it.isDirectory() )) } } return returnData.setData(fileList) } suspend fun getWebdavFile(context: RoutingContext) { val returnData = ReturnData() if (!checkAuth(context)) { context.success(returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用")) return } if (appConfig.secure) { var userInfo = context.get("userInfo") as User? if (userInfo == null) { context.success(returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用")) return } if (!userInfo.enable_webdav) { context.success(returnData.setErrorMsg("未开启webdav功能")) return } } var path: String if (context.request().method() == HttpMethod.POST) { // post 请求 path = context.bodyAsJson.getString("path") ?: "" } else { // get 请求 path = context.queryParam("path").firstOrNull() ?: "" path = URLDecoder.decode(path, "UTF-8") } if (path.isEmpty()) { context.success(returnData.setErrorMsg("参数错误")) return } var home = getUserWebdavHome(context) var file = File(home + path) logger.info("file: {} {}", path, file) if (!file.exists()) { context.success(returnData.setErrorMsg("路径不存在")) return } context.response() .putHeader("Cache-Control", "86400") .putHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(file.name, "UTF-8")) .sendFile(file.toString()) } suspend fun uploadFileToWebdav(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (context.fileUploads() == null || context.fileUploads().isEmpty()) { return returnData.setErrorMsg("请上传文件") } if (appConfig.secure) { var userInfo = context.get("userInfo") as User? if (userInfo == null) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (!userInfo.enable_webdav) { return returnData.setErrorMsg("未开启webdav功能") } } var path = context.request().getParam("path") if (path.isNullOrEmpty()) { path = "/" } var fileList = arrayListOf>() var home = getUserWebdavHome(context) + path + File.separator // logger.info("type: {}", type) context.fileUploads().forEach { var file = File(it.uploadedFileName()) logger.info("uploadFile: {} {} {}", it.uploadedFileName(), it.fileName(), file) if (file.exists()) { var fileName = it.fileName() var newFile = File(home + fileName) if (!newFile.parentFile.exists()) { newFile.parentFile.mkdirs() } if (newFile.exists()) { newFile.delete() } logger.info("moveTo: {}", newFile) if (file.copyRecursively(newFile)) { fileList.add(mapOf( "name" to newFile.name, "size" to newFile.length(), "path" to newFile.toString().replace(home, ""), "lastModified" to newFile.lastModified(), "isDirectory" to newFile.isDirectory() )) } file.deleteRecursively() } } return returnData.setData(fileList) } suspend fun deleteWebdavFile(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (appConfig.secure) { var userInfo = context.get("userInfo") as User? if (userInfo == null) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (!userInfo.enable_webdav) { return returnData.setErrorMsg("未开启webdav功能") } } var path: String if (context.request().method() == HttpMethod.POST) { // post 请求 path = context.bodyAsJson.getString("path") ?: "" } else { // get 请求 path = context.queryParam("path").firstOrNull() ?: "" path = URLDecoder.decode(path, "UTF-8") } if (path.isEmpty()) { return returnData.setErrorMsg("参数错误") } var home = getUserWebdavHome(context) var file = File(home + path) logger.info("file: {} {}", path, file) if (!file.exists()) { return returnData.setErrorMsg("路径不存在") } file.deleteRecursively() return returnData.setData("") } suspend fun deleteWebdavFileList(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (appConfig.secure) { var userInfo = context.get("userInfo") as User? if (userInfo == null) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (!userInfo.enable_webdav) { return returnData.setErrorMsg("未开启webdav功能") } } var path = context.bodyAsJson.getJsonArray("path") if (path == null) { return returnData.setErrorMsg("参数错误") } var home = getUserWebdavHome(context) path.forEach { var filePath = URLDecoder.decode(it as String? ?: "", "UTF-8") if (filePath.isNotEmpty()) { var file = File(home + filePath) file.deleteRecursively() } } return returnData.setData("") } suspend fun restoreFromWebdav(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (appConfig.secure) { var userInfo = context.get("userInfo") as User? if (userInfo == null) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (!userInfo.enable_webdav) { return returnData.setErrorMsg("未开启webdav功能") } } var path: String if (context.request().method() == HttpMethod.POST) { // post 请求 path = context.bodyAsJson.getString("path") ?: "" } else { // get 请求 path = context.queryParam("path").firstOrNull() ?: "" path = URLDecoder.decode(path, "UTF-8") } if (path.isEmpty()) { path = "/" } var ext = getFileExt(path) if (ext != "zip") { return returnData.setErrorMsg("路径不是zip备份文件") } var home = getUserWebdavHome(context) var file = File(home + path) logger.info("file: {} {}", path, file) if (!file.exists()) { return returnData.setErrorMsg("路径不存在") } val bookController = BookController(coroutineContext) if (!bookController.syncFromWebdav(file.toString(), getUserNameSpace(context))) { return returnData.setErrorMsg("恢复失败") } return returnData.setData("") } suspend fun backupToWebdav(context: RoutingContext): ReturnData { val returnData = ReturnData() if (!checkAuth(context)) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (appConfig.secure) { var userInfo = context.get("userInfo") as User? if (userInfo == null) { return returnData.setData("NEED_LOGIN").setErrorMsg("请登录后使用") } if (!userInfo.enable_webdav) { return returnData.setErrorMsg("未开启webdav功能") } } val bookController = BookController(coroutineContext) val userNameSpace = getUserNameSpace(context) var latestZipFilePath = bookController.getLastBackFileFromWebdav(userNameSpace) if (latestZipFilePath == null) { return returnData.setErrorMsg("请先使用阅读App备份到webdav") } if (!bookController.saveToWebdav(latestZipFilePath, userNameSpace)) { return returnData.setErrorMsg("备份失败") } return returnData.setData("") } } ================================================ FILE: src/main/java/com/htmake/reader/config/AppConfig.kt ================================================ package com.htmake.reader.config import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.stereotype.Component @Component @ConfigurationProperties(prefix = "reader.app") class AppConfig { lateinit var storagePath: String // 存储路径 var showUI = false // 是否显示UI var debug = false // 是否调试web var packaged = false // 是否打包为app var secure = false // 是否启用登录鉴权 var inviteCode = "" // 注册邀请码 var secureKey = "" // 管理密码 var cacheChapterContent = false // 是否缓存章节内容 var userLimit = 50 // 用户上限 var userBookLimit = 200 // 用户书籍上限 var debugLog = false // 调试日志 var autoClearInactiveUser = 0 // 自动清理不活跃用户 var exportUseReplace = false // 导出不使用净化 var exportCharset = "UTF-8" // 导出字符集 var exportNoChapterName = false // 不添加章节名 var exportPictureFile = false // 导出图片 } ================================================ FILE: src/main/java/com/htmake/reader/config/BookConfig.kt ================================================ package com.htmake.reader.config import java.io.File object BookConfig { val javascriptVersion = "reader-inject-javascript-1.1.0" val epubInjectJavascript = """ // """ fun injectJavascriptToEpubChapter(filePath: String) { val file = File(filePath); if (file.exists()) { var content = file.readText() if (content.indexOf(javascriptVersion) < 0) { content = content.replace("", """""") file.writeText(content) } } } } ================================================ FILE: src/main/java/com/htmake/reader/entity/BasicError.kt ================================================ package com.htmake.reader.entity data class BasicError( val error: String, val exception: String, val message: String, val path: String, val status: Int, val timestamp: Long ) ================================================ FILE: src/main/java/com/htmake/reader/entity/Size.kt ================================================ package com.htmake.reader.entity data class Size( val width: Double, val height: Double ) ================================================ FILE: src/main/java/com/htmake/reader/entity/User.kt ================================================ package com.htmake.reader.entity data class User( var username: String="", var password: String="", var salt: String="", var token: String="", var last_login_at: Long = System.currentTimeMillis(), var created_at: Long = System.currentTimeMillis(), var enable_webdav: Boolean = false, // 是否开启 WebDAV 功能 var token_map: Map? = null, var enable_local_store: Boolean = false // 是否开启本地书仓功能 ) ================================================ FILE: src/main/java/com/htmake/reader/init/appCtx.kt ================================================ package com.htmake.reader.init import com.htmake.reader.utils.getWorkDir // 处理 appCtx object appCtx { val cacheDir: String by lazy { getWorkDir("storage", "cache") } } ================================================ FILE: src/main/java/com/htmake/reader/utils/Ext.kt ================================================ package com.htmake.reader.utils import io.vertx.core.buffer.Buffer import io.vertx.ext.web.client.HttpRequest import io.vertx.ext.web.client.WebClient import okhttp3.HttpUrl.Companion.toHttpUrl import java.io.File import java.io.OutputStream import java.io.InputStream import org.xml.sax.InputSource import java.io.FileOutputStream import java.io.FileInputStream import java.util.zip.ZipFile import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream import org.w3c.dom.*; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; /** * @Date: 2019-07-19 23:43 * @Description: */ fun String.url(): String { if (this.startsWith("//")) { return ("http:" + this).toHttpUrl().toString() } else if (this.startsWith("http")) { return this.toHttpUrl().toString() } return this } fun WebClient.getEncodeAbs(absoluteURI: String): HttpRequest { return this.getAbs(absoluteURI.toHttpUrl().toString()) } fun File.deleteRecursively() { if (this.exists()) { if (this.isFile() ) { this.delete(); } else { this.listFiles().forEach{ it.deleteRecursively() } this.delete() } } } fun File.unzip(descDir: String): Boolean { if (!this.exists()) { return false } val buffer = ByteArray(1024) var outputStream: OutputStream? = null var inputStream: InputStream? = null try { val zf = ZipFile(this.toString()) val entries = zf.entries() while (entries.hasMoreElements()) { val zipEntry: ZipEntry = entries.nextElement() as ZipEntry val zipEntryName: String = zipEntry.name val descFilePath: String = descDir + File.separator + zipEntryName if (zipEntry.isDirectory) { createDir(descFilePath) } else { inputStream = zf.getInputStream(zipEntry) val descFile: File = createFile(descFilePath) outputStream = FileOutputStream(descFile) var len: Int while (inputStream.read(buffer).also { len = it } > 0) { outputStream.write(buffer, 0, len) } inputStream.close() outputStream.close() } } return true } catch(e: Exception) { e.printStackTrace() } finally { inputStream?.close() outputStream?.close() } return false } fun File.zip(zipFilePath: String): Boolean { if (!this.exists()) { return false } if (this.isDirectory()) { val files = this.listFiles() val filesList: List = files.toList() return zip(filesList, zipFilePath) } else { return zip(arrayListOf(this), zipFilePath) } } fun zip(files: List, zipFilePath: String): Boolean { if (files.isEmpty()) { return false } val zipFile = createFile(zipFilePath) val buffer = ByteArray(1024) var zipOutputStream: ZipOutputStream? = null var inputStream: FileInputStream? = null try { zipOutputStream = ZipOutputStream(FileOutputStream(zipFile)) for (file in files) { if (!file.exists()) continue zipOutputStream.putNextEntry(ZipEntry(file.name)) inputStream = FileInputStream(file) var len: Int while (inputStream.read(buffer).also { len = it } > 0) { zipOutputStream.write(buffer, 0, len) } zipOutputStream.closeEntry() } return true } catch(e: Exception) { e.printStackTrace() } finally { inputStream?.close() zipOutputStream?.close() } return false } fun createDir(filePath: String): File { val file = File(filePath) if (!file.exists()) { file.mkdirs() } return file } fun createFile(filePath: String): File { val file = File(filePath) val parentFile = file.parentFile!! if (!parentFile.exists()) { parentFile.mkdirs() } if (!file.exists()) { file.createNewFile() } return file } fun getFileExtetion(url: String, defaultExt: String=""): String { try { var seqs = url.split("?", ignoreCase = true, limit = 2) var file = seqs[0].split("/").last() val dotPos = file.lastIndexOf('.') return if (0 <= dotPos) { file.substring(dotPos + 1) } else { defaultExt } } catch (e: Exception) { return defaultExt } } fun xml2map(source: Any): MutableMap { //1.创建DocumentBuilderFactory对象 val factory = DocumentBuilderFactory.newInstance() //2.创建DocumentBuilder对象 var doc = mutableMapOf() try { val builder = factory.newDocumentBuilder() // val document = builder.parse(filePath) when { source is String -> { val document = builder.parse(source as String) return parseNode(document.getChildNodes()) } source is InputStream -> { val document = builder.parse(source as InputStream) return parseNode(document.getChildNodes()) } source is InputSource -> { val document = builder.parse(source as InputSource) return parseNode(document.getChildNodes()) } else -> { return doc } } } catch (e: Exception) { e.printStackTrace() return doc } } fun parseNode(list: NodeList): MutableMap { var doc = mutableMapOf() for (i in 0 until list.getLength()) { val node = list.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { val childNodes = node.getChildNodes() // // logger.info("index: {} node: {} type: {} childNodesLength: {}", i, node, node.getNodeType(), childNodes.getLength()) if (childNodes.getLength() == 1 && node.getFirstChild().getNodeType() == Node.TEXT_NODE) { doc.put(node.getNodeName(), node.getFirstChild().getNodeValue()) } else if(childNodes.getLength() > 1) { doc.put(node.getNodeName(), parseNode(childNodes)) } } } return doc } ================================================ FILE: src/main/java/com/htmake/reader/utils/SpringContextUtils.java ================================================ package com.htmake.reader.utils; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; @Component public class SpringContextUtils implements ApplicationContextAware { /** * 上下文对象实例 */ private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext context) throws BeansException { applicationContext = context; } /** * 获取applicationContext * * @return */ public static ApplicationContext getApplicationContext() { return applicationContext; } /** * 通过name获取 Bean. * * @param name * @return */ public static Object getBean(String name) { if (applicationContext != null) { return getApplicationContext().getBean(name); } return null; } /** * 通过class获取Bean. * * @param clazz * @param * @return */ public static T getBean(Class clazz) { if (applicationContext != null) { return getApplicationContext().getBean(clazz); } return null; } /** * 通过name,以及Clazz返回指定的Bean * * @param name * @param clazz * @param * @return */ public static T getBean(String name, Class clazz) { if (applicationContext != null) { return getApplicationContext().getBean(name, clazz); } return null; } } ================================================ FILE: src/main/java/com/htmake/reader/utils/VertExt.kt ================================================ package com.htmake.reader.utils import com.google.common.base.Throwables import com.google.gson.Gson import com.google.gson.GsonBuilder import io.vertx.core.Handler import io.vertx.core.json.JsonObject import io.vertx.core.json.JsonArray import io.vertx.ext.web.RoutingContext import mu.KotlinLogging import com.htmake.reader.entity.BasicError import java.net.URLDecoder import java.net.URLEncoder import java.io.File import java.nio.file.Paths import com.htmake.reader.config.AppConfig import com.google.gson.reflect.TypeToken import kotlin.reflect.KProperty1 import kotlin.reflect.KMutableProperty import kotlin.reflect.full.memberProperties import io.legado.app.data.entities.Book import io.legado.app.utils.MD5Utils /** * @Auther: zoharSoul * @Date: 2019-05-21 16:17 * @Description: */ val logger = KotlinLogging.logger {} val gson = GsonBuilder().disableHtmlEscaping().create() val prettyGson = GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create() var storageFinalPath = "" var workDirPath = "" var workDirInit = false fun RoutingContext.success(any: Any?) { val toJson: String = if (any is JsonObject) { any.toString() } else { gson.toJson(any) } this.response() .putHeader("content-type", "application/json; charset=utf-8") .end(toJson) } fun RoutingContext.error(throwable: Throwable) { val path = URLDecoder.decode(this.request().absoluteURI(), "UTF-8") val basicError = BasicError( "Internal Server Error", throwable.toString(), throwable.message.toString(), path, 500, System.currentTimeMillis() ) val errorJson = gson.toJson(basicError) logger.error("Internal Server Error", throwable) logger.error { errorJson } this.response() .putHeader("content-type", "application/json; charset=utf-8") .setStatusCode(500) .end(errorJson) } fun getWorkDir(subPath: String = ""): String { if (!workDirInit && workDirPath.isEmpty()) { var osName = System.getProperty("os.name") var currentDir = System.getProperty("user.dir") logger.info("osName: {} currentDir: {}", osName, currentDir) // MacOS 存放目录为用户目录 if (osName.startsWith("Mac OS", true) && !currentDir.startsWith("/Users/")) { workDirPath = Paths.get(System.getProperty("user.home"), ".reader").toString() } else { workDirPath = currentDir } workDirInit = true } var path = Paths.get(workDirPath, subPath); return path.toString(); } fun getWorkDir(vararg subDirFiles: String): String { return getWorkDir(getRelativePath(*subDirFiles)) } fun getRelativePath(vararg subDirFiles: String): String { val path = StringBuilder("") subDirFiles.forEach { if (it.isNotEmpty()) { path.append(File.separator).append(it) } } return path.toString().let{ if (it.startsWith("/")) { it.substring(1) } else { it } } } fun getStoragePath(): String { if (storageFinalPath.isNotEmpty()) { return storageFinalPath; } var appConfig = SpringContextUtils.getBean("appConfig", AppConfig::class.java) var storageDir = File("storage") if (appConfig != null) { // logger.info("storagePath from appConfig: {}", appConfig.storagePath) storageDir = File(appConfig.storagePath) } if (storageDir.isAbsolute()) { return storageDir.toString(); } var storagePath = getWorkDir(storageDir.toString()) if (appConfig != null) { storageFinalPath = storagePath } return storagePath; } fun saveStorage(vararg name: String, value: Any, pretty: Boolean = false) { val toJson: String = if (value is JsonObject || value is JsonArray) { value.toString() } else if (pretty) { prettyGson.toJson(value) } else { gson.toJson(value) } var storagePath = getStoragePath() var storageDir = File(storagePath) if (!storageDir.exists()) { storageDir.mkdirs() } val filename = name.last() val file = File(getRelativePath(storagePath, *name.copyOfRange(0, name.size - 1), "${filename}.json")) // val file = File(storagePath + "/${name}.json") logger.info("Save file to storage name: {} path: {}", name, file.absoluteFile) if (!file.parentFile.exists()) { file.parentFile.mkdirs() } if (!file.exists()) { file.createNewFile() } file.writeText(toJson) } fun getStorage(vararg name: String): String? { var storagePath = getStoragePath() var storageDir = File(storagePath) if (!storageDir.exists()) { storageDir.mkdirs() } val filename = name.last() val file = File(getRelativePath(storagePath, *name.copyOfRange(0, name.size - 1), "${filename}.json")) logger.info("Read file from storage name: {} path: {}", name, file.absoluteFile) if (!file.exists()) { return null } return file.readText() } fun asJsonArray(value: Any?): JsonArray? { if (value is JsonArray) { return value } else if (value is String) { return JsonArray(value) } return null } fun asJsonObject(value: Any?): JsonObject? { if (value is JsonObject) { return value } else if (value is String) { return JsonObject(value) } return null } //convert a data class to a map fun T.serializeToMap(): Map { return convert() } //convert string to a map fun T.toMap(): Map { return convert() } //convert a map to a data class inline fun Map.toDataClass(): T { return convert() } //convert an object of type I to type O inline fun I.convert(): O { val json = if (this is String) { this } else { gson.toJson(this) } return gson.fromJson(json, object : TypeToken() {}.type) } @Suppress("UNCHECKED_CAST") fun readInstanceProperty(instance: Any, propertyName: String): R { val property = instance::class.memberProperties // don't cast here to , it would succeed silently .first { it.name == propertyName } as KProperty1 // force a invalid cast exception if incorrect type here return property.get(instance) as R } @Suppress("UNCHECKED_CAST") fun setInstanceProperty(instance: Any, propertyName: String, propertyValue: Any) { val property = instance::class.memberProperties .first { it.name == propertyName } if(property is KMutableProperty<*>) { property.setter.call(instance, propertyValue) } } fun Book.fillData(newBook: Book, keys: List): Book { keys.let { for (key in it) { var current = readInstanceProperty(this, key) if (current.isNullOrEmpty()) { var cacheValue = readInstanceProperty(newBook, key) if (!cacheValue.isNullOrEmpty()) { setInstanceProperty(this, key, cacheValue) } } } } return this } fun getRandomString(length: Int) : String { val allowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz0123456789" return (1..length) .map { allowedChars.random() } .joinToString("") } fun genEncryptedPassword(password: String, salt: String): String { return MD5Utils.md5Encode( MD5Utils.md5Encode(password + salt).toString() + salt ).toString() } fun jsonEncode(value: Any, pretty: Boolean = false): String { if (pretty) { return prettyGson.toJson(value) } return gson.toJson(value) } ================================================ FILE: src/main/java/com/htmake/reader/verticle/RestVerticle.kt ================================================ package com.htmake.reader.verticle import io.vertx.core.http.HttpMethod import io.vertx.ext.web.Route import io.vertx.ext.web.Router import io.vertx.ext.web.RoutingContext import io.vertx.ext.web.handler.BodyHandler import io.vertx.ext.web.handler.CorsHandler import io.vertx.ext.web.handler.LoggerFormat import io.vertx.ext.web.handler.LoggerHandler import io.vertx.ext.web.handler.SessionHandler import io.vertx.ext.web.sstore.LocalSessionStore import io.vertx.kotlin.coroutines.CoroutineVerticle import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import mu.KotlinLogging import com.htmake.reader.utils.error import com.htmake.reader.utils.success import java.net.URLDecoder private val logger = KotlinLogging.logger {} abstract class RestVerticle : CoroutineVerticle() { protected lateinit var router: Router open var port: Int = 8080 override suspend fun start() { super.start() router = Router.router(vertx) val cookieName = "reader.session" router.route().handler( SessionHandler.create(LocalSessionStore.create(vertx)) .setSessionCookieName(cookieName) .setSessionTimeout(7L * 86400 * 1000) .setSessionCookiePath("/") ); router.route().handler { it.addHeadersEndHandler { _ -> val cookie = it.getCookie(cookieName) if (cookie != null) { // 每次访问都延长cookie有效期 cookie.setMaxAge(2L * 86400 * 1000) cookie.setPath("/") } } it.next() } // CORS support router.route().handler { it.addHeadersEndHandler { _ -> val origin = it.request().getHeader("Origin") if (origin != null && origin.isNotEmpty()) { var res = it.response() res.putHeader("Access-Control-Allow-Origin", origin) res.putHeader("Access-Control-Allow-Credentials", "true") res.putHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, PUT, DELETE") res.putHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With") } } val origin = it.request().getHeader("Origin") if (origin != null && origin.isNotEmpty() && it.request().method() == HttpMethod.OPTIONS) { it.removeCookie(cookieName) it.success("") } else { it.next() } } router.route().handler(BodyHandler.create()) router.route().handler(LoggerHandler.create(LoggerFormat.DEFAULT)); router.route("/reader3/*").handler { logger.info("{} {}", it.request().rawMethod(), URLDecoder.decode(it.request().absoluteURI(), "UTF-8")) if (!it.request().rawMethod().equals("PUT") && (it.fileUploads() == null || it.fileUploads().isEmpty()) && it.bodyAsString.length > 0 && it.bodyAsString.length < 1000) { logger.info("Request body: {}", it.bodyAsString) } it.next() } router.get("/health").handler { it.success("ok!") } initRouter(router) // router.errorHandler(500) { routerContext -> // logger.error { routerContext.failure().message } // routerContext.error(routerContext.failure()) // } router.route().last().failureHandler { ctx -> ctx.error(ctx.failure()) } logger.info("port: {}", port) vertx.createHttpServer().requestHandler(router).exceptionHandler{error -> onException(error) }.listen(port) { res -> if (res.succeeded()) { logger.info("Server running at: http://localhost:{}", port); logger.info("Web reader running at: http://localhost:{}", port); started(); } else { onStartError(); } } } abstract suspend fun initRouter(router: Router); open fun onException(error: Throwable) { logger.error("vertx exception: {}", error) } open fun onStartError() { } open fun started() { } open fun onHandlerError(ctx: RoutingContext, error: Exception) { logger.error("Error: {}", error) ctx.error(error) } /** * An extension method for simplifying coroutines usage with Vert.x Web routers */ fun Route.coroutineHandler(fn: suspend (RoutingContext) -> Any) { handler { ctx -> val job = launch(Dispatchers.IO) { try { ctx.success(fn(ctx)) // fn(ctx) } catch (e: Exception) { onHandlerError(ctx, e) } } } } fun Route.coroutineHandlerWithoutRes(fn: suspend (RoutingContext) -> Any) { handler { ctx -> val job = launch(Dispatchers.IO) { try { fn(ctx) } catch (e: Exception) { onHandlerError(ctx, e) } } } } } ================================================ FILE: src/main/java/io/legado/app/README.md ================================================ # 文件结构介绍 * constant 常量 * data 数据 * help 帮助 * lib 库 * model 解析 ================================================ FILE: src/main/java/io/legado/app/constant/Action.kt ================================================ package io.legado.app.constant object Action { const val play = "play" const val stop = "stop" const val resume = "resume" const val pause = "pause" const val addTimer = "addTimer" const val setTimer = "setTimer" const val prevParagraph = "prevParagraph" const val nextParagraph = "nextParagraph" const val upTtsSpeechRate = "upTtsSpeechRate" const val adjustProgress = "adjustProgress" const val prev = "prev" const val next = "next" const val moveTo = "moveTo" const val init = "init" } ================================================ FILE: src/main/java/io/legado/app/constant/AppConst.kt ================================================ package io.legado.app.constant import java.text.SimpleDateFormat import com.script.javascript.RhinoScriptEngine object AppConst { const val UA_NAME = "User-Agent" val userAgent: String by lazy { "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" } val SCRIPT_ENGINE: RhinoScriptEngine by lazy { RhinoScriptEngine() } val TIME_FORMAT: SimpleDateFormat by lazy { SimpleDateFormat("HH:mm") } val timeFormat: SimpleDateFormat by lazy { SimpleDateFormat("HH:mm") } val dateFormat: SimpleDateFormat by lazy { SimpleDateFormat("yyyy/MM/dd HH:mm") } val fileNameFormat: SimpleDateFormat by lazy { SimpleDateFormat("yy-MM-dd-HH-mm-ss") } val keyboardToolChars: List by lazy { arrayListOf( "@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\", "$", "#", "!", ".", "href", "src", "textNodes", "xpath", "json", "css", "id", "class", "tag" ) } } ================================================ FILE: src/main/java/io/legado/app/constant/AppPattern.kt ================================================ package io.legado.app.constant import java.util.regex.Pattern object AppPattern { val JS_PATTERN: Pattern = Pattern.compile("([\\w\\W]*?)|@js:([\\w\\W]*)", Pattern.CASE_INSENSITIVE) val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}") //匹配格式化后的图片格式 val imgPattern: Pattern = Pattern.compile("]*src=\"([^\"]*(?:\"[^>]+\\})?)\"[^>]*>") //dataURL图片类型 val dataUriRegex = Regex("data:.*?;base64,(.*)") val nameRegex = Regex("\\s+作\\s*者.*|\\s+\\S+\\s+著") val authorRegex = Regex("^\\s*作\\s*者[::\\s]+|\\s+著") val fileNameRegex = Regex("[\\\\/:*?\"<>|.]") val splitGroupRegex = Regex("[,;,;]") //书源调试信息中的各种符号 val debugMessageSymbolRegex = Regex("[⇒◇┌└≡]") //本地书籍支持类型 val bookFileRegex = Regex(".*\\.(txt|epub|umd)", RegexOption.IGNORE_CASE) /** * 所有标点 */ val bdRegex = Regex("(\\p{P})+") /** * 换行 */ val rnRegex = Regex("[\\r\\n]") /** * 不发音段落判断 */ val notReadAloudRegex = Regex("^(\\s|\\p{C}|\\p{P}|\\p{Z}|\\p{S})+$") } ================================================ FILE: src/main/java/io/legado/app/constant/BookType.kt ================================================ package io.legado.app.constant object BookType { const val default = 0 // 0 文本 const val audio = 1 // 1 音频 const val image = 2 // 2 图片 const val file = 3 // 3 只提供下载服务的网站 const val local = "loc_book" } ================================================ FILE: src/main/java/io/legado/app/constant/DeepinkBookSource.kt ================================================ package io.legado.app.constant import java.io.File object DeepinkBookSource { fun generate(name: String, url: String, md5: String) { val text = "{\n" + " \"name\": \"$name by [yuedu.best]\",\n" + " \"url\": \"$url\",\n" + " \"version\": 100,\n" + " \"search\": {\n" + " \"url\": \"http://api.yuedu.best/yuedu/searchBook@post->{\\\"key\\\":\\\"\${key}\\\", \\\"bookSourceCode\\\":\\\"$md5\\\"}\",\n" + " \"charset\": \"utf-8\",\n" + " \"list\": \"\$.[*]\",\n" + " \"name\": \"\$.name\",\n" + " \"author\": \"\$.author\",\n" + " \"cover\": \"\$.coverUrl\",\n" + " \"summary\": \"\$.intro\",\n" + " \"detail\": \"http://api.yuedu.best/yuedu/getBookInfo@post->{\\\"searchBook\\\":\${$}, \\\"bookSourceCode\\\":\\\"$md5\\\"}\"\n" + " },\n" + " \"detail\": {\n" + " \"name\": \"\$.name\",\n" + " \"author\": \"\$.author\",\n" + " \"cover\": \"\$.coverUrl\",\n" + " \"summary\": \"\$.intro\",\n" + " \"status\": \"\",\n" + " \"update\": \"\$.latestChapterTime\",\n" + " \"lastChapter\": \"\$.latestChapterTitle\",\n" + " \"catalog\": \"http://api.yuedu.best/yuedu/getChapterList@post->{\\\"book\\\":\${$}, \\\"bookSourceCode\\\":\\\"$md5\\\"}\"\n" + " },\n" + " \"catalog\": {\n" + " \"list\": \"\$.[*]\",\n" + " \"name\": \"\$.title\",\n" + " \"chapter\": \"http://api.yuedu.best/yuedu/getContent@post->{\\\"bookChapter\\\":\${$}, \\\"bookSourceCode\\\":\\\"$md5\\\"}\"\n" + " },\n" + " \"chapter\": {\n" + " \"content\": \"\$.text\"\n" + " }\n" + "}" val file = File("repo/${url.replace("https://","").replace("http://","")}.json") println("file path: "+ file.absoluteFile) file.createNewFile() file.writeText(text) // println("file path: "+ file.absoluteFile) } } ================================================ FILE: src/main/java/io/legado/app/constant/PreferKey.kt ================================================ package io.legado.app.constant object PreferKey { const val downloadPath = "downloadPath" const val hideStatusBar = "hideStatusBar" const val hideNavigationBar = "hideNavigationBar" const val precisionSearch = "precisionSearch" const val prevKey = "prevKeyCode" const val nextKey = "nextKeyCode" } ================================================ FILE: src/main/java/io/legado/app/constant/RSSKeywords.kt ================================================ package io.legado.app.constant object RSSKeywords { const val RSS_ITEM = "item" const val RSS_ITEM_TITLE = "title" const val RSS_ITEM_LINK = "link" const val RSS_ITEM_CATEGORY = "category" const val RSS_ITEM_THUMBNAIL = "media:thumbnail" const val RSS_ITEM_ENCLOSURE = "enclosure" const val RSS_ITEM_DESCRIPTION = "description" const val RSS_ITEM_CONTENT = "content:encoded" const val RSS_ITEM_PUB_DATE = "pubDate" const val RSS_ITEM_TIME = "time" const val RSS_ITEM_URL = "url" const val RSS_ITEM_TYPE = "type" } ================================================ FILE: src/main/java/io/legado/app/constant/Status.kt ================================================ package io.legado.app.constant object Status { const val STOP = 0 const val PLAY = 1 const val PAUSE = 3 } ================================================ FILE: src/main/java/io/legado/app/data/entities/BaseBook.kt ================================================ package io.legado.app.data.entities import io.legado.app.model.analyzeRule.RuleDataInterface import io.legado.app.utils.splitNotBlank interface BaseBook : RuleDataInterface { var name: String var author: String var bookUrl: String var kind: String? var wordCount: String? var infoHtml: String? var tocHtml: String? fun getKindList(): List { val kindList = arrayListOf() wordCount?.let { if (it.isNotBlank()) kindList.add(it) } kind?.let { val kinds = it.splitNotBlank(",", "\n") kindList.addAll(kinds) } return kindList } } ================================================ FILE: src/main/java/io/legado/app/data/entities/BaseSource.kt ================================================ package io.legado.app.data.entities import com.script.SimpleBindings import io.legado.app.utils.Base64 import io.legado.app.constant.AppConst import io.legado.app.help.CacheManager import io.legado.app.help.JsExtensions import io.legado.app.help.http.CookieStore import io.legado.app.utils.* /** * 可在js里调用,source.xxx() */ @Suppress("unused") interface BaseSource : JsExtensions { var concurrentRate: String? // 并发率 var loginUrl: String? // 登录地址 // var loginUi: String? // 登录UI var header: String? // 请求头 fun getTag(): String fun getKey(): String override fun getSource(): BaseSource? { return this } fun getLoginJs(): String? { val loginJs = loginUrl return when { loginJs == null -> null loginJs.startsWith("@js:") -> loginJs.substring(4) loginJs.startsWith("") -> loginJs.substring(4, loginJs.lastIndexOf("<")) else -> loginJs } } fun login() { getLoginJs()?.let { evalJS(it) } } /** * 解析header规则 */ fun getHeaderMap(hasLoginHeader: Boolean = false) = HashMap().apply { this[AppConst.UA_NAME] = AppConst.userAgent header?.let { GSON.fromJsonObject>( when { it.startsWith("@js:", true) -> evalJS(it.substring(4)).toString() it.startsWith("", true) -> evalJS(it.substring(4, it.lastIndexOf("<"))).toString() else -> it } ).getOrNull()?.let { map -> putAll(map) } } if (hasLoginHeader) { getLoginHeaderMap()?.let { putAll(it) } } } /** * 获取用于登录的头部信息 */ fun getLoginHeader(): String? { return CacheManager.get("loginHeader_${getKey()}") } fun getLoginHeaderMap(): Map? { val cache = getLoginHeader() ?: return null return GSON.fromJsonObject>(cache).getOrNull() } /** * 保存登录头部信息,map格式,访问时自动添加 */ fun putLoginHeader(header: String) { CacheManager.put("loginHeader_${getKey()}", header) } fun removeLoginHeader() { CacheManager.delete("loginHeader_${getKey()}") } /** * 获取用户信息,可以用来登录 * 用户信息采用aes加密存储 */ fun getLoginInfo(): String? { try { val key = AppConst.userAgent.encodeToByteArray(0, 8) val cache = CacheManager.get("userInfo_${getKey()}") ?: return null val encodeBytes = EncoderUtils.base64Decode(cache, Base64.DEFAULT).toByteArray() val decodeBytes = EncoderUtils.decryptAES(encodeBytes, key) ?: return null return String(decodeBytes) } catch (e: Exception) { log("获取登陆信息出错 " + e.localizedMessage) return null } } fun getLoginInfoMap(): Map? { return GSON.fromJsonObject>(getLoginInfo()).getOrNull() } /** * 保存用户信息,aes加密 */ fun putLoginInfo(info: String): Boolean { return try { val key = (AppConst.userAgent).encodeToByteArray(0, 8) val encodeBytes = EncoderUtils.encryptAES(info.toByteArray(), key) val encodeStr = Base64.encodeToString(encodeBytes, Base64.DEFAULT) CacheManager.put("userInfo_${getKey()}", encodeStr) true } catch (e: Exception) { log("保存登陆信息出错 " + e.localizedMessage) false } } fun removeLoginInfo() { CacheManager.delete("userInfo_${getKey()}") } fun setVariable(variable: String?) { if (variable != null) { CacheManager.put("sourceVariable_${getKey()}", variable) } else { CacheManager.delete("sourceVariable_${getKey()}") } } fun getVariable(): String? { return CacheManager.get("sourceVariable_${getKey()}") } /** * 执行JS */ @Throws(Exception::class) fun evalJS(jsStr: String, bindingsConfig: SimpleBindings.() -> Unit = {}): Any? { val bindings = SimpleBindings() bindings.apply(bindingsConfig) bindings["java"] = this bindings["source"] = this bindings["baseUrl"] = getKey() bindings["cookie"] = CookieStore bindings["cache"] = CacheManager return AppConst.SCRIPT_ENGINE.eval(jsStr, bindings) } } ================================================ FILE: src/main/java/io/legado/app/data/entities/Book.kt ================================================ package io.legado.app.data.entities import io.legado.app.constant.BookType import io.legado.app.constant.AppPattern import io.legado.app.utils.GSON import io.legado.app.utils.fromJsonObject import io.legado.app.utils.MD5Utils import io.legado.app.utils.FileUtils import io.legado.app.model.localBook.LocalBook import io.legado.app.model.localBook.EpubFile import io.legado.app.model.localBook.UmdFile import io.legado.app.model.localBook.CbzFile import java.nio.charset.Charset import java.io.File import kotlin.math.max import kotlin.math.min import org.jsoup.Jsoup import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties("variableMap", "infoHtml", "tocHtml", "config", "rootDir", "readConfig", "localBook", "epub", "epubRootDir", "onLineTxt", "localTxt", "umd", "realAuthor", "unreadChapterNum", "folderName", "localFile", "kindList", "_userNameSpace", "bookDir", "userNameSpace") data class Book( override var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径) var tocUrl: String = "", // 目录页Url (toc=table of Contents) var origin: String = BookType.local, // 书源URL(默认BookType.local) var originName: String = "", //书源名称 override var name: String = "", // 书籍名称(书源获取) override var author: String = "", // 作者名称(书源获取) override var kind: String? = null, // 分类信息(书源获取) var customTag: String? = null, // 分类信息(用户修改) var coverUrl: String? = null, // 封面Url(书源获取) var customCoverUrl: String? = null, // 封面Url(用户修改) var intro: String? = null, // 简介内容(书源获取) var customIntro: String? = null, // 简介内容(用户修改) var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍) var type: Int = 0, // @BookType var group: Int = 0, // 自定义分组索引号 var latestChapterTitle: String? = null, // 最新章节标题 var latestChapterTime: Long = System.currentTimeMillis(), // 最新章节标题更新时间 var lastCheckTime: Long = System.currentTimeMillis(), // 最近一次更新书籍信息的时间 var lastCheckCount: Int = 0, // 最近一次发现新章节的数量 var totalChapterNum: Int = 0, // 书籍目录总数 var durChapterTitle: String? = null, // 当前章节名称 var durChapterIndex: Int = 0, // 当前章节索引 var durChapterPos: Int = 0, // 当前阅读的进度(首行字符的索引位置) var durChapterTime: Long = System.currentTimeMillis(), // 最近一次阅读书籍的时间(打开正文的时间) override var wordCount: String? = null, var canUpdate: Boolean = true, // 刷新书架时更新书籍信息 var order: Int = 0, // 手动排序 var originOrder: Int = 0, //书源排序 var useReplaceRule: Boolean = true, // 正文使用净化替换规则 var variable: String? = null, // 自定义书籍变量信息(用于书源规则检索书籍信息) var readConfig: ReadConfig? = null ) : BaseBook { fun isLocalBook(): Boolean { return origin == BookType.local } fun isLocalTxt(): Boolean { return isLocalBook() && originName.endsWith(".txt", true) } fun isLocalEpub(): Boolean { return isLocalBook() && originName.endsWith(".epub", true) } fun isEpub(): Boolean { return originName.endsWith(".epub", true) } fun isCbz(): Boolean { return originName.endsWith(".cbz", true) } fun isUmd(): Boolean { return originName.endsWith(".umd", true) } fun isOnLineTxt(): Boolean { return !isLocalBook() && type == 0 } override fun equals(other: Any?): Boolean { if (other is Book) { return other.bookUrl == bookUrl } return false } override fun hashCode(): Int { return bookUrl.hashCode() } @delegate:Transient override val variableMap: HashMap by lazy { GSON.fromJsonObject>(variable).getOrNull() ?: hashMapOf() } override fun putVariable(key: String, value: String?) { if (value != null) { variableMap[key] = value } else { variableMap.remove(key) } variable = GSON.toJson(variableMap) } override var infoHtml: String? = null override var tocHtml: String? = null fun getRealAuthor() = author.replace(AppPattern.authorRegex, "") fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0) fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro fun fileCharset(): Charset { return charset(charset ?: "UTF-8") } private fun config(): ReadConfig { if (readConfig == null) { readConfig = ReadConfig() } return readConfig!! } fun setDelTag(tag: Long) { config().delTag = if ((config().delTag and tag) == tag) config().delTag and tag.inv() else config().delTag or tag } fun getDelTag(tag: Long): Boolean { return config().delTag and tag == tag } fun getFolderName(): String { //防止书名过长,只取9位 var folderName = name.replace(AppPattern.fileNameRegex, "") folderName = folderName.substring(0, min(9, folderName.length)) return folderName + MD5Utils.md5Encode16(bookUrl) } @Transient private var rootDir: String = "" fun setRootDir(root: String) { if (root.isNotEmpty() && !root.endsWith(File.separator)) { rootDir = root + File.separator } else { rootDir = root } } fun getLocalFile(): File { if (isEpub() && originName.indexOf("localStore") < 0 && originName.indexOf("webdav") < 0) { // 非本地/webdav书仓的 epub文件 return FileUtils.getFile(File(rootDir + originName), "index.epub") } if (isCbz() && originName.indexOf("localStore") < 0 && originName.indexOf("webdav") < 0) { // 非本地/webdav书仓的 cbz文件 return FileUtils.getFile(File(rootDir + originName), "index.cbz") } return File(rootDir + originName) } @Transient private var _userNameSpace: String = "" fun setUserNameSpace(nameSpace: String) { _userNameSpace = nameSpace } fun getUserNameSpace(): String { return _userNameSpace } fun getBookDir(): String { return FileUtils.getPath(File(rootDir), "storage", "data", _userNameSpace, name + "_" + author) } fun getSplitLongChapter(): Boolean { return false } fun toSearchBook(): SearchBook { return SearchBook( name = name, author = author, kind = kind, bookUrl = bookUrl, origin = origin, originName = originName, type = type, wordCount = wordCount, latestChapterTitle = latestChapterTitle, coverUrl = coverUrl, intro = intro, tocUrl = tocUrl, // originOrder = originOrder, variable = variable ).apply { this.infoHtml = this@Book.infoHtml this.tocHtml = this@Book.tocHtml } } fun getEpubRootDir(): String { // 根据 content.opf 位置来确认root目录 // var contentOPF = "OEBPS/content.opf" val defaultPath = "OEBPS" // 根据 META-INF/container.xml 来获取 contentOPF 位置 val containerRes = File(bookUrl + File.separator + "index" + File.separator + "META-INF" + File.separator + "container.xml") if (containerRes.exists()) { try { val document = Jsoup.parse(containerRes.readText()) val rootFileElement = document .getElementsByTag("rootfiles").get(0) .getElementsByTag("rootfile").get(0); val result = rootFileElement.attr("full-path"); System.out.println("result: " + result) if (result != null && result.isNotEmpty()) { return File(result).parentFile?.let{ it.toString() } ?: "" } } catch (e: Exception) { e.printStackTrace(); // Log.e(TAG, e.getMessage(), e); } } // 返回默认位置 return defaultPath } fun updateFromLocal(onlyCover: Boolean = false) { try { if (isEpub()) { EpubFile.upBookInfo(this, onlyCover) } else if (isUmd()) { UmdFile.upBookInfo(this, onlyCover) } else if (isCbz()) { CbzFile.upBookInfo(this, onlyCover) } } catch(e: Exception) { e.printStackTrace() } } fun workRoot(): String { return rootDir } companion object { const val hTag = 2L const val rubyTag = 4L const val imgTag = 8L const val imgStyleDefault = "DEFAULT" const val imgStyleFull = "FULL" const val imgStyleText = "TEXT" fun initLocalBook(bookUrl: String, localPath: String, rootDir: String = ""): Book { val fileName = File(localPath).name val nameAuthor = LocalBook.analyzeNameAuthor(fileName) val book = Book(bookUrl, "", BookType.local, localPath, nameAuthor.first, nameAuthor.second).also { it.canUpdate = false } book.setRootDir(rootDir) book.updateFromLocal() return book } } data class ReadConfig( var reverseToc: Boolean = false, var pageAnim: Int = -1, var reSegment: Boolean = false, var imageStyle: String? = null, var useReplaceRule: Boolean = false, // 正文使用净化替换规则 var delTag: Long = 0L //去除标签 ) class Converters { fun readConfigToString(config: ReadConfig?): String = GSON.toJson(config) fun stringToReadConfig(json: String?) = GSON.fromJsonObject(json) } } ================================================ FILE: src/main/java/io/legado/app/data/entities/BookChapter.kt ================================================ package io.legado.app.data.entities import io.legado.app.utils.GSON import io.legado.app.utils.fromJsonObject import io.legado.app.utils.MD5Utils import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.RuleDataInterface import io.legado.app.utils.NetworkUtils import com.fasterxml.jackson.annotation.JsonIgnoreProperties @JsonIgnoreProperties("variableMap") data class BookChapter( var url: String = "", // 章节地址 var title: String = "", // 章节标题 var isVolume: Boolean = false, // 是否是卷名 var baseUrl: String = "", //用来拼接相对url var bookUrl: String = "", // 书籍地址 var index: Int = 0, // 章节序号 var resourceUrl: String? = null, // 音频真实URL var tag: String? = null, // var start: Long? = null, // 章节起始位置 var end: Long? = null, // 章节终止位置 var startFragmentId: String? = null, //EPUB书籍当前章节的fragmentId var endFragmentId: String? = null, //EPUB书籍下一章节的fragmentId var variable: String? = null //变量 ): RuleDataInterface { @delegate:Transient override val variableMap: HashMap by lazy { GSON.fromJsonObject>(variable).getOrNull() ?: hashMapOf() } override fun putVariable(key: String, value: String?) { if (value != null) { variableMap[key] = value } else { variableMap.remove(key) } variable = GSON.toJson(variableMap) } override fun hashCode() = url.hashCode() override fun equals(other: Any?): Boolean { if (other is BookChapter) { return other.url == url } return false } fun getAbsoluteURL():String{ val urlMatcher = AnalyzeUrl.paramPattern.matcher(url) val urlBefore = if(urlMatcher.find())url.substring(0,urlMatcher.start()) else url val urlAbsoluteBefore = NetworkUtils.getAbsoluteURL(baseUrl,urlBefore) return if(urlBefore.length == url.length) urlAbsoluteBefore else urlAbsoluteBefore + ',' + url.substring(urlMatcher.end()) } fun getFileName(): String = String.format("%05d-%s.nb", index, MD5Utils.md5Encode16(title)) } ================================================ FILE: src/main/java/io/legado/app/data/entities/BookGroup.kt ================================================ package io.legado.app.data.entities //@Parcelize //@Entity(tableName = "book_groups") data class BookGroup( // @PrimaryKey var groupId: Int = 0, var groupName: String = "", var order: Int = 0, var show: Boolean = true ) ================================================ FILE: src/main/java/io/legado/app/data/entities/BookSource.kt ================================================ package io.legado.app.data.entities //import io.legado.app.App import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst.userAgent import io.legado.app.data.entities.rule.* import io.legado.app.help.JsExtensions import io.legado.app.help.http.CookieStore import io.legado.app.help.CacheManager import io.legado.app.utils.GSON import io.legado.app.utils.fromJsonObject //import io.legado.app.utils.getPrefString import io.legado.app.help.SourceAnalyzer import java.io.InputStream import java.util.* import javax.script.SimpleBindings import com.fasterxml.jackson.annotation.JsonIgnoreProperties //@Parcelize //@Entity( // tableName = "book_sources", // indices = [(Index(value = ["bookSourceUrl"], unique = false))] //) @JsonIgnoreProperties("headerMap", "source") data class BookSource( var bookSourceName: String = "", // 名称 var bookSourceGroup: String? = null, // 分组 // @PrimaryKey var bookSourceUrl: String = "", // 地址,包括 http/https var bookSourceType: Int = 0, // 类型,0 文本,1 音频 var bookUrlPattern: String? = null, //详情页url正则 var customOrder: Int = 0, // 手动排序编号 var enabled: Boolean = true, // 是否启用 var enabledExplore: Boolean = true, //启用发现 override var concurrentRate: String? = null, //并发率 override var header: String? = null, override var loginUrl: String? = null, // 登录地址 var loginCheckJs: String? = null, // 登录检测js var lastUpdateTime: Long = 0, // 最后更新时间,用于排序 var weight: Int = 0, // 智能排序的权重 var exploreUrl: String? = null, // 发现url var ruleExplore: ExploreRule? = null, // 发现规则 var searchUrl: String? = null, // 搜索url var ruleSearch: SearchRule? = null, // 搜索规则 var ruleBookInfo: BookInfoRule? = null, // 书籍信息页规则 var ruleToc: TocRule? = null, // 目录页规则 var ruleContent: ContentRule? = null, // 正文页规则 var bookSourceComment: String? = null, // 注释 var respondTime: Long = 180000L, // 响应时间,用于排序 ) : BaseSource { // @Ignore // @IgnoredOnParcel private var searchRuleV: SearchRule? = null // @Ignore // @IgnoredOnParcel private var exploreRuleV: ExploreRule? = null // @Ignore // @IgnoredOnParcel private var bookInfoRuleV: BookInfoRule? = null // @Ignore // @IgnoredOnParcel private var tocRuleV: TocRule? = null // @Ignore // @IgnoredOnParcel private var contentRuleV: ContentRule? = null override fun getTag(): String { return bookSourceName } override fun getKey(): String { return bookSourceUrl } override fun hashCode(): Int { return bookSourceUrl.hashCode() } override fun equals(other: Any?) = if (other is BookSource) other.bookSourceUrl == bookSourceUrl else false fun getSearchRule(): SearchRule { return ruleSearch ?: SearchRule() } fun getExploreRule(): ExploreRule { return ruleExplore ?: ExploreRule() } fun getBookInfoRule(): BookInfoRule { return ruleBookInfo ?: BookInfoRule() } fun getTocRule(): TocRule { return ruleToc ?: TocRule() } fun getContentRule(): ContentRule { return ruleContent ?: ContentRule() } // fun getExploreKinds(): ArrayList? { // val exploreKinds = arrayListOf() // exploreUrl?.let { // var a = it // if (a.isNotBlank()) { // try { // if (it.startsWith("", false)) { // val aCache = ACache.get(App.INSTANCE, "explore") // a = aCache.getAsString(bookSourceUrl) ?: "" // if (a.isBlank()) { // val bindings = SimpleBindings() // bindings["baseUrl"] = bookSourceUrl // bindings["java"] = JsExtensions // a = AppConst.SCRIPT_ENGINE.eval( // it.substring(4, it.lastIndexOf("<")), // bindings // ).toString() // aCache.put(bookSourceUrl, a) // } // } // val b = a.split("(&&|\n)+".toRegex()) // b.map { c -> // val d = c.split("::") // if (d.size > 1) // exploreKinds.add(ExploreKind(d[0], d[1])) // } // } catch (e: Exception) { // exploreKinds.add(ExploreKind(e.localizedMessage)) // } // } // } // return exploreKinds // } fun equal(source: BookSource): Boolean { return equal(bookSourceName, source.bookSourceName) && equal(bookSourceUrl, source.bookSourceUrl) && equal(bookSourceGroup, source.bookSourceGroup) && bookSourceType == source.bookSourceType && equal(bookUrlPattern, source.bookUrlPattern) && enabled == source.enabled && enabledExplore == source.enabledExplore && equal(header, source.header) && equal(loginUrl, source.loginUrl) && equal(exploreUrl, source.exploreUrl) && equal(searchUrl, source.searchUrl) && getSearchRule() == source.getSearchRule() && getExploreRule() == source.getExploreRule() && getBookInfoRule() == source.getBookInfoRule() && getTocRule() == source.getTocRule() && getContentRule() == source.getContentRule() } private fun equal(a: String?, b: String?): Boolean { return a == b || (a.isNullOrEmpty() && b.isNullOrEmpty()) } data class ExploreKind( var title: String, var url: String? = null ) companion object { fun fromJson(json: String): Result { return SourceAnalyzer.jsonToBookSource(json) } fun fromJsonArray(json: String): Result> { return SourceAnalyzer.jsonToBookSources(json) } fun fromJsonArray(inputStream: InputStream): Result> { return SourceAnalyzer.jsonToBookSources(inputStream) } } class Converters { fun exploreRuleToString(exploreRule: ExploreRule?): String = GSON.toJson(exploreRule) fun stringToExploreRule(json: String?) = GSON.fromJsonObject(json).getOrNull() fun searchRuleToString(searchRule: SearchRule?): String = GSON.toJson(searchRule) fun stringToSearchRule(json: String?) = GSON.fromJsonObject(json).getOrNull() fun bookInfoRuleToString(bookInfoRule: BookInfoRule?): String = GSON.toJson(bookInfoRule) fun stringToBookInfoRule(json: String?) = GSON.fromJsonObject(json).getOrNull() fun tocRuleToString(tocRule: TocRule?): String = GSON.toJson(tocRule) fun stringToTocRule(json: String?) = GSON.fromJsonObject(json).getOrNull() fun contentRuleToString(contentRule: ContentRule?): String = GSON.toJson(contentRule) fun stringToContentRule(json: String?) = GSON.fromJsonObject(json).getOrNull() } } ================================================ FILE: src/main/java/io/legado/app/data/entities/Bookmark.kt ================================================ package io.legado.app.data.entities //@Parcelize //@Entity(tableName = "bookmarks", indices = [(Index(value = ["bookUrl"], unique = true))]) data class Bookmark( // @PrimaryKey val time: Long = System.currentTimeMillis(), val bookName: String = "", val bookAuthor: String = "", var chapterIndex: Int = 0, var chapterPos: Int = 0, var chapterName: String = "", var bookText: String = "", var content: String = "" ) ================================================ FILE: src/main/java/io/legado/app/data/entities/Cache.kt ================================================ package io.legado.app.data.entities // @Entity(tableName = "caches", indices = [(Index(value = ["key"], unique = true))]) data class Cache( // @PrimaryKey val key: String = "", var value: String? = null, var deadline: Long = 0L ) ================================================ FILE: src/main/java/io/legado/app/data/entities/Cookie.kt ================================================ package io.legado.app.data.entities // @Entity(tableName = "cookies", indices = [(Index(value = ["url"], unique = true))]) data class Cookie( // @PrimaryKey var url: String = "", var cookie: String = "" ) ================================================ FILE: src/main/java/io/legado/app/data/entities/ReplaceRule.kt ================================================ package io.legado.app.data.entities import com.fasterxml.jackson.annotation.JsonProperty; //@Parcelize //@Entity( // tableName = "replace_rules", // indices = [(Index(value = ["id"]))] //) data class ReplaceRule( // @PrimaryKey(autoGenerate = true) var id: Long = System.currentTimeMillis(), var name: String = "", var group: String? = null, var pattern: String = "", var replacement: String = "", var scope: String? = null, @get:JsonProperty("isEnabled") var isEnabled: Boolean = true, @get:JsonProperty("isRegex") var isRegex: Boolean = false, // @ColumnInfo(name = "sortOrder") var order: Int = 0 ) ================================================ FILE: src/main/java/io/legado/app/data/entities/RssArticle.kt ================================================ package io.legado.app.data.entities import io.legado.app.utils.GSON import io.legado.app.utils.fromJsonObject import io.legado.app.model.analyzeRule.RuleDataInterface data class RssArticle( var origin: String = "", var sort: String = "", var title: String = "", var order: Long = 0, var link: String = "", var pubDate: String? = null, var description: String? = null, var content: String? = null, var image: String? = null, var read: Boolean = false, var variable: String? = null ): RuleDataInterface { override fun hashCode() = link.hashCode() override fun equals(other: Any?): Boolean { other ?: return false return if (other is RssArticle) origin == other.origin && link == other.link else false } @delegate:Transient override val variableMap: HashMap by lazy { GSON.fromJsonObject>(variable).getOrNull() ?: hashMapOf() } override fun putVariable(key: String, value: String?) { if (value != null) { variableMap[key] = value } else { variableMap.remove(key) } variable = GSON.toJson(variableMap) } // fun toStar() = RssStar( // origin = origin, // sort = sort, // title = title, // starTime = System.currentTimeMillis(), // link = link, // pubDate = pubDate, // description = description, // content = content, // image = image // ) } ================================================ FILE: src/main/java/io/legado/app/data/entities/RssSource.kt ================================================ package io.legado.app.data.entities import com.jayway.jsonpath.DocumentContext import com.fasterxml.jackson.annotation.JsonIgnoreProperties import io.legado.app.help.CacheManager import io.legado.app.help.JsExtensions import io.legado.app.help.http.CookieStore import io.legado.app.constant.AppConst import javax.script.SimpleBindings import io.legado.app.utils.* @JsonIgnoreProperties("headerMap", "source") data class RssSource( var sourceUrl: String = "", var sourceName: String = "", var sourceIcon: String = "", var sourceGroup: String? = null, var sourceComment: String? = null, var enabled: Boolean = true, override var concurrentRate: String? = null, //并发率 override var header: String? = null, // 请求头 override var loginUrl: String? = null, // 登录地址 // var loginUi: List? = null, //登录UI var loginCheckJs: String? = null, //登录检测js var sortUrl: String? = null, var singleUrl: Boolean = false, //列表规则 var articleStyle: Int = 0, //列表样式,0,1,2 var ruleArticles: String? = null, var ruleNextPage: String? = null, var ruleTitle: String? = null, var rulePubDate: String? = null, //webView规则 var ruleDescription: String? = null, var ruleImage: String? = null, var ruleLink: String? = null, var ruleContent: String? = null, var style: String? = null, var enableJs: Boolean = true, var loadWithBaseUrl: Boolean = true, var customOrder: Int = 0 ): BaseSource { override fun getTag(): String { return sourceName } override fun getKey(): String { return sourceUrl } override fun equals(other: Any?): Boolean { if (other is RssSource) { return other.sourceUrl == sourceUrl } return false } override fun hashCode() = sourceUrl.hashCode() fun equal(source: RssSource): Boolean { return equal(sourceUrl, source.sourceUrl) && equal(sourceIcon, source.sourceIcon) && enabled == source.enabled && equal(sourceGroup, source.sourceGroup) && equal(ruleArticles, source.ruleArticles) && equal(ruleNextPage, source.ruleNextPage) && equal(ruleTitle, source.ruleTitle) && equal(rulePubDate, source.rulePubDate) && equal(ruleDescription, source.ruleDescription) && equal(ruleLink, source.ruleLink) && equal(ruleContent, source.ruleContent) && enableJs == source.enableJs && loadWithBaseUrl == source.loadWithBaseUrl } private fun equal(a: String?, b: String?): Boolean { return a == b || (a.isNullOrEmpty() && b.isNullOrEmpty()) } fun sortUrls(): List> = arrayListOf>().apply { kotlin.runCatching { var a = sortUrl if (sortUrl?.startsWith("", false) == true || sortUrl?.startsWith("@js:", false) == true ) { val jsStr = if (sortUrl!!.startsWith("@")) { sortUrl!!.substring(4) } else { sortUrl!!.substring(4, sortUrl!!.lastIndexOf("<")) } a = evalJS(jsStr).toString() } a?.split("(&&|\n)+".toRegex())?.forEach { c -> val d = c.split("::") if (d.size > 1) add(Pair(d[0], d[1])) } if (isEmpty()) { add(Pair("", sourceUrl)) } } } @Suppress("MemberVisibilityCanBePrivate") companion object { fun fromJsonDoc(doc: DocumentContext): Result { return kotlin.runCatching { // val loginUi = doc.read("$.loginUi") RssSource( sourceUrl = doc.readString("$.sourceUrl")!!, sourceName = doc.readString("$.sourceName")!!, sourceIcon = doc.readString("$.sourceIcon") ?: "", sourceGroup = doc.readString("$.sourceGroup"), sourceComment = doc.readString("$.sourceComment"), enabled = doc.readBool("$.enabled") ?: true, concurrentRate = doc.readString("$.concurrentRate"), header = doc.readString("$.header"), loginUrl = doc.readString("$.loginUrl"), // loginUi = if (loginUi is List<*>) GSON.toJson(loginUi) else loginUi?.toString(), loginCheckJs = doc.readString("$.loginCheckJs"), sortUrl = doc.readString("$.sortUrl"), singleUrl = doc.readBool("$.singleUrl") ?: false, articleStyle = doc.readInt("$.articleStyle") ?: 0, ruleArticles = doc.readString("$.ruleArticles"), ruleNextPage = doc.readString("$.ruleNextPage"), ruleTitle = doc.readString("$.ruleTitle"), rulePubDate = doc.readString("$.rulePubDate"), ruleDescription = doc.readString("$.ruleDescription"), ruleImage = doc.readString("$.ruleImage"), ruleLink = doc.readString("$.ruleLink"), ruleContent = doc.readString("$.ruleContent"), style = doc.readString("$.style"), enableJs = doc.readBool("$.enableJs") ?: true, loadWithBaseUrl = doc.readBool("$.loadWithBaseUrl") ?: true, customOrder = doc.readInt("$.customOrder") ?: 0 ) } } fun fromJson(json: String): Result { return fromJsonDoc(jsonPath.parse(json)) } fun fromJsonArray(jsonArray: String): Result> { return kotlin.runCatching { val sources = arrayListOf() val doc = jsonPath.parse(jsonArray).read>("$") doc.forEach { val jsonItem = jsonPath.parse(it) fromJsonDoc(jsonItem).getOrThrow().let { source -> sources.add(source) } } sources } } } } ================================================ FILE: src/main/java/io/legado/app/data/entities/SearchBook.kt ================================================ package io.legado.app.data.entities //import android.os.Parcelable //import androidx.room.* import io.legado.app.utils.GSON import io.legado.app.utils.fromJsonObject import com.fasterxml.jackson.annotation.JsonIgnoreProperties; //@Parcelize //@Entity( // tableName = "searchBooks", // indices = [(Index(value = ["bookUrl"], unique = true))], // foreignKeys = [(ForeignKey( // entity = BookSource::class, // parentColumns = ["bookSourceUrl"], // childColumns = ["origin"], // onDelete = ForeignKey.CASCADE // ))] //) @JsonIgnoreProperties("variableMap", "infoHtml", "tocHtml", "origins", "kindList") data class SearchBook( // @PrimaryKey override var bookUrl: String = "", var origin: String = "", // 书源规则 var originName: String = "", var type: Int = 0, // @BookType override var name: String = "", override var author: String = "", override var kind: String? = null, var coverUrl: String? = null, var intro: String? = null, override var wordCount: String? = null, var latestChapterTitle: String? = null, var tocUrl: String = "", // 目录页Url (toc=table of Contents) var time: Long = 0, var variable: String? = null, var originOrder: Int = 0 ) : BaseBook, Comparable { // @Ignore // @IgnoredOnParcel override var infoHtml: String? = null // @Ignore // @IgnoredOnParcel override var tocHtml: String? = null override fun equals(other: Any?): Boolean { if (other is SearchBook) { if (other.bookUrl == bookUrl) { return true } } return false } override fun hashCode(): Int { return bookUrl.hashCode() } override fun compareTo(other: SearchBook): Int { return other.originOrder - this.originOrder } @delegate:Transient override val variableMap: HashMap by lazy { GSON.fromJsonObject>(variable).getOrNull() ?: hashMapOf() } override fun putVariable(key: String, value: String?) { if (value != null) { variableMap[key] = value } else { variableMap.remove(key) } variable = GSON.toJson(variableMap) } // @Ignore // @IgnoredOnParcel var origins: LinkedHashSet? = null private set fun addOrigin(origin: String) { if (origins == null) { origins = linkedSetOf(this.origin) } origins?.add(origin) } fun toBook(): Book { return Book( name = name, author = author, kind = kind, bookUrl = bookUrl, origin = origin, originName = originName, type = type, wordCount = wordCount, latestChapterTitle = latestChapterTitle, coverUrl = coverUrl, intro = intro, tocUrl = tocUrl, // originOrder = originOrder, variable = variable ).apply { this.infoHtml = this@SearchBook.infoHtml this.tocUrl = this@SearchBook.tocUrl } } } ================================================ FILE: src/main/java/io/legado/app/data/entities/SearchKeyword.kt ================================================ package io.legado.app.data.entities //@Parcelize //@Entity(tableName = "search_keywords", indices = [(Index(value = ["word"], unique = true))]) data class SearchKeyword( // @PrimaryKey var word: String = "", // 搜索关键词 var usage: Int = 1, // 使用次数 var lastUseTime: Long = System.currentTimeMillis() // 最后一次使用时间 ) ================================================ FILE: src/main/java/io/legado/app/data/entities/SearchResult.kt ================================================ package io.legado.app.data.entities data class SearchResult( val resultCount: Int = 0, val resultCountWithinChapter: Int = 0, val resultText: String = "", val chapterTitle: String = "", val query: String = "", val pageSize: Int = 0, val chapterIndex: Int = 0, val pageIndex: Int = 0, val queryIndexInResult: Int = 0, val queryIndexInChapter: Int = 0 ) { } ================================================ FILE: src/main/java/io/legado/app/data/entities/TxtTocRule.kt ================================================ package io.legado.app.data.entities // import androidx.room.Entity // import androidx.room.PrimaryKey // @Entity(tableName = "txtTocRules") data class TxtTocRule( // @PrimaryKey var id: Long = System.currentTimeMillis(), var name: String = "", var rule: String = "", var serialNumber: Int = -1, var enable: Boolean = true ) ================================================ FILE: src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt ================================================ package io.legado.app.data.entities.rule data class BookInfoRule( var init: String? = null, var name: String? = null, var author: String? = null, var intro: String? = null, var kind: String? = null, var lastChapter: String? = null, var updateTime: String? = null, var coverUrl: String? = null, var tocUrl: String? = null, var wordCount: String? = null, var canReName: String? = null ) ================================================ FILE: src/main/java/io/legado/app/data/entities/rule/BookListRule.kt ================================================ package io.legado.app.data.entities.rule interface BookListRule { var bookList: String? var name: String? var author: String? var intro: String? var kind: String? var lastChapter: String? var updateTime: String? var bookUrl: String? var coverUrl: String? var wordCount: String? } ================================================ FILE: src/main/java/io/legado/app/data/entities/rule/ContentRule.kt ================================================ package io.legado.app.data.entities.rule data class ContentRule( var content: String? = null, var nextContentUrl: String? = null, var webJs: String? = null, var sourceRegex: String? = null, var replaceRegex: String? = null, //替换规则 var imageStyle: String? = null, //默认大小居中,FULL最大宽度 ) ================================================ FILE: src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt ================================================ package io.legado.app.data.entities.rule data class ExploreRule( override var bookList: String? = null, override var name: String? = null, override var author: String? = null, override var intro: String? = null, override var kind: String? = null, override var lastChapter: String? = null, override var updateTime: String? = null, override var bookUrl: String? = null, override var coverUrl: String? = null, override var wordCount: String? = null ) : BookListRule ================================================ FILE: src/main/java/io/legado/app/data/entities/rule/SearchRule.kt ================================================ package io.legado.app.data.entities.rule data class SearchRule( override var bookList: String? = null, override var name: String? = null, override var author: String? = null, override var intro: String? = null, override var kind: String? = null, override var lastChapter: String? = null, override var updateTime: String? = null, override var bookUrl: String? = null, override var coverUrl: String? = null, override var wordCount: String? = null ) : BookListRule ================================================ FILE: src/main/java/io/legado/app/data/entities/rule/TocRule.kt ================================================ package io.legado.app.data.entities.rule data class TocRule( var preUpdateJs: String? = null, var chapterList: String? = null, var chapterName: String? = null, var chapterUrl: String? = null, var isVolume: String? = null, var isVip: String? = null, var updateTime: String? = null, var nextTocUrl: String? = null ) ================================================ FILE: src/main/java/io/legado/app/exception/ConcurrentException.kt ================================================ @file:Suppress("unused") package io.legado.app.exception /** * 并发限制 */ class ConcurrentException(msg: String, val waitTime: Int) : NoStackTraceException(msg) ================================================ FILE: src/main/java/io/legado/app/exception/ContentEmptyException.kt ================================================ package io.legado.app.exception /** * 内容为空 */ class ContentEmptyException(msg: String) : NoStackTraceException(msg) ================================================ FILE: src/main/java/io/legado/app/exception/NoStackTraceException.kt ================================================ package io.legado.app.exception /** * 不记录错误堆栈的报错 */ open class NoStackTraceException(msg: String) : Exception(msg) { override fun fillInStackTrace(): Throwable { return this } } ================================================ FILE: src/main/java/io/legado/app/exception/RegexTimeoutException.kt ================================================ package io.legado.app.exception class RegexTimeoutException(msg: String) : NoStackTraceException(msg) ================================================ FILE: src/main/java/io/legado/app/exception/TocEmptyException.kt ================================================ package io.legado.app.exception /** * 目录为空 */ class TocEmptyException(msg: String) : NoStackTraceException(msg) ================================================ FILE: src/main/java/io/legado/app/help/BookHelp.kt ================================================ package io.legado.app.help import io.legado.app.constant.AppPattern import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookSource import io.legado.app.utils.FileUtils import io.legado.app.utils.NetworkUtils import io.legado.app.utils.MD5Utils import io.legado.app.utils.getFile import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.localBook.LocalBook import java.io.File import java.util.concurrent.CopyOnWriteArraySet import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.CoroutineScope //import org.apache.commons.text.similarity.JaccardSimilarity object BookHelp { private const val cacheImageFolderName = "images" private val downloadImages = CopyOnWriteArraySet() private fun formatFolderName(folderName: String): String { return folderName.replace("[\\\\/:*?\"<>|.]".toRegex(), "") } fun formatAuthor(author: String?): String { return author ?.replace("作\\s*者[\\s::]*".toRegex(), "") ?.replace("\\s+".toRegex(), " ") ?.trim { it <= ' ' } ?: "" } /** * 格式化书名 */ fun formatBookName(name: String): String { return name .replace(AppPattern.nameRegex, "") .trim { it <= ' ' } } /** * 格式化作者 */ fun formatBookAuthor(author: String): String { return author .replace(AppPattern.authorRegex, "") .trim { it <= ' ' } } fun getBookCacheDir(book: Book): File { val md5Encode = MD5Utils.md5Encode(book.bookUrl).toString() val bookDir = book.getBookDir() if (bookDir.isEmpty()) { throw Exception("bookDir不能为空") } val localCacheDir = File(bookDir).getFile(md5Encode) if (!localCacheDir.exists()) { localCacheDir.mkdirs() } return localCacheDir } /** * 读取章节内容 */ fun getContent(book: Book, bookChapter: BookChapter): String? { val file = getBookCacheDir(book).getFile( String.format("%d.txt", bookChapter.index) ) if (file.exists()) { return file.readText() } if (book.isLocalBook()) { val content = LocalBook.getContent(book, bookChapter) if (content != null && book.isEpub()) { saveText(book, bookChapter, content) } return content } return null } /** * 删除章节内容 */ fun delContent(book: Book, bookChapter: BookChapter) { FileUtils.createFileIfNotExist( getBookCacheDir(book), String.format("%d.txt", bookChapter.index) ).delete() } suspend fun saveContent( scope: CoroutineScope, bookSource: BookSource, book: Book, bookChapter: BookChapter, content: String ) { saveText(book, bookChapter, content) saveImages(scope, bookSource, book, bookChapter, content) } fun saveText( book: Book, bookChapter: BookChapter, content: String ) { // if (content.isEmpty()) return //保存文本 FileUtils.createFileIfNotExist( getBookCacheDir(book), String.format("%d.txt", bookChapter.index) ).writeText(content) } suspend fun saveImages( scope: CoroutineScope, bookSource: BookSource, book: Book, bookChapter: BookChapter, content: String ) { val awaitList = arrayListOf>() content.split("\n").forEach { val matcher = AppPattern.imgPattern.matcher(it) if (matcher.find()) { matcher.group(1)?.let { src -> val mSrc = NetworkUtils.getAbsoluteURL(bookChapter.url, src) val req: Deferred = scope.async { saveImage(bookSource, book, mSrc) return@async 1 } awaitList.add(req) } } } awaitList.forEach { it.await() } } suspend fun saveImage(bookSource: BookSource?, book: Book, src: String) { while (downloadImages.contains(src)) { delay(100) } if (getImage(book, src).exists()) { return } downloadImages.add(src) val analyzeUrl = AnalyzeUrl(src, source = bookSource) try { analyzeUrl.getByteArrayAwait().let { FileUtils.createFileIfNotExist( getBookCacheDir(book), cacheImageFolderName, "${MD5Utils.md5Encode16(src)}.${getImageSuffix(src)}" ).writeBytes(it) } } catch (e: Exception) { e.printStackTrace() } finally { downloadImages.remove(src) } } fun getImage(book: Book, src: String): File { return getBookCacheDir(book).getFile( cacheImageFolderName, "${MD5Utils.md5Encode16(src)}.${getImageSuffix(src)}" ) } fun getImageSuffix(src: String): String { var suffix = src.substringAfterLast(".").substringBefore(",") //检查截取的后缀字符是否合法 [a-zA-Z0-9] val fileSuffixRegex = Regex("^[a-z0-9]+$", RegexOption.IGNORE_CASE) if (suffix.length > 5 || !suffix.matches(fileSuffixRegex)) { suffix = "jpg" } return suffix } } ================================================ FILE: src/main/java/io/legado/app/help/CacheManager.kt ================================================ package io.legado.app.help import io.legado.app.data.entities.Cache import io.legado.app.model.analyzeRule.QueryTTF import io.legado.app.utils.ACache // TODO 处理缓存 @Suppress("unused") object CacheManager { private val queryTTFMap = hashMapOf>() /** * saveTime 单位为秒 */ @JvmOverloads fun put(key: String, value: Any, saveTime: Int = 0) { val deadline = if (saveTime == 0) 0 else System.currentTimeMillis() + saveTime * 1000 when (value) { is QueryTTF -> queryTTFMap[key] = Pair(deadline, value) is ByteArray -> ACache.get().put(key, value, saveTime) else -> { val cache = Cache(key, value.toString(), deadline) // appDb.cacheDao.insert(cache) } } } fun get(key: String): String? { // return appDb.cacheDao.get(key, System.currentTimeMillis()) return null } fun getInt(key: String): Int? { return get(key)?.toIntOrNull() } fun getLong(key: String): Long? { return get(key)?.toLongOrNull() } fun getDouble(key: String): Double? { return get(key)?.toDoubleOrNull() } fun getFloat(key: String): Float? { return get(key)?.toFloatOrNull() } fun getByteArray(key: String): ByteArray? { return ACache.get().getAsBinary(key) } fun getQueryTTF(key: String): QueryTTF? { val cache = queryTTFMap[key] ?: return null if (cache.first == 0L || cache.first > System.currentTimeMillis()) { return cache.second } return null } fun putFile(key: String, value: String, saveTime: Int = 0) { ACache.get().put(key, value, saveTime) } fun getFile(key: String): String? { return ACache.get().getAsString(key) } fun delete(key: String) { ACache.get().remove(key) } } ================================================ FILE: src/main/java/io/legado/app/help/DefaultData.kt ================================================ package io.legado.app.help // import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.TxtTocRule import io.legado.app.utils.GSON import io.legado.app.utils.fromJsonArray import java.io.File object DefaultData { const val txtTocRuleFileName = "txtTocRule.json" val txtTocRules: List by lazy { val json = String(DefaultData::class.java.getResource("/defaultData/${txtTocRuleFileName}").readBytes()) GSON.fromJsonArray(json).getOrNull() ?: emptyList() } // val rssSources by lazy { // val json = String( // File("defaultData${File.separator}rssSources.json") // .readBytes() // ) // GSON.fromJsonArray(json)!! // } } ================================================ FILE: src/main/java/io/legado/app/help/EncodingDetectHelp.java ================================================ package io.legado.app.help; //import androidx.annotation.NonNull; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.net.URL; import java.nio.charset.StandardCharsets; import static io.legado.app.utils.TextUtils.isEmpty; /** * Copyright (C) <2009> *

* This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later * version. *

* This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. *

* EncodingDetect.java
* 自动获取文件的编码 * * @author Billows.Van * @version 1.0 * @since Create on 2010-01-27 11:19:00 */ public class EncodingDetectHelp { public static String getHtmlEncode( byte[] bytes) { try { Document doc = Jsoup.parse(new String(bytes, StandardCharsets.UTF_8)); Elements metaTags = doc.getElementsByTag("meta"); String charsetStr; for (Element metaTag : metaTags) { charsetStr = metaTag.attr("charset"); if (!isEmpty(charsetStr)) { return charsetStr; } String content = metaTag.attr("content"); String http_equiv = metaTag.attr("http-equiv"); if (http_equiv.toLowerCase().equals("content-type")) { if (content.toLowerCase().contains("charset")) { charsetStr = content.substring(content.toLowerCase().indexOf("charset") + "charset=".length()); } else { charsetStr = content.substring(content.toLowerCase().indexOf(";") + 1); } if (!isEmpty(charsetStr)) { return charsetStr; } } } } catch (Exception ignored) { } return getJavaEncode(bytes); } public static String getJavaEncode(byte[] bytes) { int len = bytes.length > 2000 ? 2000 : bytes.length; byte[] cBytes = new byte[len]; System.arraycopy(bytes, 0, cBytes, 0, len); BytesEncodingDetect bytesEncodingDetect = new BytesEncodingDetect(); String code = BytesEncodingDetect.javaname[bytesEncodingDetect.detectEncoding(cBytes)]; // UTF-16LE 特殊处理 if ("Unicode".equals(code)) { if (cBytes[0] == -1) { code = "UTF-16LE"; } } return code; } /** * 得到文件的编码 */ public static String getJavaEncode( String filePath) { BytesEncodingDetect s = new BytesEncodingDetect(); String fileCode = BytesEncodingDetect.javaname[s .detectEncoding(new File(filePath))]; // UTF-16LE 特殊处理 if ("Unicode".equals(fileCode)) { byte[] tempByte = BytesEncodingDetect.getFileBytes(new File( filePath)); if (tempByte[0] == -1) { fileCode = "UTF-16LE"; } } return fileCode; } /** * 得到文件的编码 */ public static String getJavaEncode( File file) { BytesEncodingDetect s = new BytesEncodingDetect(); String fileCode = BytesEncodingDetect.javaname[s.detectEncoding(file)]; // UTF-16LE 特殊处理 if ("Unicode".equals(fileCode)) { byte[] tempByte = BytesEncodingDetect.getFileBytes(file); if (tempByte[0] == -1) { fileCode = "UTF-16LE"; } } return fileCode; } } class BytesEncodingDetect extends Encoding { // Frequency tables to hold the GB, Big5, and EUC-TW character // frequencies int GBFreq[][]; int GBKFreq[][]; int Big5Freq[][]; int Big5PFreq[][]; int EUC_TWFreq[][]; int KRFreq[][]; int JPFreq[][]; // int UnicodeFreq[94][128]; // public static String[] nicename; // public static String[] codings; public boolean debug; public BytesEncodingDetect() { super(); debug = false; GBFreq = new int[94][94]; GBKFreq = new int[126][191]; Big5Freq = new int[94][158]; Big5PFreq = new int[126][191]; EUC_TWFreq = new int[94][94]; KRFreq = new int[94][94]; JPFreq = new int[94][94]; // Initialize the Frequency Table for GB, GBK, Big5, EUC-TW, KR, JP initialize_frequencies(); } /** * Function : detectEncoding Aruguments: URL Returns : One of the encodings * from the Encoding enumeration (GB2312, HZ, BIG5, EUC_TW, ASCII, or OTHER) * Description: This function looks at the URL contents and assigns it a * probability score for each encoding type. The encoding type with the * highest probability is returned. */ public int detectEncoding(URL testurl) { byte[] rawtext = new byte[10000]; int bytesread = 0, byteoffset = 0; int guess = OTHER; InputStream chinesestream; try { chinesestream = testurl.openStream(); while ((bytesread = chinesestream.read(rawtext, byteoffset, rawtext.length - byteoffset)) > 0) { byteoffset += bytesread; } ; chinesestream.close(); guess = detectEncoding(rawtext); } catch (Exception e) { System.err.println("Error loading or using URL " + e.toString()); guess = -1; } return guess; } /** * Function : detectEncoding Aruguments: File Returns : One of the encodings * from the Encoding enumeration (GB2312, HZ, BIG5, EUC_TW, ASCII, or OTHER) * Description: This function looks at the file and assigns it a probability * score for each encoding type. The encoding type with the highest * probability is returned. */ public int detectEncoding(File testfile) { byte[] rawtext = getFileBytes(testfile); return detectEncoding(rawtext); } public static byte[] getFileBytes(File testfile) { FileInputStream chinesefile; byte[] rawtext; rawtext = new byte[2000]; try { chinesefile = new FileInputStream(testfile); chinesefile.read(rawtext); chinesefile.close(); } catch (Exception e) { System.err.println("Error: " + e); } return rawtext; } /** * Function : detectEncoding Aruguments: byte array Returns : One of the * encodings from the Encoding enumeration (GB2312, HZ, BIG5, EUC_TW, ASCII, * or OTHER) Description: This function looks at the byte array and assigns * it a probability score for each encoding type. The encoding type with the * highest probability is returned. */ public int detectEncoding(byte[] rawtext) { int[] scores; int index, maxscore = 0; int encoding_guess = OTHER; scores = new int[TOTALTYPES]; // Assign Scores scores[GB2312] = gb2312_probability(rawtext); scores[GBK] = gbk_probability(rawtext); scores[GB18030] = gb18030_probability(rawtext); scores[HZ] = hz_probability(rawtext); scores[BIG5] = big5_probability(rawtext); scores[CNS11643] = euc_tw_probability(rawtext); scores[ISO2022CN] = iso_2022_cn_probability(rawtext); scores[UTF8] = utf8_probability(rawtext); scores[UNICODE] = utf16_probability(rawtext); scores[EUC_KR] = euc_kr_probability(rawtext); scores[CP949] = cp949_probability(rawtext); scores[JOHAB] = 0; scores[ISO2022KR] = iso_2022_kr_probability(rawtext); scores[ASCII] = ascii_probability(rawtext); scores[SJIS] = sjis_probability(rawtext); scores[EUC_JP] = euc_jp_probability(rawtext); scores[ISO2022JP] = iso_2022_jp_probability(rawtext); scores[UNICODET] = 0; scores[UNICODES] = 0; scores[ISO2022CN_GB] = 0; scores[ISO2022CN_CNS] = 0; scores[OTHER] = 0; // Tabulate Scores for (index = 0; index < TOTALTYPES; index++) { if (debug) System.err.println("Encoding " + nicename[index] + " score " + scores[index]); if (scores[index] > maxscore) { encoding_guess = index; maxscore = scores[index]; } } // Return OTHER if nothing scored above 50 if (maxscore <= 50) { encoding_guess = OTHER; } return encoding_guess; } /* * Function: gb2312_probability Argument: pointer to byte array Returns : * number from 0 to 100 representing probability text in array uses GB-2312 * encoding */ int gb2312_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, gbchars = 1; long gbfreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int row, column; // Stage 1: Check to see if characters fit into acceptable ranges rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { // System.err.println(rawtext[i]); if (rawtext[i] >= 0) { // asciichars++; } else { dbchars++; if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xF7 && (byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) { gbchars++; totalfreq += 500; row = rawtext[i] + 256 - 0xA1; column = rawtext[i + 1] + 256 - 0xA1; if (GBFreq[row][column] != 0) { gbfreq += GBFreq[row][column]; } else if (15 <= row && row < 55) { // In GB high-freq character range gbfreq += 200; } } i++; } } rangeval = 50 * ((float) gbchars / (float) dbchars); freqval = 50 * ((float) gbfreq / (float) totalfreq); return (int) (rangeval + freqval); } /* * Function: gbk_probability Argument: pointer to byte array Returns : * number from 0 to 100 representing probability text in array uses GBK * encoding */ int gbk_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, gbchars = 1; long gbfreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int row, column; // Stage 1: Check to see if characters fit into acceptable ranges rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { // System.err.println(rawtext[i]); if (rawtext[i] >= 0) { // asciichars++; } else { dbchars++; if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xF7 && // Original GB range (byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) { gbchars++; totalfreq += 500; row = rawtext[i] + 256 - 0xA1; column = rawtext[i + 1] + 256 - 0xA1; // System.out.println("original row " + row + " column " + // column); if (GBFreq[row][column] != 0) { gbfreq += GBFreq[row][column]; } else if (15 <= row && row < 55) { gbfreq += 200; } } else if ((byte) 0x81 <= rawtext[i] && rawtext[i] <= (byte) 0xFE && // Extended GB range (((byte) 0x80 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) || ((byte) 0x40 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E))) { gbchars++; totalfreq += 500; row = rawtext[i] + 256 - 0x81; if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) { column = rawtext[i + 1] - 0x40; } else { column = rawtext[i + 1] + 256 - 0x40; } // System.out.println("extended row " + row + " column " + // column + " rawtext[i] " + rawtext[i]); if (GBKFreq[row][column] != 0) { gbfreq += GBKFreq[row][column]; } } i++; } } rangeval = 50 * ((float) gbchars / (float) dbchars); freqval = 50 * ((float) gbfreq / (float) totalfreq); // For regular GB files, this would give the same score, so I handicap // it slightly return (int) (rangeval + freqval) - 1; } /* * Function: gb18030_probability Argument: pointer to byte array Returns : * number from 0 to 100 representing probability text in array uses GBK * encoding */ int gb18030_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, gbchars = 1; long gbfreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int row, column; // Stage 1: Check to see if characters fit into acceptable ranges rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { // System.err.println(rawtext[i]); if (rawtext[i] >= 0) { // asciichars++; } else { dbchars++; if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xF7 && // Original GB range i + 1 < rawtextlen && (byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) { gbchars++; totalfreq += 500; row = rawtext[i] + 256 - 0xA1; column = rawtext[i + 1] + 256 - 0xA1; // System.out.println("original row " + row + " column " + // column); if (GBFreq[row][column] != 0) { gbfreq += GBFreq[row][column]; } else if (15 <= row && row < 55) { gbfreq += 200; } } else if ((byte) 0x81 <= rawtext[i] && rawtext[i] <= (byte) 0xFE && // Extended GB range i + 1 < rawtextlen && (((byte) 0x80 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) || ((byte) 0x40 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E))) { gbchars++; totalfreq += 500; row = rawtext[i] + 256 - 0x81; if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) { column = rawtext[i + 1] - 0x40; } else { column = rawtext[i + 1] + 256 - 0x40; } // System.out.println("extended row " + row + " column " + // column + " rawtext[i] " + rawtext[i]); if (GBKFreq[row][column] != 0) { gbfreq += GBKFreq[row][column]; } } else if ((byte) 0x81 <= rawtext[i] && rawtext[i] <= (byte) 0xFE && // Extended GB range i + 3 < rawtextlen && (byte) 0x30 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x39 && (byte) 0x81 <= rawtext[i + 2] && rawtext[i + 2] <= (byte) 0xFE && (byte) 0x30 <= rawtext[i + 3] && rawtext[i + 3] <= (byte) 0x39) { gbchars++; /* * totalfreq += 500; row = rawtext[i] + 256 - 0x81; if (0x40 * <= rawtext[i+1] && rawtext[i+1] <= 0x7E) { column = * rawtext[i+1] - 0x40; } else { column = rawtext[i+1] + 256 * - 0x40; } //System.out.println("extended row " + row + " * column " + column + " rawtext[i] " + rawtext[i]); if * (GBKFreq[row][column] != 0) { gbfreq += * GBKFreq[row][column]; } */ } i++; } } rangeval = 50 * ((float) gbchars / (float) dbchars); freqval = 50 * ((float) gbfreq / (float) totalfreq); // For regular GB files, this would give the same score, so I handicap // it slightly return (int) (rangeval + freqval) - 1; } /* * Function: hz_probability Argument: byte array Returns : number from 0 to * 100 representing probability text in array uses HZ encoding */ int hz_probability(byte[] rawtext) { int i, rawtextlen; int hzchars = 0, dbchars = 1; long hzfreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int hzstart = 0, hzend = 0; int row, column; rawtextlen = rawtext.length; for (i = 0; i < rawtextlen; i++) { if (rawtext[i] == '~') { if (rawtext[i + 1] == '{') { hzstart++; i += 2; while (i < rawtextlen - 1) { if (rawtext[i] == 0x0A || rawtext[i] == 0x0D) { break; } else if (rawtext[i] == '~' && rawtext[i + 1] == '}') { hzend++; i++; break; } else if ((0x21 <= rawtext[i] && rawtext[i] <= 0x77) && (0x21 <= rawtext[i + 1] && rawtext[i + 1] <= 0x77)) { hzchars += 2; row = rawtext[i] - 0x21; column = rawtext[i + 1] - 0x21; totalfreq += 500; if (GBFreq[row][column] != 0) { hzfreq += GBFreq[row][column]; } else if (15 <= row && row < 55) { hzfreq += 200; } } else if ((0xA1 <= rawtext[i] && rawtext[i] <= 0xF7) && (0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= 0xF7)) { hzchars += 2; row = rawtext[i] + 256 - 0xA1; column = rawtext[i + 1] + 256 - 0xA1; totalfreq += 500; if (GBFreq[row][column] != 0) { hzfreq += GBFreq[row][column]; } else if (15 <= row && row < 55) { hzfreq += 200; } } dbchars += 2; i += 2; } } else if (rawtext[i + 1] == '}') { hzend++; i++; } else if (rawtext[i + 1] == '~') { i++; } } } if (hzstart > 4) { rangeval = 50; } else if (hzstart > 1) { rangeval = 41; } else if (hzstart > 0) { // Only 39 in case the sequence happened to // occur rangeval = 39; // in otherwise non-Hz text } else { rangeval = 0; } freqval = 50 * ((float) hzfreq / (float) totalfreq); return (int) (rangeval + freqval); } /** * Function: big5_probability Argument: byte array Returns : number from 0 * to 100 representing probability text in array uses Big5 encoding */ int big5_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, bfchars = 1; float rangeval = 0, freqval = 0; long bffreq = 0, totalfreq = 1; int row, column; // Check to see if characters fit into acceptable ranges rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { if (rawtext[i] >= 0) { // asciichars++; } else { dbchars++; if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xF9 && (((byte) 0x40 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E) || ((byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE))) { bfchars++; totalfreq += 500; row = rawtext[i] + 256 - 0xA1; if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) { column = rawtext[i + 1] - 0x40; } else { column = rawtext[i + 1] + 256 - 0x61; } if (Big5Freq[row][column] != 0) { bffreq += Big5Freq[row][column]; } else if (3 <= row && row <= 37) { bffreq += 200; } } i++; } } rangeval = 50 * ((float) bfchars / (float) dbchars); freqval = 50 * ((float) bffreq / (float) totalfreq); return (int) (rangeval + freqval); } /* * Function: big5plus_probability Argument: pointer to unsigned char array * Returns : number from 0 to 100 representing probability text in array * uses Big5+ encoding */ int big5plus_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, bfchars = 1; long bffreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int row, column; // Stage 1: Check to see if characters fit into acceptable ranges rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { // System.err.println(rawtext[i]); if (rawtext[i] >= 128) { // asciichars++; } else { dbchars++; if (0xA1 <= rawtext[i] && rawtext[i] <= 0xF9 && // Original Big5 range ((0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) || (0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= 0xFE))) { bfchars++; totalfreq += 500; row = rawtext[i] - 0xA1; if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) { column = rawtext[i + 1] - 0x40; } else { column = rawtext[i + 1] - 0x61; } // System.out.println("original row " + row + " column " + // column); if (Big5Freq[row][column] != 0) { bffreq += Big5Freq[row][column]; } else if (3 <= row && row < 37) { bffreq += 200; } } else if (0x81 <= rawtext[i] && rawtext[i] <= 0xFE && // Extended Big5 range ((0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) || (0x80 <= rawtext[i + 1] && rawtext[i + 1] <= 0xFE))) { bfchars++; totalfreq += 500; row = rawtext[i] - 0x81; if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) { column = rawtext[i + 1] - 0x40; } else { column = rawtext[i + 1] - 0x40; } // System.out.println("extended row " + row + " column " + // column + " rawtext[i] " + rawtext[i]); if (Big5PFreq[row][column] != 0) { bffreq += Big5PFreq[row][column]; } } i++; } } rangeval = 50 * ((float) bfchars / (float) dbchars); freqval = 50 * ((float) bffreq / (float) totalfreq); // For regular Big5 files, this would give the same score, so I handicap // it slightly return (int) (rangeval + freqval) - 1; } /* * Function: euc_tw_probability Argument: byte array Returns : number from 0 * to 100 representing probability text in array uses EUC-TW (CNS 11643) * encoding */ int euc_tw_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, cnschars = 1; long cnsfreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int row, column; // Check to see if characters fit into acceptable ranges // and have expected frequency of use rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { if (rawtext[i] >= 0) { // in ASCII range // asciichars++; } else { // high bit set dbchars++; if (i + 3 < rawtextlen && (byte) 0x8E == rawtext[i] && (byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xB0 && (byte) 0xA1 <= rawtext[i + 2] && rawtext[i + 2] <= (byte) 0xFE && (byte) 0xA1 <= rawtext[i + 3] && rawtext[i + 3] <= (byte) 0xFE) { // Planes 1 - 16 cnschars++; // System.out.println("plane 2 or above CNS char"); // These are all less frequent chars so just ignore freq i += 3; } else if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xFE && // Plane 1 (byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) { cnschars++; totalfreq += 500; row = rawtext[i] + 256 - 0xA1; column = rawtext[i + 1] + 256 - 0xA1; if (EUC_TWFreq[row][column] != 0) { cnsfreq += EUC_TWFreq[row][column]; } else if (35 <= row && row <= 92) { cnsfreq += 150; } i++; } } } rangeval = 50 * ((float) cnschars / (float) dbchars); freqval = 50 * ((float) cnsfreq / (float) totalfreq); return (int) (rangeval + freqval); } /* * Function: iso_2022_cn_probability Argument: byte array Returns : number * from 0 to 100 representing probability text in array uses ISO 2022-CN * encoding WORKS FOR BASIC CASES, BUT STILL NEEDS MORE WORK */ int iso_2022_cn_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, isochars = 1; long isofreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int row, column; // Check to see if characters fit into acceptable ranges // and have expected frequency of use rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { if (rawtext[i] == (byte) 0x1B && i + 3 < rawtextlen) { // Escape // char ESC if (rawtext[i + 1] == (byte) 0x24 && rawtext[i + 2] == 0x29 && rawtext[i + 3] == (byte) 0x41) { // GB Escape $ ) A i += 4; while (rawtext[i] != (byte) 0x1B) { dbchars++; if ((0x21 <= rawtext[i] && rawtext[i] <= 0x77) && (0x21 <= rawtext[i + 1] && rawtext[i + 1] <= 0x77)) { isochars++; row = rawtext[i] - 0x21; column = rawtext[i + 1] - 0x21; totalfreq += 500; if (GBFreq[row][column] != 0) { isofreq += GBFreq[row][column]; } else if (15 <= row && row < 55) { isofreq += 200; } i++; } i++; } } else if (i + 3 < rawtextlen && rawtext[i + 1] == (byte) 0x24 && rawtext[i + 2] == (byte) 0x29 && rawtext[i + 3] == (byte) 0x47) { // CNS Escape $ ) G i += 4; while (rawtext[i] != (byte) 0x1B) { dbchars++; if ((byte) 0x21 <= rawtext[i] && rawtext[i] <= (byte) 0x7E && (byte) 0x21 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E) { isochars++; totalfreq += 500; row = rawtext[i] - 0x21; column = rawtext[i + 1] - 0x21; if (EUC_TWFreq[row][column] != 0) { isofreq += EUC_TWFreq[row][column]; } else if (35 <= row && row <= 92) { isofreq += 150; } i++; } i++; } } if (rawtext[i] == (byte) 0x1B && i + 2 < rawtextlen && rawtext[i + 1] == (byte) 0x28 && rawtext[i + 2] == (byte) 0x42) { // ASCII: // ESC // ( B i += 2; } } } rangeval = 50 * ((float) isochars / (float) dbchars); freqval = 50 * ((float) isofreq / (float) totalfreq); // System.out.println("isochars dbchars isofreq totalfreq " + isochars + // " " + dbchars + " " + isofreq + " " + totalfreq + " // " + rangeval + " " + freqval); return (int) (rangeval + freqval); // return 0; } /* * Function: utf8_probability Argument: byte array Returns : number from 0 * to 100 representing probability text in array uses UTF-8 encoding of * Unicode */ int utf8_probability(byte[] rawtext) { int score = 0; int i, rawtextlen = 0; int goodbytes = 0, asciibytes = 0; // Maybe also use UTF8 Byte Order Mark: EF BB BF // Check to see if characters fit into acceptable ranges rawtextlen = rawtext.length; for (i = 0; i < rawtextlen; i++) { if ((rawtext[i] & (byte) 0x7F) == rawtext[i]) { // One byte asciibytes++; // Ignore ASCII, can throw off count } else if (-64 <= rawtext[i] && rawtext[i] <= -33 && // Two bytes i + 1 < rawtextlen && -128 <= rawtext[i + 1] && rawtext[i + 1] <= -65) { goodbytes += 2; i++; } else if (-32 <= rawtext[i] && rawtext[i] <= -17 && // Three bytes i + 2 < rawtextlen && -128 <= rawtext[i + 1] && rawtext[i + 1] <= -65 && -128 <= rawtext[i + 2] && rawtext[i + 2] <= -65) { goodbytes += 3; i += 2; } } if (asciibytes == rawtextlen) { return 0; } score = (int) (100 * ((float) goodbytes / (float) (rawtextlen - asciibytes))); // System.out.println("rawtextlen " + rawtextlen + " goodbytes " + // goodbytes + " asciibytes " + asciibytes + " score " + // score); // If not above 98, reduce to zero to prevent coincidental matches // Allows for some (few) bad formed sequences if (score > 98) { return score; } else if (score > 95 && goodbytes > 30) { return score; } else { return 0; } } /* * Function: utf16_probability Argument: byte array Returns : number from 0 * to 100 representing probability text in array uses UTF-16 encoding of * Unicode, guess based on BOM // NOT VERY GENERAL, NEEDS MUCH MORE WORK */ int utf16_probability(byte[] rawtext) { // int score = 0; // int i, rawtextlen = 0; // int goodbytes = 0, asciibytes = 0; if (rawtext.length > 1 && ((byte) 0xFE == rawtext[0] && (byte) 0xFF == rawtext[1]) || // Big-endian ((byte) 0xFF == rawtext[0] && (byte) 0xFE == rawtext[1])) { // Little-endian return 100; } return 0; /* * // Check to see if characters fit into acceptable ranges rawtextlen = * rawtext.length; for (i = 0; i < rawtextlen; i++) { if ((rawtext[i] & * (byte)0x7F) == rawtext[i]) { // One byte goodbytes += 1; * asciibytes++; } else if ((rawtext[i] & (byte)0xDF) == rawtext[i]) { * // Two bytes if (i+1 < rawtextlen && (rawtext[i+1] & (byte)0xBF) == * rawtext[i+1]) { goodbytes += 2; i++; } } else if ((rawtext[i] & * (byte)0xEF) == rawtext[i]) { // Three bytes if (i+2 < rawtextlen && * (rawtext[i+1] & (byte)0xBF) == rawtext[i+1] && (rawtext[i+2] & * (byte)0xBF) == rawtext[i+2]) { goodbytes += 3; i+=2; } } } * * score = (int)(100 * ((float)goodbytes/(float)rawtext.length)); // An * all ASCII file is also a good UTF8 file, but I'd rather it // get * identified as ASCII. Can delete following 3 lines otherwise if * (goodbytes == asciibytes) { score = 0; } // If not above 90, reduce * to zero to prevent coincidental matches if (score > 90) { return * score; } else { return 0; } */ } /* * Function: ascii_probability Argument: byte array Returns : number from 0 * to 100 representing probability text in array uses all ASCII Description: * Sees if array has any characters not in ASCII range, if so, score is * reduced */ int ascii_probability(byte[] rawtext) { int score = 75; int i, rawtextlen; rawtextlen = rawtext.length; for (i = 0; i < rawtextlen; i++) { if (rawtext[i] < 0) { score = score - 5; } else if (rawtext[i] == (byte) 0x1B) { // ESC (used by ISO 2022) score = score - 5; } if (score <= 0) { return 0; } } return score; } /* * Function: euc_kr__probability Argument: pointer to byte array Returns : * number from 0 to 100 representing probability text in array uses EUC-KR * encoding */ int euc_kr_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, krchars = 1; long krfreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int row, column; // Stage 1: Check to see if characters fit into acceptable ranges rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { // System.err.println(rawtext[i]); if (rawtext[i] >= 0) { // asciichars++; } else { dbchars++; if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xFE && (byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) { krchars++; totalfreq += 500; row = rawtext[i] + 256 - 0xA1; column = rawtext[i + 1] + 256 - 0xA1; if (KRFreq[row][column] != 0) { krfreq += KRFreq[row][column]; } else if (15 <= row && row < 55) { krfreq += 0; } } i++; } } rangeval = 50 * ((float) krchars / (float) dbchars); freqval = 50 * ((float) krfreq / (float) totalfreq); return (int) (rangeval + freqval); } /* * Function: cp949__probability Argument: pointer to byte array Returns : * number from 0 to 100 representing probability text in array uses Cp949 * encoding */ int cp949_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, krchars = 1; long krfreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int row, column; // Stage 1: Check to see if characters fit into acceptable ranges rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { // System.err.println(rawtext[i]); if (rawtext[i] >= 0) { // asciichars++; } else { dbchars++; if ((byte) 0x81 <= rawtext[i] && rawtext[i] <= (byte) 0xFE && ((byte) 0x41 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x5A || (byte) 0x61 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7A || (byte) 0x81 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE)) { krchars++; totalfreq += 500; if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xFE && (byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) { row = rawtext[i] + 256 - 0xA1; column = rawtext[i + 1] + 256 - 0xA1; if (KRFreq[row][column] != 0) { krfreq += KRFreq[row][column]; } } } i++; } } rangeval = 50 * ((float) krchars / (float) dbchars); freqval = 50 * ((float) krfreq / (float) totalfreq); return (int) (rangeval + freqval); } int iso_2022_kr_probability(byte[] rawtext) { int i; for (i = 0; i < rawtext.length; i++) { if (i + 3 < rawtext.length && rawtext[i] == 0x1b && (char) rawtext[i + 1] == '$' && (char) rawtext[i + 2] == ')' && (char) rawtext[i + 3] == 'C') { return 100; } } return 0; } /* * Function: euc_jp_probability Argument: pointer to byte array Returns : * number from 0 to 100 representing probability text in array uses EUC-JP * encoding */ int euc_jp_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, jpchars = 1; long jpfreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int row, column; // Stage 1: Check to see if characters fit into acceptable ranges rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { // System.err.println(rawtext[i]); if (rawtext[i] >= 0) { // asciichars++; } else { dbchars++; if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xFE && (byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) { jpchars++; totalfreq += 500; row = rawtext[i] + 256 - 0xA1; column = rawtext[i + 1] + 256 - 0xA1; if (JPFreq[row][column] != 0) { jpfreq += JPFreq[row][column]; } else if (15 <= row && row < 55) { jpfreq += 0; } } i++; } } rangeval = 50 * ((float) jpchars / (float) dbchars); freqval = 50 * ((float) jpfreq / (float) totalfreq); return (int) (rangeval + freqval); } int iso_2022_jp_probability(byte[] rawtext) { int i; for (i = 0; i < rawtext.length; i++) { if (i + 2 < rawtext.length && rawtext[i] == 0x1b && (char) rawtext[i + 1] == '$' && (char) rawtext[i + 2] == 'B') { return 100; } } return 0; } /* * Function: sjis_probability Argument: pointer to byte array Returns : * number from 0 to 100 representing probability text in array uses * Shift-JIS encoding */ int sjis_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, jpchars = 1; long jpfreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int row, column, adjust; // Stage 1: Check to see if characters fit into acceptable ranges rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { // System.err.println(rawtext[i]); if (rawtext[i] >= 0) { // asciichars++; } else { dbchars++; if (i + 1 < rawtext.length && (((byte) 0x81 <= rawtext[i] && rawtext[i] <= (byte) 0x9F) || ((byte) 0xE0 <= rawtext[i] && rawtext[i] <= (byte) 0xEF)) && (((byte) 0x40 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E) || ((byte) 0x80 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFC))) { jpchars++; totalfreq += 500; row = rawtext[i] + 256; column = rawtext[i + 1] + 256; if (column < 0x9f) { adjust = 1; if (column > 0x7f) { column -= 0x20; } else { column -= 0x19; } } else { adjust = 0; column -= 0x7e; } if (row < 0xa0) { row = ((row - 0x70) << 1) - adjust; } else { row = ((row - 0xb0) << 1) - adjust; } row -= 0x20; column = 0x20; // System.out.println("original row " + row + " column " + // column); if (row < JPFreq.length && column < JPFreq[row].length && JPFreq[row][column] != 0) { jpfreq += JPFreq[row][column]; } i++; } else if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xDF) { // half-width katakana, convert to full-width } } } rangeval = 50 * ((float) jpchars / (float) dbchars); freqval = 50 * ((float) jpfreq / (float) totalfreq); // For regular GB files, this would give the same score, so I handicap // it slightly return (int) (rangeval + freqval) - 1; } void initialize_frequencies() { int i, j; for (i = 93; i >= 0; i--) { for (j = 93; j >= 0; j--) { GBFreq[i][j] = 0; } } for (i = 125; i >= 0; i--) { for (j = 190; j >= 0; j--) { GBKFreq[i][j] = 0; } } // for (i = 0; i < 94; i++) { // for (j = 0; j < 158; j++) { for (i = 93; i >= 0; i--) { for (j = 157; j >= 0; j--) { Big5Freq[i][j] = 0; } } // for (i = 0; i < 126; i++) { // for (j = 0; j < 191; j++) { for (i = 125; i >= 0; i--) { for (j = 190; j >= 0; j--) { Big5PFreq[i][j] = 0; } } // for (i = 0; i < 94; i++) { // for (j = 0; j < 94; j++) { for (i = 93; i >= 0; i--) { for (j = 93; j >= 0; j--) { EUC_TWFreq[i][j] = 0; } } for (i = 93; i >= 0; i--) { for (j = 93; j >= 0; j--) { JPFreq[i][j] = 0; } } GBFreq[20][35] = 599; GBFreq[49][26] = 598; GBFreq[41][38] = 597; GBFreq[17][26] = 596; GBFreq[32][42] = 595; GBFreq[39][42] = 594; GBFreq[45][49] = 593; GBFreq[51][57] = 592; GBFreq[50][47] = 591; GBFreq[42][90] = 590; GBFreq[52][65] = 589; GBFreq[53][47] = 588; GBFreq[19][82] = 587; GBFreq[31][19] = 586; GBFreq[40][46] = 585; GBFreq[24][89] = 584; GBFreq[23][85] = 583; GBFreq[20][28] = 582; GBFreq[42][20] = 581; GBFreq[34][38] = 580; GBFreq[45][9] = 579; GBFreq[54][50] = 578; GBFreq[25][44] = 577; GBFreq[35][66] = 576; GBFreq[20][55] = 575; GBFreq[18][85] = 574; GBFreq[20][31] = 573; GBFreq[49][17] = 572; GBFreq[41][16] = 571; GBFreq[35][73] = 570; GBFreq[20][34] = 569; GBFreq[29][44] = 568; GBFreq[35][38] = 567; GBFreq[49][9] = 566; GBFreq[46][33] = 565; GBFreq[49][51] = 564; GBFreq[40][89] = 563; GBFreq[26][64] = 562; GBFreq[54][51] = 561; GBFreq[54][36] = 560; GBFreq[39][4] = 559; GBFreq[53][13] = 558; GBFreq[24][92] = 557; GBFreq[27][49] = 556; GBFreq[48][6] = 555; GBFreq[21][51] = 554; GBFreq[30][40] = 553; GBFreq[42][92] = 552; GBFreq[31][78] = 551; GBFreq[25][82] = 550; GBFreq[47][0] = 549; GBFreq[34][19] = 548; GBFreq[47][35] = 547; GBFreq[21][63] = 546; GBFreq[43][75] = 545; GBFreq[21][87] = 544; GBFreq[35][59] = 543; GBFreq[25][34] = 542; GBFreq[21][27] = 541; GBFreq[39][26] = 540; GBFreq[34][26] = 539; GBFreq[39][52] = 538; GBFreq[50][57] = 537; GBFreq[37][79] = 536; GBFreq[26][24] = 535; GBFreq[22][1] = 534; GBFreq[18][40] = 533; GBFreq[41][33] = 532; GBFreq[53][26] = 531; GBFreq[54][86] = 530; GBFreq[20][16] = 529; GBFreq[46][74] = 528; GBFreq[30][19] = 527; GBFreq[45][35] = 526; GBFreq[45][61] = 525; GBFreq[30][9] = 524; GBFreq[41][53] = 523; GBFreq[41][13] = 522; GBFreq[50][34] = 521; GBFreq[53][86] = 520; GBFreq[47][47] = 519; GBFreq[22][28] = 518; GBFreq[50][53] = 517; GBFreq[39][70] = 516; GBFreq[38][15] = 515; GBFreq[42][88] = 514; GBFreq[16][29] = 513; GBFreq[27][90] = 512; GBFreq[29][12] = 511; GBFreq[44][22] = 510; GBFreq[34][69] = 509; GBFreq[24][10] = 508; GBFreq[44][11] = 507; GBFreq[39][92] = 506; GBFreq[49][48] = 505; GBFreq[31][46] = 504; GBFreq[19][50] = 503; GBFreq[21][14] = 502; GBFreq[32][28] = 501; GBFreq[18][3] = 500; GBFreq[53][9] = 499; GBFreq[34][80] = 498; GBFreq[48][88] = 497; GBFreq[46][53] = 496; GBFreq[22][53] = 495; GBFreq[28][10] = 494; GBFreq[44][65] = 493; GBFreq[20][10] = 492; GBFreq[40][76] = 491; GBFreq[47][8] = 490; GBFreq[50][74] = 489; GBFreq[23][62] = 488; GBFreq[49][65] = 487; GBFreq[28][87] = 486; GBFreq[15][48] = 485; GBFreq[22][7] = 484; GBFreq[19][42] = 483; GBFreq[41][20] = 482; GBFreq[26][55] = 481; GBFreq[21][93] = 480; GBFreq[31][76] = 479; GBFreq[34][31] = 478; GBFreq[20][66] = 477; GBFreq[51][33] = 476; GBFreq[34][86] = 475; GBFreq[37][67] = 474; GBFreq[53][53] = 473; GBFreq[40][88] = 472; GBFreq[39][10] = 471; GBFreq[24][3] = 470; GBFreq[27][25] = 469; GBFreq[26][15] = 468; GBFreq[21][88] = 467; GBFreq[52][62] = 466; GBFreq[46][81] = 465; GBFreq[38][72] = 464; GBFreq[17][30] = 463; GBFreq[52][92] = 462; GBFreq[34][90] = 461; GBFreq[21][7] = 460; GBFreq[36][13] = 459; GBFreq[45][41] = 458; GBFreq[32][5] = 457; GBFreq[26][89] = 456; GBFreq[23][87] = 455; GBFreq[20][39] = 454; GBFreq[27][23] = 453; GBFreq[25][59] = 452; GBFreq[49][20] = 451; GBFreq[54][77] = 450; GBFreq[27][67] = 449; GBFreq[47][33] = 448; GBFreq[41][17] = 447; GBFreq[19][81] = 446; GBFreq[16][66] = 445; GBFreq[45][26] = 444; GBFreq[49][81] = 443; GBFreq[53][55] = 442; GBFreq[16][26] = 441; GBFreq[54][62] = 440; GBFreq[20][70] = 439; GBFreq[42][35] = 438; GBFreq[20][57] = 437; GBFreq[34][36] = 436; GBFreq[46][63] = 435; GBFreq[19][45] = 434; GBFreq[21][10] = 433; GBFreq[52][93] = 432; GBFreq[25][2] = 431; GBFreq[30][57] = 430; GBFreq[41][24] = 429; GBFreq[28][43] = 428; GBFreq[45][86] = 427; GBFreq[51][56] = 426; GBFreq[37][28] = 425; GBFreq[52][69] = 424; GBFreq[43][92] = 423; GBFreq[41][31] = 422; GBFreq[37][87] = 421; GBFreq[47][36] = 420; GBFreq[16][16] = 419; GBFreq[40][56] = 418; GBFreq[24][55] = 417; GBFreq[17][1] = 416; GBFreq[35][57] = 415; GBFreq[27][50] = 414; GBFreq[26][14] = 413; GBFreq[50][40] = 412; GBFreq[39][19] = 411; GBFreq[19][89] = 410; GBFreq[29][91] = 409; GBFreq[17][89] = 408; GBFreq[39][74] = 407; GBFreq[46][39] = 406; GBFreq[40][28] = 405; GBFreq[45][68] = 404; GBFreq[43][10] = 403; GBFreq[42][13] = 402; GBFreq[44][81] = 401; GBFreq[41][47] = 400; GBFreq[48][58] = 399; GBFreq[43][68] = 398; GBFreq[16][79] = 397; GBFreq[19][5] = 396; GBFreq[54][59] = 395; GBFreq[17][36] = 394; GBFreq[18][0] = 393; GBFreq[41][5] = 392; GBFreq[41][72] = 391; GBFreq[16][39] = 390; GBFreq[54][0] = 389; GBFreq[51][16] = 388; GBFreq[29][36] = 387; GBFreq[47][5] = 386; GBFreq[47][51] = 385; GBFreq[44][7] = 384; GBFreq[35][30] = 383; GBFreq[26][9] = 382; GBFreq[16][7] = 381; GBFreq[32][1] = 380; GBFreq[33][76] = 379; GBFreq[34][91] = 378; GBFreq[52][36] = 377; GBFreq[26][77] = 376; GBFreq[35][48] = 375; GBFreq[40][80] = 374; GBFreq[41][92] = 373; GBFreq[27][93] = 372; GBFreq[15][17] = 371; GBFreq[16][76] = 370; GBFreq[51][12] = 369; GBFreq[18][20] = 368; GBFreq[15][54] = 367; GBFreq[50][5] = 366; GBFreq[33][22] = 365; GBFreq[37][57] = 364; GBFreq[28][47] = 363; GBFreq[42][31] = 362; GBFreq[18][2] = 361; GBFreq[43][64] = 360; GBFreq[23][47] = 359; GBFreq[28][79] = 358; GBFreq[25][45] = 357; GBFreq[23][91] = 356; GBFreq[22][19] = 355; GBFreq[25][46] = 354; GBFreq[22][36] = 353; GBFreq[54][85] = 352; GBFreq[46][20] = 351; GBFreq[27][37] = 350; GBFreq[26][81] = 349; GBFreq[42][29] = 348; GBFreq[31][90] = 347; GBFreq[41][59] = 346; GBFreq[24][65] = 345; GBFreq[44][84] = 344; GBFreq[24][90] = 343; GBFreq[38][54] = 342; GBFreq[28][70] = 341; GBFreq[27][15] = 340; GBFreq[28][80] = 339; GBFreq[29][8] = 338; GBFreq[45][80] = 337; GBFreq[53][37] = 336; GBFreq[28][65] = 335; GBFreq[23][86] = 334; GBFreq[39][45] = 333; GBFreq[53][32] = 332; GBFreq[38][68] = 331; GBFreq[45][78] = 330; GBFreq[43][7] = 329; GBFreq[46][82] = 328; GBFreq[27][38] = 327; GBFreq[16][62] = 326; GBFreq[24][17] = 325; GBFreq[22][70] = 324; GBFreq[52][28] = 323; GBFreq[23][40] = 322; GBFreq[28][50] = 321; GBFreq[42][91] = 320; GBFreq[47][76] = 319; GBFreq[15][42] = 318; GBFreq[43][55] = 317; GBFreq[29][84] = 316; GBFreq[44][90] = 315; GBFreq[53][16] = 314; GBFreq[22][93] = 313; GBFreq[34][10] = 312; GBFreq[32][53] = 311; GBFreq[43][65] = 310; GBFreq[28][7] = 309; GBFreq[35][46] = 308; GBFreq[21][39] = 307; GBFreq[44][18] = 306; GBFreq[40][10] = 305; GBFreq[54][53] = 304; GBFreq[38][74] = 303; GBFreq[28][26] = 302; GBFreq[15][13] = 301; GBFreq[39][34] = 300; GBFreq[39][46] = 299; GBFreq[42][66] = 298; GBFreq[33][58] = 297; GBFreq[15][56] = 296; GBFreq[18][51] = 295; GBFreq[49][68] = 294; GBFreq[30][37] = 293; GBFreq[51][84] = 292; GBFreq[51][9] = 291; GBFreq[40][70] = 290; GBFreq[41][84] = 289; GBFreq[28][64] = 288; GBFreq[32][88] = 287; GBFreq[24][5] = 286; GBFreq[53][23] = 285; GBFreq[42][27] = 284; GBFreq[22][38] = 283; GBFreq[32][86] = 282; GBFreq[34][30] = 281; GBFreq[38][63] = 280; GBFreq[24][59] = 279; GBFreq[22][81] = 278; GBFreq[32][11] = 277; GBFreq[51][21] = 276; GBFreq[54][41] = 275; GBFreq[21][50] = 274; GBFreq[23][89] = 273; GBFreq[19][87] = 272; GBFreq[26][7] = 271; GBFreq[30][75] = 270; GBFreq[43][84] = 269; GBFreq[51][25] = 268; GBFreq[16][67] = 267; GBFreq[32][9] = 266; GBFreq[48][51] = 265; GBFreq[39][7] = 264; GBFreq[44][88] = 263; GBFreq[52][24] = 262; GBFreq[23][34] = 261; GBFreq[32][75] = 260; GBFreq[19][10] = 259; GBFreq[28][91] = 258; GBFreq[32][83] = 257; GBFreq[25][75] = 256; GBFreq[53][45] = 255; GBFreq[29][85] = 254; GBFreq[53][59] = 253; GBFreq[16][2] = 252; GBFreq[19][78] = 251; GBFreq[15][75] = 250; GBFreq[51][42] = 249; GBFreq[45][67] = 248; GBFreq[15][74] = 247; GBFreq[25][81] = 246; GBFreq[37][62] = 245; GBFreq[16][55] = 244; GBFreq[18][38] = 243; GBFreq[23][23] = 242; GBFreq[38][30] = 241; GBFreq[17][28] = 240; GBFreq[44][73] = 239; GBFreq[23][78] = 238; GBFreq[40][77] = 237; GBFreq[38][87] = 236; GBFreq[27][19] = 235; GBFreq[38][82] = 234; GBFreq[37][22] = 233; GBFreq[41][30] = 232; GBFreq[54][9] = 231; GBFreq[32][30] = 230; GBFreq[30][52] = 229; GBFreq[40][84] = 228; GBFreq[53][57] = 227; GBFreq[27][27] = 226; GBFreq[38][64] = 225; GBFreq[18][43] = 224; GBFreq[23][69] = 223; GBFreq[28][12] = 222; GBFreq[50][78] = 221; GBFreq[50][1] = 220; GBFreq[26][88] = 219; GBFreq[36][40] = 218; GBFreq[33][89] = 217; GBFreq[41][28] = 216; GBFreq[31][77] = 215; GBFreq[46][1] = 214; GBFreq[47][19] = 213; GBFreq[35][55] = 212; GBFreq[41][21] = 211; GBFreq[27][10] = 210; GBFreq[32][77] = 209; GBFreq[26][37] = 208; GBFreq[20][33] = 207; GBFreq[41][52] = 206; GBFreq[32][18] = 205; GBFreq[38][13] = 204; GBFreq[20][18] = 203; GBFreq[20][24] = 202; GBFreq[45][19] = 201; GBFreq[18][53] = 200; /* * GBFreq[39][0] = 199; GBFreq[40][71] = 198; GBFreq[41][27] = 197; * GBFreq[15][69] = 196; GBFreq[42][10] = 195; GBFreq[31][89] = 194; * GBFreq[51][28] = 193; GBFreq[41][22] = 192; GBFreq[40][43] = 191; * GBFreq[38][6] = 190; GBFreq[37][11] = 189; GBFreq[39][60] = 188; * GBFreq[48][47] = 187; GBFreq[46][80] = 186; GBFreq[52][49] = 185; * GBFreq[50][48] = 184; GBFreq[25][1] = 183; GBFreq[52][29] = 182; * GBFreq[24][66] = 181; GBFreq[23][35] = 180; GBFreq[49][72] = 179; * GBFreq[47][45] = 178; GBFreq[45][14] = 177; GBFreq[51][70] = 176; * GBFreq[22][30] = 175; GBFreq[49][83] = 174; GBFreq[26][79] = 173; * GBFreq[27][41] = 172; GBFreq[51][81] = 171; GBFreq[41][54] = 170; * GBFreq[20][4] = 169; GBFreq[29][60] = 168; GBFreq[20][27] = 167; * GBFreq[50][15] = 166; GBFreq[41][6] = 165; GBFreq[35][34] = 164; * GBFreq[44][87] = 163; GBFreq[46][66] = 162; GBFreq[42][37] = 161; * GBFreq[42][24] = 160; GBFreq[54][7] = 159; GBFreq[41][14] = 158; * GBFreq[39][83] = 157; GBFreq[16][87] = 156; GBFreq[20][59] = 155; * GBFreq[42][12] = 154; GBFreq[47][2] = 153; GBFreq[21][32] = 152; * GBFreq[53][29] = 151; GBFreq[22][40] = 150; GBFreq[24][58] = 149; * GBFreq[52][88] = 148; GBFreq[29][30] = 147; GBFreq[15][91] = 146; * GBFreq[54][72] = 145; GBFreq[51][75] = 144; GBFreq[33][67] = 143; * GBFreq[41][50] = 142; GBFreq[27][34] = 141; GBFreq[46][17] = 140; * GBFreq[31][74] = 139; GBFreq[42][67] = 138; GBFreq[54][87] = 137; * GBFreq[27][14] = 136; GBFreq[16][63] = 135; GBFreq[16][5] = 134; * GBFreq[43][23] = 133; GBFreq[23][13] = 132; GBFreq[31][12] = 131; * GBFreq[25][57] = 130; GBFreq[38][49] = 129; GBFreq[42][69] = 128; * GBFreq[23][80] = 127; GBFreq[29][0] = 126; GBFreq[28][2] = 125; * GBFreq[28][17] = 124; GBFreq[17][27] = 123; GBFreq[40][16] = 122; * GBFreq[45][1] = 121; GBFreq[36][33] = 120; GBFreq[35][23] = 119; * GBFreq[20][86] = 118; GBFreq[29][53] = 117; GBFreq[23][88] = 116; * GBFreq[51][87] = 115; GBFreq[54][27] = 114; GBFreq[44][36] = 113; * GBFreq[21][45] = 112; GBFreq[53][52] = 111; GBFreq[31][53] = 110; * GBFreq[38][47] = 109; GBFreq[27][21] = 108; GBFreq[30][42] = 107; * GBFreq[29][10] = 106; GBFreq[35][35] = 105; GBFreq[24][56] = 104; * GBFreq[41][29] = 103; GBFreq[18][68] = 102; GBFreq[29][24] = 101; * GBFreq[25][84] = 100; GBFreq[35][47] = 99; GBFreq[29][56] = 98; * GBFreq[30][44] = 97; GBFreq[53][3] = 96; GBFreq[30][63] = 95; * GBFreq[52][52] = 94; GBFreq[54][1] = 93; GBFreq[22][48] = 92; * GBFreq[54][66] = 91; GBFreq[21][90] = 90; GBFreq[52][47] = 89; * GBFreq[39][25] = 88; GBFreq[39][39] = 87; GBFreq[44][37] = 86; * GBFreq[44][76] = 85; GBFreq[46][75] = 84; GBFreq[18][37] = 83; * GBFreq[47][42] = 82; GBFreq[19][92] = 81; GBFreq[51][27] = 80; * GBFreq[48][83] = 79; GBFreq[23][70] = 78; GBFreq[29][9] = 77; * GBFreq[33][79] = 76; GBFreq[52][90] = 75; GBFreq[53][6] = 74; * GBFreq[24][36] = 73; GBFreq[25][25] = 72; GBFreq[44][26] = 71; * GBFreq[25][36] = 70; GBFreq[29][87] = 69; GBFreq[48][0] = 68; * GBFreq[15][40] = 67; GBFreq[17][45] = 66; GBFreq[30][14] = 65; * GBFreq[48][38] = 64; GBFreq[23][19] = 63; GBFreq[40][42] = 62; * GBFreq[31][63] = 61; GBFreq[16][23] = 60; GBFreq[26][21] = 59; * GBFreq[32][76] = 58; GBFreq[23][58] = 57; GBFreq[41][37] = 56; * GBFreq[30][43] = 55; GBFreq[47][38] = 54; GBFreq[21][46] = 53; * GBFreq[18][33] = 52; GBFreq[52][37] = 51; GBFreq[36][8] = 50; * GBFreq[49][24] = 49; GBFreq[15][66] = 48; GBFreq[35][77] = 47; * GBFreq[27][58] = 46; GBFreq[35][51] = 45; GBFreq[24][69] = 44; * GBFreq[20][54] = 43; GBFreq[24][41] = 42; GBFreq[41][0] = 41; * GBFreq[33][71] = 40; GBFreq[23][52] = 39; GBFreq[29][67] = 38; * GBFreq[46][51] = 37; GBFreq[46][90] = 36; GBFreq[49][33] = 35; * GBFreq[33][28] = 34; GBFreq[37][86] = 33; GBFreq[39][22] = 32; * GBFreq[37][37] = 31; GBFreq[29][62] = 30; GBFreq[29][50] = 29; * GBFreq[36][89] = 28; GBFreq[42][44] = 27; GBFreq[51][82] = 26; * GBFreq[28][83] = 25; GBFreq[15][78] = 24; GBFreq[46][62] = 23; * GBFreq[19][69] = 22; GBFreq[51][23] = 21; GBFreq[37][69] = 20; * GBFreq[25][5] = 19; GBFreq[51][85] = 18; GBFreq[48][77] = 17; * GBFreq[32][46] = 16; GBFreq[53][60] = 15; GBFreq[28][57] = 14; * GBFreq[54][82] = 13; GBFreq[54][15] = 12; GBFreq[49][54] = 11; * GBFreq[53][87] = 10; GBFreq[27][16] = 9; GBFreq[29][34] = 8; * GBFreq[20][44] = 7; GBFreq[42][73] = 6; GBFreq[47][71] = 5; * GBFreq[29][37] = 4; GBFreq[25][50] = 3; GBFreq[18][84] = 2; * GBFreq[50][45] = 1; GBFreq[48][46] = 0; */ // GBFreq[43][89] = -1; GBFreq[54][68] = -2; Big5Freq[9][89] = 600; Big5Freq[11][15] = 599; Big5Freq[3][66] = 598; Big5Freq[6][121] = 597; Big5Freq[3][0] = 596; Big5Freq[5][82] = 595; Big5Freq[3][42] = 594; Big5Freq[5][34] = 593; Big5Freq[3][8] = 592; Big5Freq[3][6] = 591; Big5Freq[3][67] = 590; Big5Freq[7][139] = 589; Big5Freq[23][137] = 588; Big5Freq[12][46] = 587; Big5Freq[4][8] = 586; Big5Freq[4][41] = 585; Big5Freq[18][47] = 584; Big5Freq[12][114] = 583; Big5Freq[6][1] = 582; Big5Freq[22][60] = 581; Big5Freq[5][46] = 580; Big5Freq[11][79] = 579; Big5Freq[3][23] = 578; Big5Freq[7][114] = 577; Big5Freq[29][102] = 576; Big5Freq[19][14] = 575; Big5Freq[4][133] = 574; Big5Freq[3][29] = 573; Big5Freq[4][109] = 572; Big5Freq[14][127] = 571; Big5Freq[5][48] = 570; Big5Freq[13][104] = 569; Big5Freq[3][132] = 568; Big5Freq[26][64] = 567; Big5Freq[7][19] = 566; Big5Freq[4][12] = 565; Big5Freq[11][124] = 564; Big5Freq[7][89] = 563; Big5Freq[15][124] = 562; Big5Freq[4][108] = 561; Big5Freq[19][66] = 560; Big5Freq[3][21] = 559; Big5Freq[24][12] = 558; Big5Freq[28][111] = 557; Big5Freq[12][107] = 556; Big5Freq[3][112] = 555; Big5Freq[8][113] = 554; Big5Freq[5][40] = 553; Big5Freq[26][145] = 552; Big5Freq[3][48] = 551; Big5Freq[3][70] = 550; Big5Freq[22][17] = 549; Big5Freq[16][47] = 548; Big5Freq[3][53] = 547; Big5Freq[4][24] = 546; Big5Freq[32][120] = 545; Big5Freq[24][49] = 544; Big5Freq[24][142] = 543; Big5Freq[18][66] = 542; Big5Freq[29][150] = 541; Big5Freq[5][122] = 540; Big5Freq[5][114] = 539; Big5Freq[3][44] = 538; Big5Freq[10][128] = 537; Big5Freq[15][20] = 536; Big5Freq[13][33] = 535; Big5Freq[14][87] = 534; Big5Freq[3][126] = 533; Big5Freq[4][53] = 532; Big5Freq[4][40] = 531; Big5Freq[9][93] = 530; Big5Freq[15][137] = 529; Big5Freq[10][123] = 528; Big5Freq[4][56] = 527; Big5Freq[5][71] = 526; Big5Freq[10][8] = 525; Big5Freq[5][16] = 524; Big5Freq[5][146] = 523; Big5Freq[18][88] = 522; Big5Freq[24][4] = 521; Big5Freq[20][47] = 520; Big5Freq[5][33] = 519; Big5Freq[9][43] = 518; Big5Freq[20][12] = 517; Big5Freq[20][13] = 516; Big5Freq[5][156] = 515; Big5Freq[22][140] = 514; Big5Freq[8][146] = 513; Big5Freq[21][123] = 512; Big5Freq[4][90] = 511; Big5Freq[5][62] = 510; Big5Freq[17][59] = 509; Big5Freq[10][37] = 508; Big5Freq[18][107] = 507; Big5Freq[14][53] = 506; Big5Freq[22][51] = 505; Big5Freq[8][13] = 504; Big5Freq[5][29] = 503; Big5Freq[9][7] = 502; Big5Freq[22][14] = 501; Big5Freq[8][55] = 500; Big5Freq[33][9] = 499; Big5Freq[16][64] = 498; Big5Freq[7][131] = 497; Big5Freq[34][4] = 496; Big5Freq[7][101] = 495; Big5Freq[11][139] = 494; Big5Freq[3][135] = 493; Big5Freq[7][102] = 492; Big5Freq[17][13] = 491; Big5Freq[3][20] = 490; Big5Freq[27][106] = 489; Big5Freq[5][88] = 488; Big5Freq[6][33] = 487; Big5Freq[5][139] = 486; Big5Freq[6][0] = 485; Big5Freq[17][58] = 484; Big5Freq[5][133] = 483; Big5Freq[9][107] = 482; Big5Freq[23][39] = 481; Big5Freq[5][23] = 480; Big5Freq[3][79] = 479; Big5Freq[32][97] = 478; Big5Freq[3][136] = 477; Big5Freq[4][94] = 476; Big5Freq[21][61] = 475; Big5Freq[23][123] = 474; Big5Freq[26][16] = 473; Big5Freq[24][137] = 472; Big5Freq[22][18] = 471; Big5Freq[5][1] = 470; Big5Freq[20][119] = 469; Big5Freq[3][7] = 468; Big5Freq[10][79] = 467; Big5Freq[15][105] = 466; Big5Freq[3][144] = 465; Big5Freq[12][80] = 464; Big5Freq[15][73] = 463; Big5Freq[3][19] = 462; Big5Freq[8][109] = 461; Big5Freq[3][15] = 460; Big5Freq[31][82] = 459; Big5Freq[3][43] = 458; Big5Freq[25][119] = 457; Big5Freq[16][111] = 456; Big5Freq[7][77] = 455; Big5Freq[3][95] = 454; Big5Freq[24][82] = 453; Big5Freq[7][52] = 452; Big5Freq[9][151] = 451; Big5Freq[3][129] = 450; Big5Freq[5][87] = 449; Big5Freq[3][55] = 448; Big5Freq[8][153] = 447; Big5Freq[4][83] = 446; Big5Freq[3][114] = 445; Big5Freq[23][147] = 444; Big5Freq[15][31] = 443; Big5Freq[3][54] = 442; Big5Freq[11][122] = 441; Big5Freq[4][4] = 440; Big5Freq[34][149] = 439; Big5Freq[3][17] = 438; Big5Freq[21][64] = 437; Big5Freq[26][144] = 436; Big5Freq[4][62] = 435; Big5Freq[8][15] = 434; Big5Freq[35][80] = 433; Big5Freq[7][110] = 432; Big5Freq[23][114] = 431; Big5Freq[3][108] = 430; Big5Freq[3][62] = 429; Big5Freq[21][41] = 428; Big5Freq[15][99] = 427; Big5Freq[5][47] = 426; Big5Freq[4][96] = 425; Big5Freq[20][122] = 424; Big5Freq[5][21] = 423; Big5Freq[4][157] = 422; Big5Freq[16][14] = 421; Big5Freq[3][117] = 420; Big5Freq[7][129] = 419; Big5Freq[4][27] = 418; Big5Freq[5][30] = 417; Big5Freq[22][16] = 416; Big5Freq[5][64] = 415; Big5Freq[17][99] = 414; Big5Freq[17][57] = 413; Big5Freq[8][105] = 412; Big5Freq[5][112] = 411; Big5Freq[20][59] = 410; Big5Freq[6][129] = 409; Big5Freq[18][17] = 408; Big5Freq[3][92] = 407; Big5Freq[28][118] = 406; Big5Freq[3][109] = 405; Big5Freq[31][51] = 404; Big5Freq[13][116] = 403; Big5Freq[6][15] = 402; Big5Freq[36][136] = 401; Big5Freq[12][74] = 400; Big5Freq[20][88] = 399; Big5Freq[36][68] = 398; Big5Freq[3][147] = 397; Big5Freq[15][84] = 396; Big5Freq[16][32] = 395; Big5Freq[16][58] = 394; Big5Freq[7][66] = 393; Big5Freq[23][107] = 392; Big5Freq[9][6] = 391; Big5Freq[12][86] = 390; Big5Freq[23][112] = 389; Big5Freq[37][23] = 388; Big5Freq[3][138] = 387; Big5Freq[20][68] = 386; Big5Freq[15][116] = 385; Big5Freq[18][64] = 384; Big5Freq[12][139] = 383; Big5Freq[11][155] = 382; Big5Freq[4][156] = 381; Big5Freq[12][84] = 380; Big5Freq[18][49] = 379; Big5Freq[25][125] = 378; Big5Freq[25][147] = 377; Big5Freq[15][110] = 376; Big5Freq[19][96] = 375; Big5Freq[30][152] = 374; Big5Freq[6][31] = 373; Big5Freq[27][117] = 372; Big5Freq[3][10] = 371; Big5Freq[6][131] = 370; Big5Freq[13][112] = 369; Big5Freq[36][156] = 368; Big5Freq[4][60] = 367; Big5Freq[15][121] = 366; Big5Freq[4][112] = 365; Big5Freq[30][142] = 364; Big5Freq[23][154] = 363; Big5Freq[27][101] = 362; Big5Freq[9][140] = 361; Big5Freq[3][89] = 360; Big5Freq[18][148] = 359; Big5Freq[4][69] = 358; Big5Freq[16][49] = 357; Big5Freq[6][117] = 356; Big5Freq[36][55] = 355; Big5Freq[5][123] = 354; Big5Freq[4][126] = 353; Big5Freq[4][119] = 352; Big5Freq[9][95] = 351; Big5Freq[5][24] = 350; Big5Freq[16][133] = 349; Big5Freq[10][134] = 348; Big5Freq[26][59] = 347; Big5Freq[6][41] = 346; Big5Freq[6][146] = 345; Big5Freq[19][24] = 344; Big5Freq[5][113] = 343; Big5Freq[10][118] = 342; Big5Freq[34][151] = 341; Big5Freq[9][72] = 340; Big5Freq[31][25] = 339; Big5Freq[18][126] = 338; Big5Freq[18][28] = 337; Big5Freq[4][153] = 336; Big5Freq[3][84] = 335; Big5Freq[21][18] = 334; Big5Freq[25][129] = 333; Big5Freq[6][107] = 332; Big5Freq[12][25] = 331; Big5Freq[17][109] = 330; Big5Freq[7][76] = 329; Big5Freq[15][15] = 328; Big5Freq[4][14] = 327; Big5Freq[23][88] = 326; Big5Freq[18][2] = 325; Big5Freq[6][88] = 324; Big5Freq[16][84] = 323; Big5Freq[12][48] = 322; Big5Freq[7][68] = 321; Big5Freq[5][50] = 320; Big5Freq[13][54] = 319; Big5Freq[7][98] = 318; Big5Freq[11][6] = 317; Big5Freq[9][80] = 316; Big5Freq[16][41] = 315; Big5Freq[7][43] = 314; Big5Freq[28][117] = 313; Big5Freq[3][51] = 312; Big5Freq[7][3] = 311; Big5Freq[20][81] = 310; Big5Freq[4][2] = 309; Big5Freq[11][16] = 308; Big5Freq[10][4] = 307; Big5Freq[10][119] = 306; Big5Freq[6][142] = 305; Big5Freq[18][51] = 304; Big5Freq[8][144] = 303; Big5Freq[10][65] = 302; Big5Freq[11][64] = 301; Big5Freq[11][130] = 300; Big5Freq[9][92] = 299; Big5Freq[18][29] = 298; Big5Freq[18][78] = 297; Big5Freq[18][151] = 296; Big5Freq[33][127] = 295; Big5Freq[35][113] = 294; Big5Freq[10][155] = 293; Big5Freq[3][76] = 292; Big5Freq[36][123] = 291; Big5Freq[13][143] = 290; Big5Freq[5][135] = 289; Big5Freq[23][116] = 288; Big5Freq[6][101] = 287; Big5Freq[14][74] = 286; Big5Freq[7][153] = 285; Big5Freq[3][101] = 284; Big5Freq[9][74] = 283; Big5Freq[3][156] = 282; Big5Freq[4][147] = 281; Big5Freq[9][12] = 280; Big5Freq[18][133] = 279; Big5Freq[4][0] = 278; Big5Freq[7][155] = 277; Big5Freq[9][144] = 276; Big5Freq[23][49] = 275; Big5Freq[5][89] = 274; Big5Freq[10][11] = 273; Big5Freq[3][110] = 272; Big5Freq[3][40] = 271; Big5Freq[29][115] = 270; Big5Freq[9][100] = 269; Big5Freq[21][67] = 268; Big5Freq[23][145] = 267; Big5Freq[10][47] = 266; Big5Freq[4][31] = 265; Big5Freq[4][81] = 264; Big5Freq[22][62] = 263; Big5Freq[4][28] = 262; Big5Freq[27][39] = 261; Big5Freq[27][54] = 260; Big5Freq[32][46] = 259; Big5Freq[4][76] = 258; Big5Freq[26][15] = 257; Big5Freq[12][154] = 256; Big5Freq[9][150] = 255; Big5Freq[15][17] = 254; Big5Freq[5][129] = 253; Big5Freq[10][40] = 252; Big5Freq[13][37] = 251; Big5Freq[31][104] = 250; Big5Freq[3][152] = 249; Big5Freq[5][22] = 248; Big5Freq[8][48] = 247; Big5Freq[4][74] = 246; Big5Freq[6][17] = 245; Big5Freq[30][82] = 244; Big5Freq[4][116] = 243; Big5Freq[16][42] = 242; Big5Freq[5][55] = 241; Big5Freq[4][64] = 240; Big5Freq[14][19] = 239; Big5Freq[35][82] = 238; Big5Freq[30][139] = 237; Big5Freq[26][152] = 236; Big5Freq[32][32] = 235; Big5Freq[21][102] = 234; Big5Freq[10][131] = 233; Big5Freq[9][128] = 232; Big5Freq[3][87] = 231; Big5Freq[4][51] = 230; Big5Freq[10][15] = 229; Big5Freq[4][150] = 228; Big5Freq[7][4] = 227; Big5Freq[7][51] = 226; Big5Freq[7][157] = 225; Big5Freq[4][146] = 224; Big5Freq[4][91] = 223; Big5Freq[7][13] = 222; Big5Freq[17][116] = 221; Big5Freq[23][21] = 220; Big5Freq[5][106] = 219; Big5Freq[14][100] = 218; Big5Freq[10][152] = 217; Big5Freq[14][89] = 216; Big5Freq[6][138] = 215; Big5Freq[12][157] = 214; Big5Freq[10][102] = 213; Big5Freq[19][94] = 212; Big5Freq[7][74] = 211; Big5Freq[18][128] = 210; Big5Freq[27][111] = 209; Big5Freq[11][57] = 208; Big5Freq[3][131] = 207; Big5Freq[30][23] = 206; Big5Freq[30][126] = 205; Big5Freq[4][36] = 204; Big5Freq[26][124] = 203; Big5Freq[4][19] = 202; Big5Freq[9][152] = 201; /* * Big5Freq[5][0] = 200; Big5Freq[26][57] = 199; Big5Freq[13][155] = * 198; Big5Freq[3][38] = 197; Big5Freq[9][155] = 196; Big5Freq[28][53] * = 195; Big5Freq[15][71] = 194; Big5Freq[21][95] = 193; * Big5Freq[15][112] = 192; Big5Freq[14][138] = 191; Big5Freq[8][18] = * 190; Big5Freq[20][151] = 189; Big5Freq[37][27] = 188; * Big5Freq[32][48] = 187; Big5Freq[23][66] = 186; Big5Freq[9][2] = 185; * Big5Freq[13][133] = 184; Big5Freq[7][127] = 183; Big5Freq[3][11] = * 182; Big5Freq[12][118] = 181; Big5Freq[13][101] = 180; * Big5Freq[30][153] = 179; Big5Freq[4][65] = 178; Big5Freq[5][25] = * 177; Big5Freq[5][140] = 176; Big5Freq[6][25] = 175; Big5Freq[4][52] = * 174; Big5Freq[30][156] = 173; Big5Freq[16][13] = 172; Big5Freq[21][8] * = 171; Big5Freq[19][74] = 170; Big5Freq[15][145] = 169; * Big5Freq[9][15] = 168; Big5Freq[13][82] = 167; Big5Freq[26][86] = * 166; Big5Freq[18][52] = 165; Big5Freq[6][109] = 164; Big5Freq[10][99] * = 163; Big5Freq[18][101] = 162; Big5Freq[25][49] = 161; * Big5Freq[31][79] = 160; Big5Freq[28][20] = 159; Big5Freq[12][115] = * 158; Big5Freq[15][66] = 157; Big5Freq[11][104] = 156; * Big5Freq[23][106] = 155; Big5Freq[34][157] = 154; Big5Freq[32][94] = * 153; Big5Freq[29][88] = 152; Big5Freq[10][46] = 151; * Big5Freq[13][118] = 150; Big5Freq[20][37] = 149; Big5Freq[12][30] = * 148; Big5Freq[21][4] = 147; Big5Freq[16][33] = 146; Big5Freq[13][52] * = 145; Big5Freq[4][7] = 144; Big5Freq[21][49] = 143; Big5Freq[3][27] * = 142; Big5Freq[16][91] = 141; Big5Freq[5][155] = 140; * Big5Freq[29][130] = 139; Big5Freq[3][125] = 138; Big5Freq[14][26] = * 137; Big5Freq[15][39] = 136; Big5Freq[24][110] = 135; * Big5Freq[7][141] = 134; Big5Freq[21][15] = 133; Big5Freq[32][104] = * 132; Big5Freq[8][31] = 131; Big5Freq[34][112] = 130; Big5Freq[10][75] * = 129; Big5Freq[21][23] = 128; Big5Freq[34][131] = 127; * Big5Freq[12][3] = 126; Big5Freq[10][62] = 125; Big5Freq[9][120] = * 124; Big5Freq[32][149] = 123; Big5Freq[8][44] = 122; Big5Freq[24][2] * = 121; Big5Freq[6][148] = 120; Big5Freq[15][103] = 119; * Big5Freq[36][54] = 118; Big5Freq[36][134] = 117; Big5Freq[11][7] = * 116; Big5Freq[3][90] = 115; Big5Freq[36][73] = 114; Big5Freq[8][102] * = 113; Big5Freq[12][87] = 112; Big5Freq[25][64] = 111; Big5Freq[9][1] * = 110; Big5Freq[24][121] = 109; Big5Freq[5][75] = 108; * Big5Freq[17][83] = 107; Big5Freq[18][57] = 106; Big5Freq[8][95] = * 105; Big5Freq[14][36] = 104; Big5Freq[28][113] = 103; * Big5Freq[12][56] = 102; Big5Freq[14][61] = 101; Big5Freq[25][138] = * 100; Big5Freq[4][34] = 99; Big5Freq[11][152] = 98; Big5Freq[35][0] = * 97; Big5Freq[4][15] = 96; Big5Freq[8][82] = 95; Big5Freq[20][73] = * 94; Big5Freq[25][52] = 93; Big5Freq[24][6] = 92; Big5Freq[21][78] = * 91; Big5Freq[17][32] = 90; Big5Freq[17][91] = 89; Big5Freq[5][76] = * 88; Big5Freq[15][60] = 87; Big5Freq[15][150] = 86; Big5Freq[5][80] = * 85; Big5Freq[15][81] = 84; Big5Freq[28][108] = 83; Big5Freq[18][14] = * 82; Big5Freq[19][109] = 81; Big5Freq[28][133] = 80; Big5Freq[21][97] * = 79; Big5Freq[5][105] = 78; Big5Freq[18][114] = 77; Big5Freq[16][95] * = 76; Big5Freq[5][51] = 75; Big5Freq[3][148] = 74; Big5Freq[22][102] * = 73; Big5Freq[4][123] = 72; Big5Freq[8][88] = 71; Big5Freq[25][111] * = 70; Big5Freq[8][149] = 69; Big5Freq[9][48] = 68; Big5Freq[16][126] * = 67; Big5Freq[33][150] = 66; Big5Freq[9][54] = 65; Big5Freq[29][104] * = 64; Big5Freq[3][3] = 63; Big5Freq[11][49] = 62; Big5Freq[24][109] = * 61; Big5Freq[28][116] = 60; Big5Freq[34][113] = 59; Big5Freq[5][3] = * 58; Big5Freq[21][106] = 57; Big5Freq[4][98] = 56; Big5Freq[12][135] = * 55; Big5Freq[16][101] = 54; Big5Freq[12][147] = 53; Big5Freq[27][55] * = 52; Big5Freq[3][5] = 51; Big5Freq[11][101] = 50; Big5Freq[16][157] * = 49; Big5Freq[22][114] = 48; Big5Freq[18][46] = 47; Big5Freq[4][29] * = 46; Big5Freq[8][103] = 45; Big5Freq[16][151] = 44; Big5Freq[8][29] * = 43; Big5Freq[15][114] = 42; Big5Freq[22][70] = 41; * Big5Freq[13][121] = 40; Big5Freq[7][112] = 39; Big5Freq[20][83] = 38; * Big5Freq[3][36] = 37; Big5Freq[10][103] = 36; Big5Freq[3][96] = 35; * Big5Freq[21][79] = 34; Big5Freq[25][120] = 33; Big5Freq[29][121] = * 32; Big5Freq[23][71] = 31; Big5Freq[21][22] = 30; Big5Freq[18][89] = * 29; Big5Freq[25][104] = 28; Big5Freq[10][124] = 27; Big5Freq[26][4] = * 26; Big5Freq[21][136] = 25; Big5Freq[6][112] = 24; Big5Freq[12][103] * = 23; Big5Freq[17][66] = 22; Big5Freq[13][151] = 21; * Big5Freq[33][152] = 20; Big5Freq[11][148] = 19; Big5Freq[13][57] = * 18; Big5Freq[13][41] = 17; Big5Freq[7][60] = 16; Big5Freq[21][29] = * 15; Big5Freq[9][157] = 14; Big5Freq[24][95] = 13; Big5Freq[15][148] = * 12; Big5Freq[15][122] = 11; Big5Freq[6][125] = 10; Big5Freq[11][25] = * 9; Big5Freq[20][55] = 8; Big5Freq[19][84] = 7; Big5Freq[21][82] = 6; * Big5Freq[24][3] = 5; Big5Freq[13][70] = 4; Big5Freq[6][21] = 3; * Big5Freq[21][86] = 2; Big5Freq[12][23] = 1; Big5Freq[3][85] = 0; * EUC_TWFreq[45][90] = 600; */ Big5PFreq[41][122] = 600; Big5PFreq[35][0] = 599; Big5PFreq[43][15] = 598; Big5PFreq[35][99] = 597; Big5PFreq[35][6] = 596; Big5PFreq[35][8] = 595; Big5PFreq[38][154] = 594; Big5PFreq[37][34] = 593; Big5PFreq[37][115] = 592; Big5PFreq[36][12] = 591; Big5PFreq[18][77] = 590; Big5PFreq[35][100] = 589; Big5PFreq[35][42] = 588; Big5PFreq[120][75] = 587; Big5PFreq[35][23] = 586; Big5PFreq[13][72] = 585; Big5PFreq[0][67] = 584; Big5PFreq[39][172] = 583; Big5PFreq[22][182] = 582; Big5PFreq[15][186] = 581; Big5PFreq[15][165] = 580; Big5PFreq[35][44] = 579; Big5PFreq[40][13] = 578; Big5PFreq[38][1] = 577; Big5PFreq[37][33] = 576; Big5PFreq[36][24] = 575; Big5PFreq[56][4] = 574; Big5PFreq[35][29] = 573; Big5PFreq[9][96] = 572; Big5PFreq[37][62] = 571; Big5PFreq[48][47] = 570; Big5PFreq[51][14] = 569; Big5PFreq[39][122] = 568; Big5PFreq[44][46] = 567; Big5PFreq[35][21] = 566; Big5PFreq[36][8] = 565; Big5PFreq[36][141] = 564; Big5PFreq[3][81] = 563; Big5PFreq[37][155] = 562; Big5PFreq[42][84] = 561; Big5PFreq[36][40] = 560; Big5PFreq[35][103] = 559; Big5PFreq[11][84] = 558; Big5PFreq[45][33] = 557; Big5PFreq[121][79] = 556; Big5PFreq[2][77] = 555; Big5PFreq[36][41] = 554; Big5PFreq[37][47] = 553; Big5PFreq[39][125] = 552; Big5PFreq[37][26] = 551; Big5PFreq[35][48] = 550; Big5PFreq[35][28] = 549; Big5PFreq[35][159] = 548; Big5PFreq[37][40] = 547; Big5PFreq[35][145] = 546; Big5PFreq[37][147] = 545; Big5PFreq[46][160] = 544; Big5PFreq[37][46] = 543; Big5PFreq[50][99] = 542; Big5PFreq[52][13] = 541; Big5PFreq[10][82] = 540; Big5PFreq[35][169] = 539; Big5PFreq[35][31] = 538; Big5PFreq[47][31] = 537; Big5PFreq[18][79] = 536; Big5PFreq[16][113] = 535; Big5PFreq[37][104] = 534; Big5PFreq[39][134] = 533; Big5PFreq[36][53] = 532; Big5PFreq[38][0] = 531; Big5PFreq[4][86] = 530; Big5PFreq[54][17] = 529; Big5PFreq[43][157] = 528; Big5PFreq[35][165] = 527; Big5PFreq[69][147] = 526; Big5PFreq[117][95] = 525; Big5PFreq[35][162] = 524; Big5PFreq[35][17] = 523; Big5PFreq[36][142] = 522; Big5PFreq[36][4] = 521; Big5PFreq[37][166] = 520; Big5PFreq[35][168] = 519; Big5PFreq[35][19] = 518; Big5PFreq[37][48] = 517; Big5PFreq[42][37] = 516; Big5PFreq[40][146] = 515; Big5PFreq[36][123] = 514; Big5PFreq[22][41] = 513; Big5PFreq[20][119] = 512; Big5PFreq[2][74] = 511; Big5PFreq[44][113] = 510; Big5PFreq[35][125] = 509; Big5PFreq[37][16] = 508; Big5PFreq[35][20] = 507; Big5PFreq[35][55] = 506; Big5PFreq[37][145] = 505; Big5PFreq[0][88] = 504; Big5PFreq[3][94] = 503; Big5PFreq[6][65] = 502; Big5PFreq[26][15] = 501; Big5PFreq[41][126] = 500; Big5PFreq[36][129] = 499; Big5PFreq[31][75] = 498; Big5PFreq[19][61] = 497; Big5PFreq[35][128] = 496; Big5PFreq[29][79] = 495; Big5PFreq[36][62] = 494; Big5PFreq[37][189] = 493; Big5PFreq[39][109] = 492; Big5PFreq[39][135] = 491; Big5PFreq[72][15] = 490; Big5PFreq[47][106] = 489; Big5PFreq[54][14] = 488; Big5PFreq[24][52] = 487; Big5PFreq[38][162] = 486; Big5PFreq[41][43] = 485; Big5PFreq[37][121] = 484; Big5PFreq[14][66] = 483; Big5PFreq[37][30] = 482; Big5PFreq[35][7] = 481; Big5PFreq[49][58] = 480; Big5PFreq[43][188] = 479; Big5PFreq[24][66] = 478; Big5PFreq[35][171] = 477; Big5PFreq[40][186] = 476; Big5PFreq[39][164] = 475; Big5PFreq[78][186] = 474; Big5PFreq[8][72] = 473; Big5PFreq[36][190] = 472; Big5PFreq[35][53] = 471; Big5PFreq[35][54] = 470; Big5PFreq[22][159] = 469; Big5PFreq[35][9] = 468; Big5PFreq[41][140] = 467; Big5PFreq[37][22] = 466; Big5PFreq[48][97] = 465; Big5PFreq[50][97] = 464; Big5PFreq[36][127] = 463; Big5PFreq[37][23] = 462; Big5PFreq[40][55] = 461; Big5PFreq[35][43] = 460; Big5PFreq[26][22] = 459; Big5PFreq[35][15] = 458; Big5PFreq[72][179] = 457; Big5PFreq[20][129] = 456; Big5PFreq[52][101] = 455; Big5PFreq[35][12] = 454; Big5PFreq[42][156] = 453; Big5PFreq[15][157] = 452; Big5PFreq[50][140] = 451; Big5PFreq[26][28] = 450; Big5PFreq[54][51] = 449; Big5PFreq[35][112] = 448; Big5PFreq[36][116] = 447; Big5PFreq[42][11] = 446; Big5PFreq[37][172] = 445; Big5PFreq[37][29] = 444; Big5PFreq[44][107] = 443; Big5PFreq[50][17] = 442; Big5PFreq[39][107] = 441; Big5PFreq[19][109] = 440; Big5PFreq[36][60] = 439; Big5PFreq[49][132] = 438; Big5PFreq[26][16] = 437; Big5PFreq[43][155] = 436; Big5PFreq[37][120] = 435; Big5PFreq[15][159] = 434; Big5PFreq[43][6] = 433; Big5PFreq[45][188] = 432; Big5PFreq[35][38] = 431; Big5PFreq[39][143] = 430; Big5PFreq[48][144] = 429; Big5PFreq[37][168] = 428; Big5PFreq[37][1] = 427; Big5PFreq[36][109] = 426; Big5PFreq[46][53] = 425; Big5PFreq[38][54] = 424; Big5PFreq[36][0] = 423; Big5PFreq[72][33] = 422; Big5PFreq[42][8] = 421; Big5PFreq[36][31] = 420; Big5PFreq[35][150] = 419; Big5PFreq[118][93] = 418; Big5PFreq[37][61] = 417; Big5PFreq[0][85] = 416; Big5PFreq[36][27] = 415; Big5PFreq[35][134] = 414; Big5PFreq[36][145] = 413; Big5PFreq[6][96] = 412; Big5PFreq[36][14] = 411; Big5PFreq[16][36] = 410; Big5PFreq[15][175] = 409; Big5PFreq[35][10] = 408; Big5PFreq[36][189] = 407; Big5PFreq[35][51] = 406; Big5PFreq[35][109] = 405; Big5PFreq[35][147] = 404; Big5PFreq[35][180] = 403; Big5PFreq[72][5] = 402; Big5PFreq[36][107] = 401; Big5PFreq[49][116] = 400; Big5PFreq[73][30] = 399; Big5PFreq[6][90] = 398; Big5PFreq[2][70] = 397; Big5PFreq[17][141] = 396; Big5PFreq[35][62] = 395; Big5PFreq[16][180] = 394; Big5PFreq[4][91] = 393; Big5PFreq[15][171] = 392; Big5PFreq[35][177] = 391; Big5PFreq[37][173] = 390; Big5PFreq[16][121] = 389; Big5PFreq[35][5] = 388; Big5PFreq[46][122] = 387; Big5PFreq[40][138] = 386; Big5PFreq[50][49] = 385; Big5PFreq[36][152] = 384; Big5PFreq[13][43] = 383; Big5PFreq[9][88] = 382; Big5PFreq[36][159] = 381; Big5PFreq[27][62] = 380; Big5PFreq[40][18] = 379; Big5PFreq[17][129] = 378; Big5PFreq[43][97] = 377; Big5PFreq[13][131] = 376; Big5PFreq[46][107] = 375; Big5PFreq[60][64] = 374; Big5PFreq[36][179] = 373; Big5PFreq[37][55] = 372; Big5PFreq[41][173] = 371; Big5PFreq[44][172] = 370; Big5PFreq[23][187] = 369; Big5PFreq[36][149] = 368; Big5PFreq[17][125] = 367; Big5PFreq[55][180] = 366; Big5PFreq[51][129] = 365; Big5PFreq[36][51] = 364; Big5PFreq[37][122] = 363; Big5PFreq[48][32] = 362; Big5PFreq[51][99] = 361; Big5PFreq[54][16] = 360; Big5PFreq[41][183] = 359; Big5PFreq[37][179] = 358; Big5PFreq[38][179] = 357; Big5PFreq[35][143] = 356; Big5PFreq[37][24] = 355; Big5PFreq[40][177] = 354; Big5PFreq[47][117] = 353; Big5PFreq[39][52] = 352; Big5PFreq[22][99] = 351; Big5PFreq[40][142] = 350; Big5PFreq[36][49] = 349; Big5PFreq[38][17] = 348; Big5PFreq[39][188] = 347; Big5PFreq[36][186] = 346; Big5PFreq[35][189] = 345; Big5PFreq[41][7] = 344; Big5PFreq[18][91] = 343; Big5PFreq[43][137] = 342; Big5PFreq[35][142] = 341; Big5PFreq[35][117] = 340; Big5PFreq[39][138] = 339; Big5PFreq[16][59] = 338; Big5PFreq[39][174] = 337; Big5PFreq[55][145] = 336; Big5PFreq[37][21] = 335; Big5PFreq[36][180] = 334; Big5PFreq[37][156] = 333; Big5PFreq[49][13] = 332; Big5PFreq[41][107] = 331; Big5PFreq[36][56] = 330; Big5PFreq[53][8] = 329; Big5PFreq[22][114] = 328; Big5PFreq[5][95] = 327; Big5PFreq[37][0] = 326; Big5PFreq[26][183] = 325; Big5PFreq[22][66] = 324; Big5PFreq[35][58] = 323; Big5PFreq[48][117] = 322; Big5PFreq[36][102] = 321; Big5PFreq[22][122] = 320; Big5PFreq[35][11] = 319; Big5PFreq[46][19] = 318; Big5PFreq[22][49] = 317; Big5PFreq[48][166] = 316; Big5PFreq[41][125] = 315; Big5PFreq[41][1] = 314; Big5PFreq[35][178] = 313; Big5PFreq[41][12] = 312; Big5PFreq[26][167] = 311; Big5PFreq[42][152] = 310; Big5PFreq[42][46] = 309; Big5PFreq[42][151] = 308; Big5PFreq[20][135] = 307; Big5PFreq[37][162] = 306; Big5PFreq[37][50] = 305; Big5PFreq[22][185] = 304; Big5PFreq[36][166] = 303; Big5PFreq[19][40] = 302; Big5PFreq[22][107] = 301; Big5PFreq[22][102] = 300; Big5PFreq[57][162] = 299; Big5PFreq[22][124] = 298; Big5PFreq[37][138] = 297; Big5PFreq[37][25] = 296; Big5PFreq[0][69] = 295; Big5PFreq[43][172] = 294; Big5PFreq[42][167] = 293; Big5PFreq[35][120] = 292; Big5PFreq[41][128] = 291; Big5PFreq[2][88] = 290; Big5PFreq[20][123] = 289; Big5PFreq[35][123] = 288; Big5PFreq[36][28] = 287; Big5PFreq[42][188] = 286; Big5PFreq[42][164] = 285; Big5PFreq[42][4] = 284; Big5PFreq[43][57] = 283; Big5PFreq[39][3] = 282; Big5PFreq[42][3] = 281; Big5PFreq[57][158] = 280; Big5PFreq[35][146] = 279; Big5PFreq[24][54] = 278; Big5PFreq[13][110] = 277; Big5PFreq[23][132] = 276; Big5PFreq[26][102] = 275; Big5PFreq[55][178] = 274; Big5PFreq[17][117] = 273; Big5PFreq[41][161] = 272; Big5PFreq[38][150] = 271; Big5PFreq[10][71] = 270; Big5PFreq[47][60] = 269; Big5PFreq[16][114] = 268; Big5PFreq[21][47] = 267; Big5PFreq[39][101] = 266; Big5PFreq[18][45] = 265; Big5PFreq[40][121] = 264; Big5PFreq[45][41] = 263; Big5PFreq[22][167] = 262; Big5PFreq[26][149] = 261; Big5PFreq[15][189] = 260; Big5PFreq[41][177] = 259; Big5PFreq[46][36] = 258; Big5PFreq[20][40] = 257; Big5PFreq[41][54] = 256; Big5PFreq[3][87] = 255; Big5PFreq[40][16] = 254; Big5PFreq[42][15] = 253; Big5PFreq[11][83] = 252; Big5PFreq[0][94] = 251; Big5PFreq[122][81] = 250; Big5PFreq[41][26] = 249; Big5PFreq[36][34] = 248; Big5PFreq[44][148] = 247; Big5PFreq[35][3] = 246; Big5PFreq[36][114] = 245; Big5PFreq[42][112] = 244; Big5PFreq[35][183] = 243; Big5PFreq[49][73] = 242; Big5PFreq[39][2] = 241; Big5PFreq[38][121] = 240; Big5PFreq[44][114] = 239; Big5PFreq[49][32] = 238; Big5PFreq[1][65] = 237; Big5PFreq[38][25] = 236; Big5PFreq[39][4] = 235; Big5PFreq[42][62] = 234; Big5PFreq[35][40] = 233; Big5PFreq[24][2] = 232; Big5PFreq[53][49] = 231; Big5PFreq[41][133] = 230; Big5PFreq[43][134] = 229; Big5PFreq[3][83] = 228; Big5PFreq[38][158] = 227; Big5PFreq[24][17] = 226; Big5PFreq[52][59] = 225; Big5PFreq[38][41] = 224; Big5PFreq[37][127] = 223; Big5PFreq[22][175] = 222; Big5PFreq[44][30] = 221; Big5PFreq[47][178] = 220; Big5PFreq[43][99] = 219; Big5PFreq[19][4] = 218; Big5PFreq[37][97] = 217; Big5PFreq[38][181] = 216; Big5PFreq[45][103] = 215; Big5PFreq[1][86] = 214; Big5PFreq[40][15] = 213; Big5PFreq[22][136] = 212; Big5PFreq[75][165] = 211; Big5PFreq[36][15] = 210; Big5PFreq[46][80] = 209; Big5PFreq[59][55] = 208; Big5PFreq[37][108] = 207; Big5PFreq[21][109] = 206; Big5PFreq[24][165] = 205; Big5PFreq[79][158] = 204; Big5PFreq[44][139] = 203; Big5PFreq[36][124] = 202; Big5PFreq[42][185] = 201; Big5PFreq[39][186] = 200; Big5PFreq[22][128] = 199; Big5PFreq[40][44] = 198; Big5PFreq[41][105] = 197; Big5PFreq[1][70] = 196; Big5PFreq[1][68] = 195; Big5PFreq[53][22] = 194; Big5PFreq[36][54] = 193; Big5PFreq[47][147] = 192; Big5PFreq[35][36] = 191; Big5PFreq[35][185] = 190; Big5PFreq[45][37] = 189; Big5PFreq[43][163] = 188; Big5PFreq[56][115] = 187; Big5PFreq[38][164] = 186; Big5PFreq[35][141] = 185; Big5PFreq[42][132] = 184; Big5PFreq[46][120] = 183; Big5PFreq[69][142] = 182; Big5PFreq[38][175] = 181; Big5PFreq[22][112] = 180; Big5PFreq[38][142] = 179; Big5PFreq[40][37] = 178; Big5PFreq[37][109] = 177; Big5PFreq[40][144] = 176; Big5PFreq[44][117] = 175; Big5PFreq[35][181] = 174; Big5PFreq[26][105] = 173; Big5PFreq[16][48] = 172; Big5PFreq[44][122] = 171; Big5PFreq[12][86] = 170; Big5PFreq[84][53] = 169; Big5PFreq[17][44] = 168; Big5PFreq[59][54] = 167; Big5PFreq[36][98] = 166; Big5PFreq[45][115] = 165; Big5PFreq[73][9] = 164; Big5PFreq[44][123] = 163; Big5PFreq[37][188] = 162; Big5PFreq[51][117] = 161; Big5PFreq[15][156] = 160; Big5PFreq[36][155] = 159; Big5PFreq[44][25] = 158; Big5PFreq[38][12] = 157; Big5PFreq[38][140] = 156; Big5PFreq[23][4] = 155; Big5PFreq[45][149] = 154; Big5PFreq[22][189] = 153; Big5PFreq[38][147] = 152; Big5PFreq[27][5] = 151; Big5PFreq[22][42] = 150; Big5PFreq[3][68] = 149; Big5PFreq[39][51] = 148; Big5PFreq[36][29] = 147; Big5PFreq[20][108] = 146; Big5PFreq[50][57] = 145; Big5PFreq[55][104] = 144; Big5PFreq[22][46] = 143; Big5PFreq[18][164] = 142; Big5PFreq[50][159] = 141; Big5PFreq[85][131] = 140; Big5PFreq[26][79] = 139; Big5PFreq[38][100] = 138; Big5PFreq[53][112] = 137; Big5PFreq[20][190] = 136; Big5PFreq[14][69] = 135; Big5PFreq[23][11] = 134; Big5PFreq[40][114] = 133; Big5PFreq[40][148] = 132; Big5PFreq[53][130] = 131; Big5PFreq[36][2] = 130; Big5PFreq[66][82] = 129; Big5PFreq[45][166] = 128; Big5PFreq[4][88] = 127; Big5PFreq[16][57] = 126; Big5PFreq[22][116] = 125; Big5PFreq[36][108] = 124; Big5PFreq[13][48] = 123; Big5PFreq[54][12] = 122; Big5PFreq[40][136] = 121; Big5PFreq[36][128] = 120; Big5PFreq[23][6] = 119; Big5PFreq[38][125] = 118; Big5PFreq[45][154] = 117; Big5PFreq[51][127] = 116; Big5PFreq[44][163] = 115; Big5PFreq[16][173] = 114; Big5PFreq[43][49] = 113; Big5PFreq[20][112] = 112; Big5PFreq[15][168] = 111; Big5PFreq[35][129] = 110; Big5PFreq[20][45] = 109; Big5PFreq[38][10] = 108; Big5PFreq[57][171] = 107; Big5PFreq[44][190] = 106; Big5PFreq[40][56] = 105; Big5PFreq[36][156] = 104; Big5PFreq[3][88] = 103; Big5PFreq[50][122] = 102; Big5PFreq[36][7] = 101; Big5PFreq[39][43] = 100; Big5PFreq[15][166] = 99; Big5PFreq[42][136] = 98; Big5PFreq[22][131] = 97; Big5PFreq[44][23] = 96; Big5PFreq[54][147] = 95; Big5PFreq[41][32] = 94; Big5PFreq[23][121] = 93; Big5PFreq[39][108] = 92; Big5PFreq[2][78] = 91; Big5PFreq[40][155] = 90; Big5PFreq[55][51] = 89; Big5PFreq[19][34] = 88; Big5PFreq[48][128] = 87; Big5PFreq[48][159] = 86; Big5PFreq[20][70] = 85; Big5PFreq[34][71] = 84; Big5PFreq[16][31] = 83; Big5PFreq[42][157] = 82; Big5PFreq[20][44] = 81; Big5PFreq[11][92] = 80; Big5PFreq[44][180] = 79; Big5PFreq[84][33] = 78; Big5PFreq[16][116] = 77; Big5PFreq[61][163] = 76; Big5PFreq[35][164] = 75; Big5PFreq[36][42] = 74; Big5PFreq[13][40] = 73; Big5PFreq[43][176] = 72; Big5PFreq[2][66] = 71; Big5PFreq[20][133] = 70; Big5PFreq[36][65] = 69; Big5PFreq[38][33] = 68; Big5PFreq[12][91] = 67; Big5PFreq[36][26] = 66; Big5PFreq[15][174] = 65; Big5PFreq[77][32] = 64; Big5PFreq[16][1] = 63; Big5PFreq[25][86] = 62; Big5PFreq[17][13] = 61; Big5PFreq[5][75] = 60; Big5PFreq[36][52] = 59; Big5PFreq[51][164] = 58; Big5PFreq[12][85] = 57; Big5PFreq[39][168] = 56; Big5PFreq[43][16] = 55; Big5PFreq[40][69] = 54; Big5PFreq[26][108] = 53; Big5PFreq[51][56] = 52; Big5PFreq[16][37] = 51; Big5PFreq[40][29] = 50; Big5PFreq[46][171] = 49; Big5PFreq[40][128] = 48; Big5PFreq[72][114] = 47; Big5PFreq[21][103] = 46; Big5PFreq[22][44] = 45; Big5PFreq[40][115] = 44; Big5PFreq[43][7] = 43; Big5PFreq[43][153] = 42; Big5PFreq[17][20] = 41; Big5PFreq[16][49] = 40; Big5PFreq[36][57] = 39; Big5PFreq[18][38] = 38; Big5PFreq[45][184] = 37; Big5PFreq[37][167] = 36; Big5PFreq[26][106] = 35; Big5PFreq[61][121] = 34; Big5PFreq[89][140] = 33; Big5PFreq[46][61] = 32; Big5PFreq[39][163] = 31; Big5PFreq[40][62] = 30; Big5PFreq[38][165] = 29; Big5PFreq[47][37] = 28; Big5PFreq[18][155] = 27; Big5PFreq[20][33] = 26; Big5PFreq[29][90] = 25; Big5PFreq[20][103] = 24; Big5PFreq[37][51] = 23; Big5PFreq[57][0] = 22; Big5PFreq[40][31] = 21; Big5PFreq[45][32] = 20; Big5PFreq[59][23] = 19; Big5PFreq[18][47] = 18; Big5PFreq[45][134] = 17; Big5PFreq[37][59] = 16; Big5PFreq[21][128] = 15; Big5PFreq[36][106] = 14; Big5PFreq[31][39] = 13; Big5PFreq[40][182] = 12; Big5PFreq[52][155] = 11; Big5PFreq[42][166] = 10; Big5PFreq[35][27] = 9; Big5PFreq[38][3] = 8; Big5PFreq[13][44] = 7; Big5PFreq[58][157] = 6; Big5PFreq[47][51] = 5; Big5PFreq[41][37] = 4; Big5PFreq[41][172] = 3; Big5PFreq[51][165] = 2; Big5PFreq[15][161] = 1; Big5PFreq[24][181] = 0; EUC_TWFreq[48][49] = 599; EUC_TWFreq[35][65] = 598; EUC_TWFreq[41][27] = 597; EUC_TWFreq[35][0] = 596; EUC_TWFreq[39][19] = 595; EUC_TWFreq[35][42] = 594; EUC_TWFreq[38][66] = 593; EUC_TWFreq[35][8] = 592; EUC_TWFreq[35][6] = 591; EUC_TWFreq[35][66] = 590; EUC_TWFreq[43][14] = 589; EUC_TWFreq[69][80] = 588; EUC_TWFreq[50][48] = 587; EUC_TWFreq[36][71] = 586; EUC_TWFreq[37][10] = 585; EUC_TWFreq[60][52] = 584; EUC_TWFreq[51][21] = 583; EUC_TWFreq[40][2] = 582; EUC_TWFreq[67][35] = 581; EUC_TWFreq[38][78] = 580; EUC_TWFreq[49][18] = 579; EUC_TWFreq[35][23] = 578; EUC_TWFreq[42][83] = 577; EUC_TWFreq[79][47] = 576; EUC_TWFreq[61][82] = 575; EUC_TWFreq[38][7] = 574; EUC_TWFreq[35][29] = 573; EUC_TWFreq[37][77] = 572; EUC_TWFreq[54][67] = 571; EUC_TWFreq[38][80] = 570; EUC_TWFreq[52][74] = 569; EUC_TWFreq[36][37] = 568; EUC_TWFreq[74][8] = 567; EUC_TWFreq[41][83] = 566; EUC_TWFreq[36][75] = 565; EUC_TWFreq[49][63] = 564; EUC_TWFreq[42][58] = 563; EUC_TWFreq[56][33] = 562; EUC_TWFreq[37][76] = 561; EUC_TWFreq[62][39] = 560; EUC_TWFreq[35][21] = 559; EUC_TWFreq[70][19] = 558; EUC_TWFreq[77][88] = 557; EUC_TWFreq[51][14] = 556; EUC_TWFreq[36][17] = 555; EUC_TWFreq[44][51] = 554; EUC_TWFreq[38][72] = 553; EUC_TWFreq[74][90] = 552; EUC_TWFreq[35][48] = 551; EUC_TWFreq[35][69] = 550; EUC_TWFreq[66][86] = 549; EUC_TWFreq[57][20] = 548; EUC_TWFreq[35][53] = 547; EUC_TWFreq[36][87] = 546; EUC_TWFreq[84][67] = 545; EUC_TWFreq[70][56] = 544; EUC_TWFreq[71][54] = 543; EUC_TWFreq[60][70] = 542; EUC_TWFreq[80][1] = 541; EUC_TWFreq[39][59] = 540; EUC_TWFreq[39][51] = 539; EUC_TWFreq[35][44] = 538; EUC_TWFreq[48][4] = 537; EUC_TWFreq[55][24] = 536; EUC_TWFreq[52][4] = 535; EUC_TWFreq[54][26] = 534; EUC_TWFreq[36][31] = 533; EUC_TWFreq[37][22] = 532; EUC_TWFreq[37][9] = 531; EUC_TWFreq[46][0] = 530; EUC_TWFreq[56][46] = 529; EUC_TWFreq[47][93] = 528; EUC_TWFreq[37][25] = 527; EUC_TWFreq[39][8] = 526; EUC_TWFreq[46][73] = 525; EUC_TWFreq[38][48] = 524; EUC_TWFreq[39][83] = 523; EUC_TWFreq[60][92] = 522; EUC_TWFreq[70][11] = 521; EUC_TWFreq[63][84] = 520; EUC_TWFreq[38][65] = 519; EUC_TWFreq[45][45] = 518; EUC_TWFreq[63][49] = 517; EUC_TWFreq[63][50] = 516; EUC_TWFreq[39][93] = 515; EUC_TWFreq[68][20] = 514; EUC_TWFreq[44][84] = 513; EUC_TWFreq[66][34] = 512; EUC_TWFreq[37][58] = 511; EUC_TWFreq[39][0] = 510; EUC_TWFreq[59][1] = 509; EUC_TWFreq[47][8] = 508; EUC_TWFreq[61][17] = 507; EUC_TWFreq[53][87] = 506; EUC_TWFreq[67][26] = 505; EUC_TWFreq[43][46] = 504; EUC_TWFreq[38][61] = 503; EUC_TWFreq[45][9] = 502; EUC_TWFreq[66][83] = 501; EUC_TWFreq[43][88] = 500; EUC_TWFreq[85][20] = 499; EUC_TWFreq[57][36] = 498; EUC_TWFreq[43][6] = 497; EUC_TWFreq[86][77] = 496; EUC_TWFreq[42][70] = 495; EUC_TWFreq[49][78] = 494; EUC_TWFreq[36][40] = 493; EUC_TWFreq[42][71] = 492; EUC_TWFreq[58][49] = 491; EUC_TWFreq[35][20] = 490; EUC_TWFreq[76][20] = 489; EUC_TWFreq[39][25] = 488; EUC_TWFreq[40][34] = 487; EUC_TWFreq[39][76] = 486; EUC_TWFreq[40][1] = 485; EUC_TWFreq[59][0] = 484; EUC_TWFreq[39][70] = 483; EUC_TWFreq[46][14] = 482; EUC_TWFreq[68][77] = 481; EUC_TWFreq[38][55] = 480; EUC_TWFreq[35][78] = 479; EUC_TWFreq[84][44] = 478; EUC_TWFreq[36][41] = 477; EUC_TWFreq[37][62] = 476; EUC_TWFreq[65][67] = 475; EUC_TWFreq[69][66] = 474; EUC_TWFreq[73][55] = 473; EUC_TWFreq[71][49] = 472; EUC_TWFreq[66][87] = 471; EUC_TWFreq[38][33] = 470; EUC_TWFreq[64][61] = 469; EUC_TWFreq[35][7] = 468; EUC_TWFreq[47][49] = 467; EUC_TWFreq[56][14] = 466; EUC_TWFreq[36][49] = 465; EUC_TWFreq[50][81] = 464; EUC_TWFreq[55][76] = 463; EUC_TWFreq[35][19] = 462; EUC_TWFreq[44][47] = 461; EUC_TWFreq[35][15] = 460; EUC_TWFreq[82][59] = 459; EUC_TWFreq[35][43] = 458; EUC_TWFreq[73][0] = 457; EUC_TWFreq[57][83] = 456; EUC_TWFreq[42][46] = 455; EUC_TWFreq[36][0] = 454; EUC_TWFreq[70][88] = 453; EUC_TWFreq[42][22] = 452; EUC_TWFreq[46][58] = 451; EUC_TWFreq[36][34] = 450; EUC_TWFreq[39][24] = 449; EUC_TWFreq[35][55] = 448; EUC_TWFreq[44][91] = 447; EUC_TWFreq[37][51] = 446; EUC_TWFreq[36][19] = 445; EUC_TWFreq[69][90] = 444; EUC_TWFreq[55][35] = 443; EUC_TWFreq[35][54] = 442; EUC_TWFreq[49][61] = 441; EUC_TWFreq[36][67] = 440; EUC_TWFreq[88][34] = 439; EUC_TWFreq[35][17] = 438; EUC_TWFreq[65][69] = 437; EUC_TWFreq[74][89] = 436; EUC_TWFreq[37][31] = 435; EUC_TWFreq[43][48] = 434; EUC_TWFreq[89][27] = 433; EUC_TWFreq[42][79] = 432; EUC_TWFreq[69][57] = 431; EUC_TWFreq[36][13] = 430; EUC_TWFreq[35][62] = 429; EUC_TWFreq[65][47] = 428; EUC_TWFreq[56][8] = 427; EUC_TWFreq[38][79] = 426; EUC_TWFreq[37][64] = 425; EUC_TWFreq[64][64] = 424; EUC_TWFreq[38][53] = 423; EUC_TWFreq[38][31] = 422; EUC_TWFreq[56][81] = 421; EUC_TWFreq[36][22] = 420; EUC_TWFreq[43][4] = 419; EUC_TWFreq[36][90] = 418; EUC_TWFreq[38][62] = 417; EUC_TWFreq[66][85] = 416; EUC_TWFreq[39][1] = 415; EUC_TWFreq[59][40] = 414; EUC_TWFreq[58][93] = 413; EUC_TWFreq[44][43] = 412; EUC_TWFreq[39][49] = 411; EUC_TWFreq[64][2] = 410; EUC_TWFreq[41][35] = 409; EUC_TWFreq[60][22] = 408; EUC_TWFreq[35][91] = 407; EUC_TWFreq[78][1] = 406; EUC_TWFreq[36][14] = 405; EUC_TWFreq[82][29] = 404; EUC_TWFreq[52][86] = 403; EUC_TWFreq[40][16] = 402; EUC_TWFreq[91][52] = 401; EUC_TWFreq[50][75] = 400; EUC_TWFreq[64][30] = 399; EUC_TWFreq[90][78] = 398; EUC_TWFreq[36][52] = 397; EUC_TWFreq[55][87] = 396; EUC_TWFreq[57][5] = 395; EUC_TWFreq[57][31] = 394; EUC_TWFreq[42][35] = 393; EUC_TWFreq[69][50] = 392; EUC_TWFreq[45][8] = 391; EUC_TWFreq[50][87] = 390; EUC_TWFreq[69][55] = 389; EUC_TWFreq[92][3] = 388; EUC_TWFreq[36][43] = 387; EUC_TWFreq[64][10] = 386; EUC_TWFreq[56][25] = 385; EUC_TWFreq[60][68] = 384; EUC_TWFreq[51][46] = 383; EUC_TWFreq[50][0] = 382; EUC_TWFreq[38][30] = 381; EUC_TWFreq[50][85] = 380; EUC_TWFreq[60][54] = 379; EUC_TWFreq[73][6] = 378; EUC_TWFreq[73][28] = 377; EUC_TWFreq[56][19] = 376; EUC_TWFreq[62][69] = 375; EUC_TWFreq[81][66] = 374; EUC_TWFreq[40][32] = 373; EUC_TWFreq[76][31] = 372; EUC_TWFreq[35][10] = 371; EUC_TWFreq[41][37] = 370; EUC_TWFreq[52][82] = 369; EUC_TWFreq[91][72] = 368; EUC_TWFreq[37][29] = 367; EUC_TWFreq[56][30] = 366; EUC_TWFreq[37][80] = 365; EUC_TWFreq[81][56] = 364; EUC_TWFreq[70][3] = 363; EUC_TWFreq[76][15] = 362; EUC_TWFreq[46][47] = 361; EUC_TWFreq[35][88] = 360; EUC_TWFreq[61][58] = 359; EUC_TWFreq[37][37] = 358; EUC_TWFreq[57][22] = 357; EUC_TWFreq[41][23] = 356; EUC_TWFreq[90][66] = 355; EUC_TWFreq[39][60] = 354; EUC_TWFreq[38][0] = 353; EUC_TWFreq[37][87] = 352; EUC_TWFreq[46][2] = 351; EUC_TWFreq[38][56] = 350; EUC_TWFreq[58][11] = 349; EUC_TWFreq[48][10] = 348; EUC_TWFreq[74][4] = 347; EUC_TWFreq[40][42] = 346; EUC_TWFreq[41][52] = 345; EUC_TWFreq[61][92] = 344; EUC_TWFreq[39][50] = 343; EUC_TWFreq[47][88] = 342; EUC_TWFreq[88][36] = 341; EUC_TWFreq[45][73] = 340; EUC_TWFreq[82][3] = 339; EUC_TWFreq[61][36] = 338; EUC_TWFreq[60][33] = 337; EUC_TWFreq[38][27] = 336; EUC_TWFreq[35][83] = 335; EUC_TWFreq[65][24] = 334; EUC_TWFreq[73][10] = 333; EUC_TWFreq[41][13] = 332; EUC_TWFreq[50][27] = 331; EUC_TWFreq[59][50] = 330; EUC_TWFreq[42][45] = 329; EUC_TWFreq[55][19] = 328; EUC_TWFreq[36][77] = 327; EUC_TWFreq[69][31] = 326; EUC_TWFreq[60][7] = 325; EUC_TWFreq[40][88] = 324; EUC_TWFreq[57][56] = 323; EUC_TWFreq[50][50] = 322; EUC_TWFreq[42][37] = 321; EUC_TWFreq[38][82] = 320; EUC_TWFreq[52][25] = 319; EUC_TWFreq[42][67] = 318; EUC_TWFreq[48][40] = 317; EUC_TWFreq[45][81] = 316; EUC_TWFreq[57][14] = 315; EUC_TWFreq[42][13] = 314; EUC_TWFreq[78][0] = 313; EUC_TWFreq[35][51] = 312; EUC_TWFreq[41][67] = 311; EUC_TWFreq[64][23] = 310; EUC_TWFreq[36][65] = 309; EUC_TWFreq[48][50] = 308; EUC_TWFreq[46][69] = 307; EUC_TWFreq[47][89] = 306; EUC_TWFreq[41][48] = 305; EUC_TWFreq[60][56] = 304; EUC_TWFreq[44][82] = 303; EUC_TWFreq[47][35] = 302; EUC_TWFreq[49][3] = 301; EUC_TWFreq[49][69] = 300; EUC_TWFreq[45][93] = 299; EUC_TWFreq[60][34] = 298; EUC_TWFreq[60][82] = 297; EUC_TWFreq[61][61] = 296; EUC_TWFreq[86][42] = 295; EUC_TWFreq[89][60] = 294; EUC_TWFreq[48][31] = 293; EUC_TWFreq[35][75] = 292; EUC_TWFreq[91][39] = 291; EUC_TWFreq[53][19] = 290; EUC_TWFreq[39][72] = 289; EUC_TWFreq[69][59] = 288; EUC_TWFreq[41][7] = 287; EUC_TWFreq[54][13] = 286; EUC_TWFreq[43][28] = 285; EUC_TWFreq[36][6] = 284; EUC_TWFreq[45][75] = 283; EUC_TWFreq[36][61] = 282; EUC_TWFreq[38][21] = 281; EUC_TWFreq[45][14] = 280; EUC_TWFreq[61][43] = 279; EUC_TWFreq[36][63] = 278; EUC_TWFreq[43][30] = 277; EUC_TWFreq[46][51] = 276; EUC_TWFreq[68][87] = 275; EUC_TWFreq[39][26] = 274; EUC_TWFreq[46][76] = 273; EUC_TWFreq[36][15] = 272; EUC_TWFreq[35][40] = 271; EUC_TWFreq[79][60] = 270; EUC_TWFreq[46][7] = 269; EUC_TWFreq[65][72] = 268; EUC_TWFreq[69][88] = 267; EUC_TWFreq[47][18] = 266; EUC_TWFreq[37][0] = 265; EUC_TWFreq[37][49] = 264; EUC_TWFreq[67][37] = 263; EUC_TWFreq[36][91] = 262; EUC_TWFreq[75][48] = 261; EUC_TWFreq[75][63] = 260; EUC_TWFreq[83][87] = 259; EUC_TWFreq[37][44] = 258; EUC_TWFreq[73][54] = 257; EUC_TWFreq[51][61] = 256; EUC_TWFreq[46][57] = 255; EUC_TWFreq[55][21] = 254; EUC_TWFreq[39][66] = 253; EUC_TWFreq[47][11] = 252; EUC_TWFreq[52][8] = 251; EUC_TWFreq[82][81] = 250; EUC_TWFreq[36][57] = 249; EUC_TWFreq[38][54] = 248; EUC_TWFreq[43][81] = 247; EUC_TWFreq[37][42] = 246; EUC_TWFreq[40][18] = 245; EUC_TWFreq[80][90] = 244; EUC_TWFreq[37][84] = 243; EUC_TWFreq[57][15] = 242; EUC_TWFreq[38][87] = 241; EUC_TWFreq[37][32] = 240; EUC_TWFreq[53][53] = 239; EUC_TWFreq[89][29] = 238; EUC_TWFreq[81][53] = 237; EUC_TWFreq[75][3] = 236; EUC_TWFreq[83][73] = 235; EUC_TWFreq[66][13] = 234; EUC_TWFreq[48][7] = 233; EUC_TWFreq[46][35] = 232; EUC_TWFreq[35][86] = 231; EUC_TWFreq[37][20] = 230; EUC_TWFreq[46][80] = 229; EUC_TWFreq[38][24] = 228; EUC_TWFreq[41][68] = 227; EUC_TWFreq[42][21] = 226; EUC_TWFreq[43][32] = 225; EUC_TWFreq[38][20] = 224; EUC_TWFreq[37][59] = 223; EUC_TWFreq[41][77] = 222; EUC_TWFreq[59][57] = 221; EUC_TWFreq[68][59] = 220; EUC_TWFreq[39][43] = 219; EUC_TWFreq[54][39] = 218; EUC_TWFreq[48][28] = 217; EUC_TWFreq[54][28] = 216; EUC_TWFreq[41][44] = 215; EUC_TWFreq[51][64] = 214; EUC_TWFreq[47][72] = 213; EUC_TWFreq[62][67] = 212; EUC_TWFreq[42][43] = 211; EUC_TWFreq[61][38] = 210; EUC_TWFreq[76][25] = 209; EUC_TWFreq[48][91] = 208; EUC_TWFreq[36][36] = 207; EUC_TWFreq[80][32] = 206; EUC_TWFreq[81][40] = 205; EUC_TWFreq[37][5] = 204; EUC_TWFreq[74][69] = 203; EUC_TWFreq[36][82] = 202; EUC_TWFreq[46][59] = 201; /* * EUC_TWFreq[38][32] = 200; EUC_TWFreq[74][2] = 199; EUC_TWFreq[53][31] * = 198; EUC_TWFreq[35][38] = 197; EUC_TWFreq[46][62] = 196; * EUC_TWFreq[77][31] = 195; EUC_TWFreq[55][74] = 194; EUC_TWFreq[66][6] * = 193; EUC_TWFreq[56][21] = 192; EUC_TWFreq[54][78] = 191; * EUC_TWFreq[43][51] = 190; EUC_TWFreq[64][93] = 189; EUC_TWFreq[92][7] * = 188; EUC_TWFreq[83][89] = 187; EUC_TWFreq[69][9] = 186; * EUC_TWFreq[45][4] = 185; EUC_TWFreq[53][9] = 184; EUC_TWFreq[43][2] = * 183; EUC_TWFreq[35][11] = 182; EUC_TWFreq[51][25] = 181; * EUC_TWFreq[52][71] = 180; EUC_TWFreq[81][67] = 179; * EUC_TWFreq[37][33] = 178; EUC_TWFreq[38][57] = 177; * EUC_TWFreq[39][77] = 176; EUC_TWFreq[40][26] = 175; * EUC_TWFreq[37][21] = 174; EUC_TWFreq[81][70] = 173; * EUC_TWFreq[56][80] = 172; EUC_TWFreq[65][14] = 171; * EUC_TWFreq[62][47] = 170; EUC_TWFreq[56][54] = 169; * EUC_TWFreq[45][17] = 168; EUC_TWFreq[52][52] = 167; * EUC_TWFreq[74][30] = 166; EUC_TWFreq[60][57] = 165; * EUC_TWFreq[41][15] = 164; EUC_TWFreq[47][69] = 163; * EUC_TWFreq[61][11] = 162; EUC_TWFreq[72][25] = 161; * EUC_TWFreq[82][56] = 160; EUC_TWFreq[76][92] = 159; * EUC_TWFreq[51][22] = 158; EUC_TWFreq[55][69] = 157; * EUC_TWFreq[49][43] = 156; EUC_TWFreq[69][49] = 155; * EUC_TWFreq[88][42] = 154; EUC_TWFreq[84][41] = 153; * EUC_TWFreq[79][33] = 152; EUC_TWFreq[47][17] = 151; * EUC_TWFreq[52][88] = 150; EUC_TWFreq[63][74] = 149; * EUC_TWFreq[50][32] = 148; EUC_TWFreq[65][10] = 147; EUC_TWFreq[57][6] * = 146; EUC_TWFreq[52][23] = 145; EUC_TWFreq[36][70] = 144; * EUC_TWFreq[65][55] = 143; EUC_TWFreq[35][27] = 142; * EUC_TWFreq[57][63] = 141; EUC_TWFreq[39][92] = 140; * EUC_TWFreq[79][75] = 139; EUC_TWFreq[36][30] = 138; * EUC_TWFreq[53][60] = 137; EUC_TWFreq[55][43] = 136; * EUC_TWFreq[71][22] = 135; EUC_TWFreq[43][16] = 134; * EUC_TWFreq[65][21] = 133; EUC_TWFreq[84][51] = 132; * EUC_TWFreq[43][64] = 131; EUC_TWFreq[87][91] = 130; * EUC_TWFreq[47][45] = 129; EUC_TWFreq[65][29] = 128; * EUC_TWFreq[88][16] = 127; EUC_TWFreq[50][5] = 126; EUC_TWFreq[47][33] * = 125; EUC_TWFreq[46][27] = 124; EUC_TWFreq[85][2] = 123; * EUC_TWFreq[43][77] = 122; EUC_TWFreq[70][9] = 121; EUC_TWFreq[41][54] * = 120; EUC_TWFreq[56][12] = 119; EUC_TWFreq[90][65] = 118; * EUC_TWFreq[91][50] = 117; EUC_TWFreq[48][41] = 116; * EUC_TWFreq[35][89] = 115; EUC_TWFreq[90][83] = 114; * EUC_TWFreq[44][40] = 113; EUC_TWFreq[50][88] = 112; * EUC_TWFreq[72][39] = 111; EUC_TWFreq[45][3] = 110; EUC_TWFreq[71][33] * = 109; EUC_TWFreq[39][12] = 108; EUC_TWFreq[59][24] = 107; * EUC_TWFreq[60][62] = 106; EUC_TWFreq[44][33] = 105; * EUC_TWFreq[53][70] = 104; EUC_TWFreq[77][90] = 103; * EUC_TWFreq[50][58] = 102; EUC_TWFreq[54][1] = 101; EUC_TWFreq[73][19] * = 100; EUC_TWFreq[37][3] = 99; EUC_TWFreq[49][91] = 98; * EUC_TWFreq[88][43] = 97; EUC_TWFreq[36][78] = 96; EUC_TWFreq[44][20] * = 95; EUC_TWFreq[64][15] = 94; EUC_TWFreq[72][28] = 93; * EUC_TWFreq[70][13] = 92; EUC_TWFreq[65][83] = 91; EUC_TWFreq[58][68] * = 90; EUC_TWFreq[59][32] = 89; EUC_TWFreq[39][13] = 88; * EUC_TWFreq[55][64] = 87; EUC_TWFreq[56][59] = 86; EUC_TWFreq[39][17] * = 85; EUC_TWFreq[55][84] = 84; EUC_TWFreq[77][85] = 83; * EUC_TWFreq[60][19] = 82; EUC_TWFreq[62][82] = 81; EUC_TWFreq[78][16] * = 80; EUC_TWFreq[66][8] = 79; EUC_TWFreq[39][42] = 78; * EUC_TWFreq[61][24] = 77; EUC_TWFreq[57][67] = 76; EUC_TWFreq[38][83] * = 75; EUC_TWFreq[36][53] = 74; EUC_TWFreq[67][76] = 73; * EUC_TWFreq[37][91] = 72; EUC_TWFreq[44][26] = 71; EUC_TWFreq[72][86] * = 70; EUC_TWFreq[44][87] = 69; EUC_TWFreq[45][50] = 68; * EUC_TWFreq[58][4] = 67; EUC_TWFreq[86][65] = 66; EUC_TWFreq[45][56] = * 65; EUC_TWFreq[79][49] = 64; EUC_TWFreq[35][3] = 63; * EUC_TWFreq[48][83] = 62; EUC_TWFreq[71][21] = 61; EUC_TWFreq[77][93] * = 60; EUC_TWFreq[87][92] = 59; EUC_TWFreq[38][35] = 58; * EUC_TWFreq[66][17] = 57; EUC_TWFreq[37][66] = 56; EUC_TWFreq[51][42] * = 55; EUC_TWFreq[57][73] = 54; EUC_TWFreq[51][54] = 53; * EUC_TWFreq[75][64] = 52; EUC_TWFreq[35][5] = 51; EUC_TWFreq[49][40] = * 50; EUC_TWFreq[58][35] = 49; EUC_TWFreq[67][88] = 48; * EUC_TWFreq[60][51] = 47; EUC_TWFreq[36][92] = 46; EUC_TWFreq[44][41] * = 45; EUC_TWFreq[58][29] = 44; EUC_TWFreq[43][62] = 43; * EUC_TWFreq[56][23] = 42; EUC_TWFreq[67][44] = 41; EUC_TWFreq[52][91] * = 40; EUC_TWFreq[42][81] = 39; EUC_TWFreq[64][25] = 38; * EUC_TWFreq[35][36] = 37; EUC_TWFreq[47][73] = 36; EUC_TWFreq[36][1] = * 35; EUC_TWFreq[65][84] = 34; EUC_TWFreq[73][1] = 33; * EUC_TWFreq[79][66] = 32; EUC_TWFreq[69][14] = 31; EUC_TWFreq[65][28] * = 30; EUC_TWFreq[60][93] = 29; EUC_TWFreq[72][79] = 28; * EUC_TWFreq[48][0] = 27; EUC_TWFreq[73][43] = 26; EUC_TWFreq[66][47] = * 25; EUC_TWFreq[41][18] = 24; EUC_TWFreq[51][10] = 23; * EUC_TWFreq[59][7] = 22; EUC_TWFreq[53][27] = 21; EUC_TWFreq[86][67] = * 20; EUC_TWFreq[49][87] = 19; EUC_TWFreq[52][28] = 18; * EUC_TWFreq[52][12] = 17; EUC_TWFreq[42][30] = 16; EUC_TWFreq[65][35] * = 15; EUC_TWFreq[46][64] = 14; EUC_TWFreq[71][7] = 13; * EUC_TWFreq[56][57] = 12; EUC_TWFreq[56][31] = 11; EUC_TWFreq[41][31] * = 10; EUC_TWFreq[48][59] = 9; EUC_TWFreq[63][92] = 8; * EUC_TWFreq[62][57] = 7; EUC_TWFreq[65][87] = 6; EUC_TWFreq[70][10] = * 5; EUC_TWFreq[52][40] = 4; EUC_TWFreq[40][22] = 3; EUC_TWFreq[65][91] * = 2; EUC_TWFreq[50][25] = 1; EUC_TWFreq[35][84] = 0; */ GBKFreq[52][132] = 600; GBKFreq[73][135] = 599; GBKFreq[49][123] = 598; GBKFreq[77][146] = 597; GBKFreq[81][123] = 596; GBKFreq[82][144] = 595; GBKFreq[51][179] = 594; GBKFreq[83][154] = 593; GBKFreq[71][139] = 592; GBKFreq[64][139] = 591; GBKFreq[85][144] = 590; GBKFreq[52][125] = 589; GBKFreq[88][25] = 588; GBKFreq[81][106] = 587; GBKFreq[81][148] = 586; GBKFreq[62][137] = 585; GBKFreq[94][0] = 584; GBKFreq[1][64] = 583; GBKFreq[67][163] = 582; GBKFreq[20][190] = 581; GBKFreq[57][131] = 580; GBKFreq[29][169] = 579; GBKFreq[72][143] = 578; GBKFreq[0][173] = 577; GBKFreq[11][23] = 576; GBKFreq[61][141] = 575; GBKFreq[60][123] = 574; GBKFreq[81][114] = 573; GBKFreq[82][131] = 572; GBKFreq[67][156] = 571; GBKFreq[71][167] = 570; GBKFreq[20][50] = 569; GBKFreq[77][132] = 568; GBKFreq[84][38] = 567; GBKFreq[26][29] = 566; GBKFreq[74][187] = 565; GBKFreq[62][116] = 564; GBKFreq[67][135] = 563; GBKFreq[5][86] = 562; GBKFreq[72][186] = 561; GBKFreq[75][161] = 560; GBKFreq[78][130] = 559; GBKFreq[94][30] = 558; GBKFreq[84][72] = 557; GBKFreq[1][67] = 556; GBKFreq[75][172] = 555; GBKFreq[74][185] = 554; GBKFreq[53][160] = 553; GBKFreq[123][14] = 552; GBKFreq[79][97] = 551; GBKFreq[85][110] = 550; GBKFreq[78][171] = 549; GBKFreq[52][131] = 548; GBKFreq[56][100] = 547; GBKFreq[50][182] = 546; GBKFreq[94][64] = 545; GBKFreq[106][74] = 544; GBKFreq[11][102] = 543; GBKFreq[53][124] = 542; GBKFreq[24][3] = 541; GBKFreq[86][148] = 540; GBKFreq[53][184] = 539; GBKFreq[86][147] = 538; GBKFreq[96][161] = 537; GBKFreq[82][77] = 536; GBKFreq[59][146] = 535; GBKFreq[84][126] = 534; GBKFreq[79][132] = 533; GBKFreq[85][123] = 532; GBKFreq[71][101] = 531; GBKFreq[85][106] = 530; GBKFreq[6][184] = 529; GBKFreq[57][156] = 528; GBKFreq[75][104] = 527; GBKFreq[50][137] = 526; GBKFreq[79][133] = 525; GBKFreq[76][108] = 524; GBKFreq[57][142] = 523; GBKFreq[84][130] = 522; GBKFreq[52][128] = 521; GBKFreq[47][44] = 520; GBKFreq[52][152] = 519; GBKFreq[54][104] = 518; GBKFreq[30][47] = 517; GBKFreq[71][123] = 516; GBKFreq[52][107] = 515; GBKFreq[45][84] = 514; GBKFreq[107][118] = 513; GBKFreq[5][161] = 512; GBKFreq[48][126] = 511; GBKFreq[67][170] = 510; GBKFreq[43][6] = 509; GBKFreq[70][112] = 508; GBKFreq[86][174] = 507; GBKFreq[84][166] = 506; GBKFreq[79][130] = 505; GBKFreq[57][141] = 504; GBKFreq[81][178] = 503; GBKFreq[56][187] = 502; GBKFreq[81][162] = 501; GBKFreq[53][104] = 500; GBKFreq[123][35] = 499; GBKFreq[70][169] = 498; GBKFreq[69][164] = 497; GBKFreq[109][61] = 496; GBKFreq[73][130] = 495; GBKFreq[62][134] = 494; GBKFreq[54][125] = 493; GBKFreq[79][105] = 492; GBKFreq[70][165] = 491; GBKFreq[71][189] = 490; GBKFreq[23][147] = 489; GBKFreq[51][139] = 488; GBKFreq[47][137] = 487; GBKFreq[77][123] = 486; GBKFreq[86][183] = 485; GBKFreq[63][173] = 484; GBKFreq[79][144] = 483; GBKFreq[84][159] = 482; GBKFreq[60][91] = 481; GBKFreq[66][187] = 480; GBKFreq[73][114] = 479; GBKFreq[85][56] = 478; GBKFreq[71][149] = 477; GBKFreq[84][189] = 476; GBKFreq[104][31] = 475; GBKFreq[83][82] = 474; GBKFreq[68][35] = 473; GBKFreq[11][77] = 472; GBKFreq[15][155] = 471; GBKFreq[83][153] = 470; GBKFreq[71][1] = 469; GBKFreq[53][190] = 468; GBKFreq[50][135] = 467; GBKFreq[3][147] = 466; GBKFreq[48][136] = 465; GBKFreq[66][166] = 464; GBKFreq[55][159] = 463; GBKFreq[82][150] = 462; GBKFreq[58][178] = 461; GBKFreq[64][102] = 460; GBKFreq[16][106] = 459; GBKFreq[68][110] = 458; GBKFreq[54][14] = 457; GBKFreq[60][140] = 456; GBKFreq[91][71] = 455; GBKFreq[54][150] = 454; GBKFreq[78][177] = 453; GBKFreq[78][117] = 452; GBKFreq[104][12] = 451; GBKFreq[73][150] = 450; GBKFreq[51][142] = 449; GBKFreq[81][145] = 448; GBKFreq[66][183] = 447; GBKFreq[51][178] = 446; GBKFreq[75][107] = 445; GBKFreq[65][119] = 444; GBKFreq[69][176] = 443; GBKFreq[59][122] = 442; GBKFreq[78][160] = 441; GBKFreq[85][183] = 440; GBKFreq[105][16] = 439; GBKFreq[73][110] = 438; GBKFreq[104][39] = 437; GBKFreq[119][16] = 436; GBKFreq[76][162] = 435; GBKFreq[67][152] = 434; GBKFreq[82][24] = 433; GBKFreq[73][121] = 432; GBKFreq[83][83] = 431; GBKFreq[82][145] = 430; GBKFreq[49][133] = 429; GBKFreq[94][13] = 428; GBKFreq[58][139] = 427; GBKFreq[74][189] = 426; GBKFreq[66][177] = 425; GBKFreq[85][184] = 424; GBKFreq[55][183] = 423; GBKFreq[71][107] = 422; GBKFreq[11][98] = 421; GBKFreq[72][153] = 420; GBKFreq[2][137] = 419; GBKFreq[59][147] = 418; GBKFreq[58][152] = 417; GBKFreq[55][144] = 416; GBKFreq[73][125] = 415; GBKFreq[52][154] = 414; GBKFreq[70][178] = 413; GBKFreq[79][148] = 412; GBKFreq[63][143] = 411; GBKFreq[50][140] = 410; GBKFreq[47][145] = 409; GBKFreq[48][123] = 408; GBKFreq[56][107] = 407; GBKFreq[84][83] = 406; GBKFreq[59][112] = 405; GBKFreq[124][72] = 404; GBKFreq[79][99] = 403; GBKFreq[3][37] = 402; GBKFreq[114][55] = 401; GBKFreq[85][152] = 400; GBKFreq[60][47] = 399; GBKFreq[65][96] = 398; GBKFreq[74][110] = 397; GBKFreq[86][182] = 396; GBKFreq[50][99] = 395; GBKFreq[67][186] = 394; GBKFreq[81][74] = 393; GBKFreq[80][37] = 392; GBKFreq[21][60] = 391; GBKFreq[110][12] = 390; GBKFreq[60][162] = 389; GBKFreq[29][115] = 388; GBKFreq[83][130] = 387; GBKFreq[52][136] = 386; GBKFreq[63][114] = 385; GBKFreq[49][127] = 384; GBKFreq[83][109] = 383; GBKFreq[66][128] = 382; GBKFreq[78][136] = 381; GBKFreq[81][180] = 380; GBKFreq[76][104] = 379; GBKFreq[56][156] = 378; GBKFreq[61][23] = 377; GBKFreq[4][30] = 376; GBKFreq[69][154] = 375; GBKFreq[100][37] = 374; GBKFreq[54][177] = 373; GBKFreq[23][119] = 372; GBKFreq[71][171] = 371; GBKFreq[84][146] = 370; GBKFreq[20][184] = 369; GBKFreq[86][76] = 368; GBKFreq[74][132] = 367; GBKFreq[47][97] = 366; GBKFreq[82][137] = 365; GBKFreq[94][56] = 364; GBKFreq[92][30] = 363; GBKFreq[19][117] = 362; GBKFreq[48][173] = 361; GBKFreq[2][136] = 360; GBKFreq[7][182] = 359; GBKFreq[74][188] = 358; GBKFreq[14][132] = 357; GBKFreq[62][172] = 356; GBKFreq[25][39] = 355; GBKFreq[85][129] = 354; GBKFreq[64][98] = 353; GBKFreq[67][127] = 352; GBKFreq[72][167] = 351; GBKFreq[57][143] = 350; GBKFreq[76][187] = 349; GBKFreq[83][181] = 348; GBKFreq[84][10] = 347; GBKFreq[55][166] = 346; GBKFreq[55][188] = 345; GBKFreq[13][151] = 344; GBKFreq[62][124] = 343; GBKFreq[53][136] = 342; GBKFreq[106][57] = 341; GBKFreq[47][166] = 340; GBKFreq[109][30] = 339; GBKFreq[78][114] = 338; GBKFreq[83][19] = 337; GBKFreq[56][162] = 336; GBKFreq[60][177] = 335; GBKFreq[88][9] = 334; GBKFreq[74][163] = 333; GBKFreq[52][156] = 332; GBKFreq[71][180] = 331; GBKFreq[60][57] = 330; GBKFreq[72][173] = 329; GBKFreq[82][91] = 328; GBKFreq[51][186] = 327; GBKFreq[75][86] = 326; GBKFreq[75][78] = 325; GBKFreq[76][170] = 324; GBKFreq[60][147] = 323; GBKFreq[82][75] = 322; GBKFreq[80][148] = 321; GBKFreq[86][150] = 320; GBKFreq[13][95] = 319; GBKFreq[0][11] = 318; GBKFreq[84][190] = 317; GBKFreq[76][166] = 316; GBKFreq[14][72] = 315; GBKFreq[67][144] = 314; GBKFreq[84][44] = 313; GBKFreq[72][125] = 312; GBKFreq[66][127] = 311; GBKFreq[60][25] = 310; GBKFreq[70][146] = 309; GBKFreq[79][135] = 308; GBKFreq[54][135] = 307; GBKFreq[60][104] = 306; GBKFreq[55][132] = 305; GBKFreq[94][2] = 304; GBKFreq[54][133] = 303; GBKFreq[56][190] = 302; GBKFreq[58][174] = 301; GBKFreq[80][144] = 300; GBKFreq[85][113] = 299; /* * GBKFreq[83][15] = 298; GBKFreq[105][80] = 297; GBKFreq[7][179] = 296; * GBKFreq[93][4] = 295; GBKFreq[123][40] = 294; GBKFreq[85][120] = 293; * GBKFreq[77][165] = 292; GBKFreq[86][67] = 291; GBKFreq[25][162] = * 290; GBKFreq[77][183] = 289; GBKFreq[83][71] = 288; GBKFreq[78][99] = * 287; GBKFreq[72][177] = 286; GBKFreq[71][97] = 285; GBKFreq[58][111] * = 284; GBKFreq[77][175] = 283; GBKFreq[76][181] = 282; * GBKFreq[71][142] = 281; GBKFreq[64][150] = 280; GBKFreq[5][142] = * 279; GBKFreq[73][128] = 278; GBKFreq[73][156] = 277; GBKFreq[60][188] * = 276; GBKFreq[64][56] = 275; GBKFreq[74][128] = 274; * GBKFreq[48][163] = 273; GBKFreq[54][116] = 272; GBKFreq[73][127] = * 271; GBKFreq[16][176] = 270; GBKFreq[62][149] = 269; GBKFreq[105][96] * = 268; GBKFreq[55][186] = 267; GBKFreq[4][51] = 266; GBKFreq[48][113] * = 265; GBKFreq[48][152] = 264; GBKFreq[23][9] = 263; GBKFreq[56][102] * = 262; GBKFreq[11][81] = 261; GBKFreq[82][112] = 260; GBKFreq[65][85] * = 259; GBKFreq[69][125] = 258; GBKFreq[68][31] = 257; GBKFreq[5][20] * = 256; GBKFreq[60][176] = 255; GBKFreq[82][81] = 254; * GBKFreq[72][107] = 253; GBKFreq[3][52] = 252; GBKFreq[71][157] = 251; * GBKFreq[24][46] = 250; GBKFreq[69][108] = 249; GBKFreq[78][178] = * 248; GBKFreq[9][69] = 247; GBKFreq[73][144] = 246; GBKFreq[63][187] = * 245; GBKFreq[68][36] = 244; GBKFreq[47][151] = 243; GBKFreq[14][74] = * 242; GBKFreq[47][114] = 241; GBKFreq[80][171] = 240; GBKFreq[75][152] * = 239; GBKFreq[86][40] = 238; GBKFreq[93][43] = 237; GBKFreq[2][50] = * 236; GBKFreq[62][66] = 235; GBKFreq[1][183] = 234; GBKFreq[74][124] = * 233; GBKFreq[58][104] = 232; GBKFreq[83][106] = 231; GBKFreq[60][144] * = 230; GBKFreq[48][99] = 229; GBKFreq[54][157] = 228; * GBKFreq[70][179] = 227; GBKFreq[61][127] = 226; GBKFreq[57][135] = * 225; GBKFreq[59][190] = 224; GBKFreq[77][116] = 223; GBKFreq[26][17] * = 222; GBKFreq[60][13] = 221; GBKFreq[71][38] = 220; GBKFreq[85][177] * = 219; GBKFreq[59][73] = 218; GBKFreq[50][150] = 217; * GBKFreq[79][102] = 216; GBKFreq[76][118] = 215; GBKFreq[67][132] = * 214; GBKFreq[73][146] = 213; GBKFreq[83][184] = 212; GBKFreq[86][159] * = 211; GBKFreq[95][120] = 210; GBKFreq[23][139] = 209; * GBKFreq[64][183] = 208; GBKFreq[85][103] = 207; GBKFreq[41][90] = * 206; GBKFreq[87][72] = 205; GBKFreq[62][104] = 204; GBKFreq[79][168] * = 203; GBKFreq[79][150] = 202; GBKFreq[104][20] = 201; * GBKFreq[56][114] = 200; GBKFreq[84][26] = 199; GBKFreq[57][99] = 198; * GBKFreq[62][154] = 197; GBKFreq[47][98] = 196; GBKFreq[61][64] = 195; * GBKFreq[112][18] = 194; GBKFreq[123][19] = 193; GBKFreq[4][98] = 192; * GBKFreq[47][163] = 191; GBKFreq[66][188] = 190; GBKFreq[81][85] = * 189; GBKFreq[82][30] = 188; GBKFreq[65][83] = 187; GBKFreq[67][24] = * 186; GBKFreq[68][179] = 185; GBKFreq[55][177] = 184; GBKFreq[2][122] * = 183; GBKFreq[47][139] = 182; GBKFreq[79][158] = 181; * GBKFreq[64][143] = 180; GBKFreq[100][24] = 179; GBKFreq[73][103] = * 178; GBKFreq[50][148] = 177; GBKFreq[86][97] = 176; GBKFreq[59][116] * = 175; GBKFreq[64][173] = 174; GBKFreq[99][91] = 173; GBKFreq[11][99] * = 172; GBKFreq[78][179] = 171; GBKFreq[18][17] = 170; * GBKFreq[58][185] = 169; GBKFreq[47][165] = 168; GBKFreq[67][131] = * 167; GBKFreq[94][40] = 166; GBKFreq[74][153] = 165; GBKFreq[79][142] * = 164; GBKFreq[57][98] = 163; GBKFreq[1][164] = 162; GBKFreq[55][168] * = 161; GBKFreq[13][141] = 160; GBKFreq[51][31] = 159; * GBKFreq[57][178] = 158; GBKFreq[50][189] = 157; GBKFreq[60][167] = * 156; GBKFreq[80][34] = 155; GBKFreq[109][80] = 154; GBKFreq[85][54] = * 153; GBKFreq[69][183] = 152; GBKFreq[67][143] = 151; GBKFreq[47][120] * = 150; GBKFreq[45][75] = 149; GBKFreq[82][98] = 148; GBKFreq[83][22] * = 147; GBKFreq[13][103] = 146; GBKFreq[49][174] = 145; * GBKFreq[57][181] = 144; GBKFreq[64][127] = 143; GBKFreq[61][131] = * 142; GBKFreq[52][180] = 141; GBKFreq[74][134] = 140; GBKFreq[84][187] * = 139; GBKFreq[81][189] = 138; GBKFreq[47][160] = 137; * GBKFreq[66][148] = 136; GBKFreq[7][4] = 135; GBKFreq[85][134] = 134; * GBKFreq[88][13] = 133; GBKFreq[88][80] = 132; GBKFreq[69][166] = 131; * GBKFreq[86][18] = 130; GBKFreq[79][141] = 129; GBKFreq[50][108] = * 128; GBKFreq[94][69] = 127; GBKFreq[81][110] = 126; GBKFreq[69][119] * = 125; GBKFreq[72][161] = 124; GBKFreq[106][45] = 123; * GBKFreq[73][124] = 122; GBKFreq[94][28] = 121; GBKFreq[63][174] = * 120; GBKFreq[3][149] = 119; GBKFreq[24][160] = 118; GBKFreq[113][94] * = 117; GBKFreq[56][138] = 116; GBKFreq[64][185] = 115; * GBKFreq[86][56] = 114; GBKFreq[56][150] = 113; GBKFreq[110][55] = * 112; GBKFreq[28][13] = 111; GBKFreq[54][190] = 110; GBKFreq[8][180] = * 109; GBKFreq[73][149] = 108; GBKFreq[80][155] = 107; GBKFreq[83][172] * = 106; GBKFreq[67][174] = 105; GBKFreq[64][180] = 104; * GBKFreq[84][46] = 103; GBKFreq[91][74] = 102; GBKFreq[69][134] = 101; * GBKFreq[61][107] = 100; GBKFreq[47][171] = 99; GBKFreq[59][51] = 98; * GBKFreq[109][74] = 97; GBKFreq[64][174] = 96; GBKFreq[52][151] = 95; * GBKFreq[51][176] = 94; GBKFreq[80][157] = 93; GBKFreq[94][31] = 92; * GBKFreq[79][155] = 91; GBKFreq[72][174] = 90; GBKFreq[69][113] = 89; * GBKFreq[83][167] = 88; GBKFreq[83][122] = 87; GBKFreq[8][178] = 86; * GBKFreq[70][186] = 85; GBKFreq[59][153] = 84; GBKFreq[84][68] = 83; * GBKFreq[79][39] = 82; GBKFreq[47][180] = 81; GBKFreq[88][53] = 80; * GBKFreq[57][154] = 79; GBKFreq[47][153] = 78; GBKFreq[3][153] = 77; * GBKFreq[76][134] = 76; GBKFreq[51][166] = 75; GBKFreq[58][176] = 74; * GBKFreq[27][138] = 73; GBKFreq[73][126] = 72; GBKFreq[76][185] = 71; * GBKFreq[52][186] = 70; GBKFreq[81][151] = 69; GBKFreq[26][50] = 68; * GBKFreq[76][173] = 67; GBKFreq[106][56] = 66; GBKFreq[85][142] = 65; * GBKFreq[11][103] = 64; GBKFreq[69][159] = 63; GBKFreq[53][142] = 62; * GBKFreq[7][6] = 61; GBKFreq[84][59] = 60; GBKFreq[86][3] = 59; * GBKFreq[64][144] = 58; GBKFreq[1][187] = 57; GBKFreq[82][128] = 56; * GBKFreq[3][66] = 55; GBKFreq[68][133] = 54; GBKFreq[55][167] = 53; * GBKFreq[52][130] = 52; GBKFreq[61][133] = 51; GBKFreq[72][181] = 50; * GBKFreq[25][98] = 49; GBKFreq[84][149] = 48; GBKFreq[91][91] = 47; * GBKFreq[47][188] = 46; GBKFreq[68][130] = 45; GBKFreq[22][44] = 44; * GBKFreq[81][121] = 43; GBKFreq[72][140] = 42; GBKFreq[55][133] = 41; * GBKFreq[55][185] = 40; GBKFreq[56][105] = 39; GBKFreq[60][30] = 38; * GBKFreq[70][103] = 37; GBKFreq[62][141] = 36; GBKFreq[70][144] = 35; * GBKFreq[59][111] = 34; GBKFreq[54][17] = 33; GBKFreq[18][190] = 32; * GBKFreq[65][164] = 31; GBKFreq[83][125] = 30; GBKFreq[61][121] = 29; * GBKFreq[48][13] = 28; GBKFreq[51][189] = 27; GBKFreq[65][68] = 26; * GBKFreq[7][0] = 25; GBKFreq[76][188] = 24; GBKFreq[85][117] = 23; * GBKFreq[45][33] = 22; GBKFreq[78][187] = 21; GBKFreq[106][48] = 20; * GBKFreq[59][52] = 19; GBKFreq[86][185] = 18; GBKFreq[84][121] = 17; * GBKFreq[82][189] = 16; GBKFreq[68][156] = 15; GBKFreq[55][125] = 14; * GBKFreq[65][175] = 13; GBKFreq[7][140] = 12; GBKFreq[50][106] = 11; * GBKFreq[59][124] = 10; GBKFreq[67][115] = 9; GBKFreq[82][114] = 8; * GBKFreq[74][121] = 7; GBKFreq[106][69] = 6; GBKFreq[94][27] = 5; * GBKFreq[78][98] = 4; GBKFreq[85][186] = 3; GBKFreq[108][90] = 2; * GBKFreq[62][160] = 1; GBKFreq[60][169] = 0; */ KRFreq[31][43] = 600; KRFreq[19][56] = 599; KRFreq[38][46] = 598; KRFreq[3][3] = 597; KRFreq[29][77] = 596; KRFreq[19][33] = 595; KRFreq[30][0] = 594; KRFreq[29][89] = 593; KRFreq[31][26] = 592; KRFreq[31][38] = 591; KRFreq[32][85] = 590; KRFreq[15][0] = 589; KRFreq[16][54] = 588; KRFreq[15][76] = 587; KRFreq[31][25] = 586; KRFreq[23][13] = 585; KRFreq[28][34] = 584; KRFreq[18][9] = 583; KRFreq[29][37] = 582; KRFreq[22][45] = 581; KRFreq[19][46] = 580; KRFreq[16][65] = 579; KRFreq[23][5] = 578; KRFreq[26][70] = 577; KRFreq[31][53] = 576; KRFreq[27][12] = 575; KRFreq[30][67] = 574; KRFreq[31][57] = 573; KRFreq[20][20] = 572; KRFreq[30][31] = 571; KRFreq[20][72] = 570; KRFreq[15][51] = 569; KRFreq[3][8] = 568; KRFreq[32][53] = 567; KRFreq[27][85] = 566; KRFreq[25][23] = 565; KRFreq[15][44] = 564; KRFreq[32][3] = 563; KRFreq[31][68] = 562; KRFreq[30][24] = 561; KRFreq[29][49] = 560; KRFreq[27][49] = 559; KRFreq[23][23] = 558; KRFreq[31][91] = 557; KRFreq[31][46] = 556; KRFreq[19][74] = 555; KRFreq[27][27] = 554; KRFreq[3][17] = 553; KRFreq[20][38] = 552; KRFreq[21][82] = 551; KRFreq[28][25] = 550; KRFreq[32][5] = 549; KRFreq[31][23] = 548; KRFreq[25][45] = 547; KRFreq[32][87] = 546; KRFreq[18][26] = 545; KRFreq[24][10] = 544; KRFreq[26][82] = 543; KRFreq[15][89] = 542; KRFreq[28][36] = 541; KRFreq[28][31] = 540; KRFreq[16][23] = 539; KRFreq[16][77] = 538; KRFreq[19][84] = 537; KRFreq[23][72] = 536; KRFreq[38][48] = 535; KRFreq[23][2] = 534; KRFreq[30][20] = 533; KRFreq[38][47] = 532; KRFreq[39][12] = 531; KRFreq[23][21] = 530; KRFreq[18][17] = 529; KRFreq[30][87] = 528; KRFreq[29][62] = 527; KRFreq[29][87] = 526; KRFreq[34][53] = 525; KRFreq[32][29] = 524; KRFreq[35][0] = 523; KRFreq[24][43] = 522; KRFreq[36][44] = 521; KRFreq[20][30] = 520; KRFreq[39][86] = 519; KRFreq[22][14] = 518; KRFreq[29][39] = 517; KRFreq[28][38] = 516; KRFreq[23][79] = 515; KRFreq[24][56] = 514; KRFreq[29][63] = 513; KRFreq[31][45] = 512; KRFreq[23][26] = 511; KRFreq[15][87] = 510; KRFreq[30][74] = 509; KRFreq[24][69] = 508; KRFreq[20][4] = 507; KRFreq[27][50] = 506; KRFreq[30][75] = 505; KRFreq[24][13] = 504; KRFreq[30][8] = 503; KRFreq[31][6] = 502; KRFreq[25][80] = 501; KRFreq[36][8] = 500; KRFreq[15][18] = 499; KRFreq[39][23] = 498; KRFreq[16][24] = 497; KRFreq[31][89] = 496; KRFreq[15][71] = 495; KRFreq[15][57] = 494; KRFreq[30][11] = 493; KRFreq[15][36] = 492; KRFreq[16][60] = 491; KRFreq[24][45] = 490; KRFreq[37][35] = 489; KRFreq[24][87] = 488; KRFreq[20][45] = 487; KRFreq[31][90] = 486; KRFreq[32][21] = 485; KRFreq[19][70] = 484; KRFreq[24][15] = 483; KRFreq[26][92] = 482; KRFreq[37][13] = 481; KRFreq[39][2] = 480; KRFreq[23][70] = 479; KRFreq[27][25] = 478; KRFreq[15][69] = 477; KRFreq[19][61] = 476; KRFreq[31][58] = 475; KRFreq[24][57] = 474; KRFreq[36][74] = 473; KRFreq[21][6] = 472; KRFreq[30][44] = 471; KRFreq[15][91] = 470; KRFreq[27][16] = 469; KRFreq[29][42] = 468; KRFreq[33][86] = 467; KRFreq[29][41] = 466; KRFreq[20][68] = 465; KRFreq[25][47] = 464; KRFreq[22][0] = 463; KRFreq[18][14] = 462; KRFreq[31][28] = 461; KRFreq[15][2] = 460; KRFreq[23][76] = 459; KRFreq[38][32] = 458; KRFreq[29][82] = 457; KRFreq[21][86] = 456; KRFreq[24][62] = 455; KRFreq[31][64] = 454; KRFreq[38][26] = 453; KRFreq[32][86] = 452; KRFreq[22][32] = 451; KRFreq[19][59] = 450; KRFreq[34][18] = 449; KRFreq[18][54] = 448; KRFreq[38][63] = 447; KRFreq[36][23] = 446; KRFreq[35][35] = 445; KRFreq[32][62] = 444; KRFreq[28][35] = 443; KRFreq[27][13] = 442; KRFreq[31][59] = 441; KRFreq[29][29] = 440; KRFreq[15][64] = 439; KRFreq[26][84] = 438; KRFreq[21][90] = 437; KRFreq[20][24] = 436; KRFreq[16][18] = 435; KRFreq[22][23] = 434; KRFreq[31][14] = 433; KRFreq[15][1] = 432; KRFreq[18][63] = 431; KRFreq[19][10] = 430; KRFreq[25][49] = 429; KRFreq[36][57] = 428; KRFreq[20][22] = 427; KRFreq[15][15] = 426; KRFreq[31][51] = 425; KRFreq[24][60] = 424; KRFreq[31][70] = 423; KRFreq[15][7] = 422; KRFreq[28][40] = 421; KRFreq[18][41] = 420; KRFreq[15][38] = 419; KRFreq[32][0] = 418; KRFreq[19][51] = 417; KRFreq[34][62] = 416; KRFreq[16][27] = 415; KRFreq[20][70] = 414; KRFreq[22][33] = 413; KRFreq[26][73] = 412; KRFreq[20][79] = 411; KRFreq[23][6] = 410; KRFreq[24][85] = 409; KRFreq[38][51] = 408; KRFreq[29][88] = 407; KRFreq[38][55] = 406; KRFreq[32][32] = 405; KRFreq[27][18] = 404; KRFreq[23][87] = 403; KRFreq[35][6] = 402; KRFreq[34][27] = 401; KRFreq[39][35] = 400; KRFreq[30][88] = 399; KRFreq[32][92] = 398; KRFreq[32][49] = 397; KRFreq[24][61] = 396; KRFreq[18][74] = 395; KRFreq[23][77] = 394; KRFreq[23][50] = 393; KRFreq[23][32] = 392; KRFreq[23][36] = 391; KRFreq[38][38] = 390; KRFreq[29][86] = 389; KRFreq[36][15] = 388; KRFreq[31][50] = 387; KRFreq[15][86] = 386; KRFreq[39][13] = 385; KRFreq[34][26] = 384; KRFreq[19][34] = 383; KRFreq[16][3] = 382; KRFreq[26][93] = 381; KRFreq[19][67] = 380; KRFreq[24][72] = 379; KRFreq[29][17] = 378; KRFreq[23][24] = 377; KRFreq[25][19] = 376; KRFreq[18][65] = 375; KRFreq[30][78] = 374; KRFreq[27][52] = 373; KRFreq[22][18] = 372; KRFreq[16][38] = 371; KRFreq[21][26] = 370; KRFreq[34][20] = 369; KRFreq[15][42] = 368; KRFreq[16][71] = 367; KRFreq[17][17] = 366; KRFreq[24][71] = 365; KRFreq[18][84] = 364; KRFreq[15][40] = 363; KRFreq[31][62] = 362; KRFreq[15][8] = 361; KRFreq[16][69] = 360; KRFreq[29][79] = 359; KRFreq[38][91] = 358; KRFreq[31][92] = 357; KRFreq[20][77] = 356; KRFreq[3][16] = 355; KRFreq[27][87] = 354; KRFreq[16][25] = 353; KRFreq[36][33] = 352; KRFreq[37][76] = 351; KRFreq[30][12] = 350; KRFreq[26][75] = 349; KRFreq[25][14] = 348; KRFreq[32][26] = 347; KRFreq[23][22] = 346; KRFreq[20][90] = 345; KRFreq[19][8] = 344; KRFreq[38][41] = 343; KRFreq[34][2] = 342; KRFreq[39][4] = 341; KRFreq[27][89] = 340; KRFreq[28][41] = 339; KRFreq[28][44] = 338; KRFreq[24][92] = 337; KRFreq[34][65] = 336; KRFreq[39][14] = 335; KRFreq[21][38] = 334; KRFreq[19][31] = 333; KRFreq[37][39] = 332; KRFreq[33][41] = 331; KRFreq[38][4] = 330; KRFreq[23][80] = 329; KRFreq[25][24] = 328; KRFreq[37][17] = 327; KRFreq[22][16] = 326; KRFreq[22][46] = 325; KRFreq[33][91] = 324; KRFreq[24][89] = 323; KRFreq[30][52] = 322; KRFreq[29][38] = 321; KRFreq[38][85] = 320; KRFreq[15][12] = 319; KRFreq[27][58] = 318; KRFreq[29][52] = 317; KRFreq[37][38] = 316; KRFreq[34][41] = 315; KRFreq[31][65] = 314; KRFreq[29][53] = 313; KRFreq[22][47] = 312; KRFreq[22][19] = 311; KRFreq[26][0] = 310; KRFreq[37][86] = 309; KRFreq[35][4] = 308; KRFreq[36][54] = 307; KRFreq[20][76] = 306; KRFreq[30][9] = 305; KRFreq[30][33] = 304; KRFreq[23][17] = 303; KRFreq[23][33] = 302; KRFreq[38][52] = 301; KRFreq[15][19] = 300; KRFreq[28][45] = 299; KRFreq[29][78] = 298; KRFreq[23][15] = 297; KRFreq[33][5] = 296; KRFreq[17][40] = 295; KRFreq[30][83] = 294; KRFreq[18][1] = 293; KRFreq[30][81] = 292; KRFreq[19][40] = 291; KRFreq[24][47] = 290; KRFreq[17][56] = 289; KRFreq[39][80] = 288; KRFreq[30][46] = 287; KRFreq[16][61] = 286; KRFreq[26][78] = 285; KRFreq[26][57] = 284; KRFreq[20][46] = 283; KRFreq[25][15] = 282; KRFreq[25][91] = 281; KRFreq[21][83] = 280; KRFreq[30][77] = 279; KRFreq[35][30] = 278; KRFreq[30][34] = 277; KRFreq[20][69] = 276; KRFreq[35][10] = 275; KRFreq[29][70] = 274; KRFreq[22][50] = 273; KRFreq[18][0] = 272; KRFreq[22][64] = 271; KRFreq[38][65] = 270; KRFreq[22][70] = 269; KRFreq[24][58] = 268; KRFreq[19][66] = 267; KRFreq[30][59] = 266; KRFreq[37][14] = 265; KRFreq[16][56] = 264; KRFreq[29][85] = 263; KRFreq[31][15] = 262; KRFreq[36][84] = 261; KRFreq[39][15] = 260; KRFreq[39][90] = 259; KRFreq[18][12] = 258; KRFreq[21][93] = 257; KRFreq[24][66] = 256; KRFreq[27][90] = 255; KRFreq[25][90] = 254; KRFreq[22][24] = 253; KRFreq[36][67] = 252; KRFreq[33][90] = 251; KRFreq[15][60] = 250; KRFreq[23][85] = 249; KRFreq[34][1] = 248; KRFreq[39][37] = 247; KRFreq[21][18] = 246; KRFreq[34][4] = 245; KRFreq[28][33] = 244; KRFreq[15][13] = 243; KRFreq[32][22] = 242; KRFreq[30][76] = 241; KRFreq[20][21] = 240; KRFreq[38][66] = 239; KRFreq[32][55] = 238; KRFreq[32][89] = 237; KRFreq[25][26] = 236; KRFreq[16][80] = 235; KRFreq[15][43] = 234; KRFreq[38][54] = 233; KRFreq[39][68] = 232; KRFreq[22][88] = 231; KRFreq[21][84] = 230; KRFreq[21][17] = 229; KRFreq[20][28] = 228; KRFreq[32][1] = 227; KRFreq[33][87] = 226; KRFreq[38][71] = 225; KRFreq[37][47] = 224; KRFreq[18][77] = 223; KRFreq[37][58] = 222; KRFreq[34][74] = 221; KRFreq[32][54] = 220; KRFreq[27][33] = 219; KRFreq[32][93] = 218; KRFreq[23][51] = 217; KRFreq[20][57] = 216; KRFreq[22][37] = 215; KRFreq[39][10] = 214; KRFreq[39][17] = 213; KRFreq[33][4] = 212; KRFreq[32][84] = 211; KRFreq[34][3] = 210; KRFreq[28][27] = 209; KRFreq[15][79] = 208; KRFreq[34][21] = 207; KRFreq[34][69] = 206; KRFreq[21][62] = 205; KRFreq[36][24] = 204; KRFreq[16][89] = 203; KRFreq[18][48] = 202; KRFreq[38][15] = 201; KRFreq[36][58] = 200; KRFreq[21][56] = 199; KRFreq[34][48] = 198; KRFreq[21][15] = 197; KRFreq[39][3] = 196; KRFreq[16][44] = 195; KRFreq[18][79] = 194; KRFreq[25][13] = 193; KRFreq[29][47] = 192; KRFreq[38][88] = 191; KRFreq[20][71] = 190; KRFreq[16][58] = 189; KRFreq[35][57] = 188; KRFreq[29][30] = 187; KRFreq[29][23] = 186; KRFreq[34][93] = 185; KRFreq[30][85] = 184; KRFreq[15][80] = 183; KRFreq[32][78] = 182; KRFreq[37][82] = 181; KRFreq[22][40] = 180; KRFreq[21][69] = 179; KRFreq[26][85] = 178; KRFreq[31][31] = 177; KRFreq[28][64] = 176; KRFreq[38][13] = 175; KRFreq[25][2] = 174; KRFreq[22][34] = 173; KRFreq[28][28] = 172; KRFreq[24][91] = 171; KRFreq[33][74] = 170; KRFreq[29][40] = 169; KRFreq[15][77] = 168; KRFreq[32][80] = 167; KRFreq[30][41] = 166; KRFreq[23][30] = 165; KRFreq[24][63] = 164; KRFreq[30][53] = 163; KRFreq[39][70] = 162; KRFreq[23][61] = 161; KRFreq[37][27] = 160; KRFreq[16][55] = 159; KRFreq[22][74] = 158; KRFreq[26][50] = 157; KRFreq[16][10] = 156; KRFreq[34][63] = 155; KRFreq[35][14] = 154; KRFreq[17][7] = 153; KRFreq[15][59] = 152; KRFreq[27][23] = 151; KRFreq[18][70] = 150; KRFreq[32][56] = 149; KRFreq[37][87] = 148; KRFreq[17][61] = 147; KRFreq[18][83] = 146; KRFreq[23][86] = 145; KRFreq[17][31] = 144; KRFreq[23][83] = 143; KRFreq[35][2] = 142; KRFreq[18][64] = 141; KRFreq[27][43] = 140; KRFreq[32][42] = 139; KRFreq[25][76] = 138; KRFreq[19][85] = 137; KRFreq[37][81] = 136; KRFreq[38][83] = 135; KRFreq[35][7] = 134; KRFreq[16][51] = 133; KRFreq[27][22] = 132; KRFreq[16][76] = 131; KRFreq[22][4] = 130; KRFreq[38][84] = 129; KRFreq[17][83] = 128; KRFreq[24][46] = 127; KRFreq[33][15] = 126; KRFreq[20][48] = 125; KRFreq[17][30] = 124; KRFreq[30][93] = 123; KRFreq[28][11] = 122; KRFreq[28][30] = 121; KRFreq[15][62] = 120; KRFreq[17][87] = 119; KRFreq[32][81] = 118; KRFreq[23][37] = 117; KRFreq[30][22] = 116; KRFreq[32][66] = 115; KRFreq[33][78] = 114; KRFreq[21][4] = 113; KRFreq[31][17] = 112; KRFreq[39][61] = 111; KRFreq[18][76] = 110; KRFreq[15][85] = 109; KRFreq[31][47] = 108; KRFreq[19][57] = 107; KRFreq[23][55] = 106; KRFreq[27][29] = 105; KRFreq[29][46] = 104; KRFreq[33][0] = 103; KRFreq[16][83] = 102; KRFreq[39][78] = 101; KRFreq[32][77] = 100; KRFreq[36][25] = 99; KRFreq[34][19] = 98; KRFreq[38][49] = 97; KRFreq[19][25] = 96; KRFreq[23][53] = 95; KRFreq[28][43] = 94; KRFreq[31][44] = 93; KRFreq[36][34] = 92; KRFreq[16][34] = 91; KRFreq[35][1] = 90; KRFreq[19][87] = 89; KRFreq[18][53] = 88; KRFreq[29][54] = 87; KRFreq[22][41] = 86; KRFreq[38][18] = 85; KRFreq[22][2] = 84; KRFreq[20][3] = 83; KRFreq[39][69] = 82; KRFreq[30][29] = 81; KRFreq[28][19] = 80; KRFreq[29][90] = 79; KRFreq[17][86] = 78; KRFreq[15][9] = 77; KRFreq[39][73] = 76; KRFreq[15][37] = 75; KRFreq[35][40] = 74; KRFreq[33][77] = 73; KRFreq[27][86] = 72; KRFreq[36][79] = 71; KRFreq[23][18] = 70; KRFreq[34][87] = 69; KRFreq[39][24] = 68; KRFreq[26][8] = 67; KRFreq[33][48] = 66; KRFreq[39][30] = 65; KRFreq[33][28] = 64; KRFreq[16][67] = 63; KRFreq[31][78] = 62; KRFreq[32][23] = 61; KRFreq[24][55] = 60; KRFreq[30][68] = 59; KRFreq[18][60] = 58; KRFreq[15][17] = 57; KRFreq[23][34] = 56; KRFreq[20][49] = 55; KRFreq[15][78] = 54; KRFreq[24][14] = 53; KRFreq[19][41] = 52; KRFreq[31][55] = 51; KRFreq[21][39] = 50; KRFreq[35][9] = 49; KRFreq[30][15] = 48; KRFreq[20][52] = 47; KRFreq[35][71] = 46; KRFreq[20][7] = 45; KRFreq[29][72] = 44; KRFreq[37][77] = 43; KRFreq[22][35] = 42; KRFreq[20][61] = 41; KRFreq[31][60] = 40; KRFreq[20][93] = 39; KRFreq[27][92] = 38; KRFreq[28][16] = 37; KRFreq[36][26] = 36; KRFreq[18][89] = 35; KRFreq[21][63] = 34; KRFreq[22][52] = 33; KRFreq[24][65] = 32; KRFreq[31][8] = 31; KRFreq[31][49] = 30; KRFreq[33][30] = 29; KRFreq[37][15] = 28; KRFreq[18][18] = 27; KRFreq[25][50] = 26; KRFreq[29][20] = 25; KRFreq[35][48] = 24; KRFreq[38][75] = 23; KRFreq[26][83] = 22; KRFreq[21][87] = 21; KRFreq[27][71] = 20; KRFreq[32][91] = 19; KRFreq[25][73] = 18; KRFreq[16][84] = 17; KRFreq[25][31] = 16; KRFreq[17][90] = 15; KRFreq[18][40] = 14; KRFreq[17][77] = 13; KRFreq[17][35] = 12; KRFreq[23][52] = 11; KRFreq[23][35] = 10; KRFreq[16][5] = 9; KRFreq[23][58] = 8; KRFreq[19][60] = 7; KRFreq[30][32] = 6; KRFreq[38][34] = 5; KRFreq[23][4] = 4; KRFreq[23][1] = 3; KRFreq[27][57] = 2; KRFreq[39][38] = 1; KRFreq[32][33] = 0; JPFreq[3][74] = 600; JPFreq[3][45] = 599; JPFreq[3][3] = 598; JPFreq[3][24] = 597; JPFreq[3][30] = 596; JPFreq[3][42] = 595; JPFreq[3][46] = 594; JPFreq[3][39] = 593; JPFreq[3][11] = 592; JPFreq[3][37] = 591; JPFreq[3][38] = 590; JPFreq[3][31] = 589; JPFreq[3][41] = 588; JPFreq[3][5] = 587; JPFreq[3][10] = 586; JPFreq[3][75] = 585; JPFreq[3][65] = 584; JPFreq[3][72] = 583; JPFreq[37][91] = 582; JPFreq[0][27] = 581; JPFreq[3][18] = 580; JPFreq[3][22] = 579; JPFreq[3][61] = 578; JPFreq[3][14] = 577; JPFreq[24][80] = 576; JPFreq[4][82] = 575; JPFreq[17][80] = 574; JPFreq[30][44] = 573; JPFreq[3][73] = 572; JPFreq[3][64] = 571; JPFreq[38][14] = 570; JPFreq[33][70] = 569; JPFreq[3][1] = 568; JPFreq[3][16] = 567; JPFreq[3][35] = 566; JPFreq[3][40] = 565; JPFreq[4][74] = 564; JPFreq[4][24] = 563; JPFreq[42][59] = 562; JPFreq[3][7] = 561; JPFreq[3][71] = 560; JPFreq[3][12] = 559; JPFreq[15][75] = 558; JPFreq[3][20] = 557; JPFreq[4][39] = 556; JPFreq[34][69] = 555; JPFreq[3][28] = 554; JPFreq[35][24] = 553; JPFreq[3][82] = 552; JPFreq[28][47] = 551; JPFreq[3][67] = 550; JPFreq[37][16] = 549; JPFreq[26][93] = 548; JPFreq[4][1] = 547; JPFreq[26][85] = 546; JPFreq[31][14] = 545; JPFreq[4][3] = 544; JPFreq[4][72] = 543; JPFreq[24][51] = 542; JPFreq[27][51] = 541; JPFreq[27][49] = 540; JPFreq[22][77] = 539; JPFreq[27][10] = 538; JPFreq[29][68] = 537; JPFreq[20][35] = 536; JPFreq[41][11] = 535; JPFreq[24][70] = 534; JPFreq[36][61] = 533; JPFreq[31][23] = 532; JPFreq[43][16] = 531; JPFreq[23][68] = 530; JPFreq[32][15] = 529; JPFreq[3][32] = 528; JPFreq[19][53] = 527; JPFreq[40][83] = 526; JPFreq[4][14] = 525; JPFreq[36][9] = 524; JPFreq[4][73] = 523; JPFreq[23][10] = 522; JPFreq[3][63] = 521; JPFreq[39][14] = 520; JPFreq[3][78] = 519; JPFreq[33][47] = 518; JPFreq[21][39] = 517; JPFreq[34][46] = 516; JPFreq[36][75] = 515; JPFreq[41][92] = 514; JPFreq[37][93] = 513; JPFreq[4][34] = 512; JPFreq[15][86] = 511; JPFreq[46][1] = 510; JPFreq[37][65] = 509; JPFreq[3][62] = 508; JPFreq[32][73] = 507; JPFreq[21][65] = 506; JPFreq[29][75] = 505; JPFreq[26][51] = 504; JPFreq[3][34] = 503; JPFreq[4][10] = 502; JPFreq[30][22] = 501; JPFreq[35][73] = 500; JPFreq[17][82] = 499; JPFreq[45][8] = 498; JPFreq[27][73] = 497; JPFreq[18][55] = 496; JPFreq[25][2] = 495; JPFreq[3][26] = 494; JPFreq[45][46] = 493; JPFreq[4][22] = 492; JPFreq[4][40] = 491; JPFreq[18][10] = 490; JPFreq[32][9] = 489; JPFreq[26][49] = 488; JPFreq[3][47] = 487; JPFreq[24][65] = 486; JPFreq[4][76] = 485; JPFreq[43][67] = 484; JPFreq[3][9] = 483; JPFreq[41][37] = 482; JPFreq[33][68] = 481; JPFreq[43][31] = 480; JPFreq[19][55] = 479; JPFreq[4][30] = 478; JPFreq[27][33] = 477; JPFreq[16][62] = 476; JPFreq[36][35] = 475; JPFreq[37][15] = 474; JPFreq[27][70] = 473; JPFreq[22][71] = 472; JPFreq[33][45] = 471; JPFreq[31][78] = 470; JPFreq[43][59] = 469; JPFreq[32][19] = 468; JPFreq[17][28] = 467; JPFreq[40][28] = 466; JPFreq[20][93] = 465; JPFreq[18][15] = 464; JPFreq[4][23] = 463; JPFreq[3][23] = 462; JPFreq[26][64] = 461; JPFreq[44][92] = 460; JPFreq[17][27] = 459; JPFreq[3][56] = 458; JPFreq[25][38] = 457; JPFreq[23][31] = 456; JPFreq[35][43] = 455; JPFreq[4][54] = 454; JPFreq[35][19] = 453; JPFreq[22][47] = 452; JPFreq[42][0] = 451; JPFreq[23][28] = 450; JPFreq[46][33] = 449; JPFreq[36][85] = 448; JPFreq[31][12] = 447; JPFreq[3][76] = 446; JPFreq[4][75] = 445; JPFreq[36][56] = 444; JPFreq[4][64] = 443; JPFreq[25][77] = 442; JPFreq[15][52] = 441; JPFreq[33][73] = 440; JPFreq[3][55] = 439; JPFreq[43][82] = 438; JPFreq[27][82] = 437; JPFreq[20][3] = 436; JPFreq[40][51] = 435; JPFreq[3][17] = 434; JPFreq[27][71] = 433; JPFreq[4][52] = 432; JPFreq[44][48] = 431; JPFreq[27][2] = 430; JPFreq[17][39] = 429; JPFreq[31][8] = 428; JPFreq[44][54] = 427; JPFreq[43][18] = 426; JPFreq[43][77] = 425; JPFreq[4][61] = 424; JPFreq[19][91] = 423; JPFreq[31][13] = 422; JPFreq[44][71] = 421; JPFreq[20][0] = 420; JPFreq[23][87] = 419; JPFreq[21][14] = 418; JPFreq[29][13] = 417; JPFreq[3][58] = 416; JPFreq[26][18] = 415; JPFreq[4][47] = 414; JPFreq[4][18] = 413; JPFreq[3][53] = 412; JPFreq[26][92] = 411; JPFreq[21][7] = 410; JPFreq[4][37] = 409; JPFreq[4][63] = 408; JPFreq[36][51] = 407; JPFreq[4][32] = 406; JPFreq[28][73] = 405; JPFreq[4][50] = 404; JPFreq[41][60] = 403; JPFreq[23][1] = 402; JPFreq[36][92] = 401; JPFreq[15][41] = 400; JPFreq[21][71] = 399; JPFreq[41][30] = 398; JPFreq[32][76] = 397; JPFreq[17][34] = 396; JPFreq[26][15] = 395; JPFreq[26][25] = 394; JPFreq[31][77] = 393; JPFreq[31][3] = 392; JPFreq[46][34] = 391; JPFreq[27][84] = 390; JPFreq[23][8] = 389; JPFreq[16][0] = 388; JPFreq[28][80] = 387; JPFreq[26][54] = 386; JPFreq[33][18] = 385; JPFreq[31][20] = 384; JPFreq[31][62] = 383; JPFreq[30][41] = 382; JPFreq[33][30] = 381; JPFreq[45][45] = 380; JPFreq[37][82] = 379; JPFreq[15][33] = 378; JPFreq[20][12] = 377; JPFreq[18][5] = 376; JPFreq[28][86] = 375; JPFreq[30][19] = 374; JPFreq[42][43] = 373; JPFreq[36][31] = 372; JPFreq[17][93] = 371; JPFreq[4][15] = 370; JPFreq[21][20] = 369; JPFreq[23][21] = 368; JPFreq[28][72] = 367; JPFreq[4][20] = 366; JPFreq[26][55] = 365; JPFreq[21][5] = 364; JPFreq[19][16] = 363; JPFreq[23][64] = 362; JPFreq[40][59] = 361; JPFreq[37][26] = 360; JPFreq[26][56] = 359; JPFreq[4][12] = 358; JPFreq[33][71] = 357; JPFreq[32][39] = 356; JPFreq[38][40] = 355; JPFreq[22][74] = 354; JPFreq[3][25] = 353; JPFreq[15][48] = 352; JPFreq[41][82] = 351; JPFreq[41][9] = 350; JPFreq[25][48] = 349; JPFreq[31][71] = 348; JPFreq[43][29] = 347; JPFreq[26][80] = 346; JPFreq[4][5] = 345; JPFreq[18][71] = 344; JPFreq[29][0] = 343; JPFreq[43][43] = 342; JPFreq[23][81] = 341; JPFreq[4][42] = 340; JPFreq[44][28] = 339; JPFreq[23][93] = 338; JPFreq[17][81] = 337; JPFreq[25][25] = 336; JPFreq[41][23] = 335; JPFreq[34][35] = 334; JPFreq[4][53] = 333; JPFreq[28][36] = 332; JPFreq[4][41] = 331; JPFreq[25][60] = 330; JPFreq[23][20] = 329; JPFreq[3][43] = 328; JPFreq[24][79] = 327; JPFreq[29][41] = 326; JPFreq[30][83] = 325; JPFreq[3][50] = 324; JPFreq[22][18] = 323; JPFreq[18][3] = 322; JPFreq[39][30] = 321; JPFreq[4][28] = 320; JPFreq[21][64] = 319; JPFreq[4][68] = 318; JPFreq[17][71] = 317; JPFreq[27][0] = 316; JPFreq[39][28] = 315; JPFreq[30][13] = 314; JPFreq[36][70] = 313; JPFreq[20][82] = 312; JPFreq[33][38] = 311; JPFreq[44][87] = 310; JPFreq[34][45] = 309; JPFreq[4][26] = 308; JPFreq[24][44] = 307; JPFreq[38][67] = 306; JPFreq[38][6] = 305; JPFreq[30][68] = 304; JPFreq[15][89] = 303; JPFreq[24][93] = 302; JPFreq[40][41] = 301; JPFreq[38][3] = 300; JPFreq[28][23] = 299; JPFreq[26][17] = 298; JPFreq[4][38] = 297; JPFreq[22][78] = 296; JPFreq[15][37] = 295; JPFreq[25][85] = 294; JPFreq[4][9] = 293; JPFreq[4][7] = 292; JPFreq[27][53] = 291; JPFreq[39][29] = 290; JPFreq[41][43] = 289; JPFreq[25][62] = 288; JPFreq[4][48] = 287; JPFreq[28][28] = 286; JPFreq[21][40] = 285; JPFreq[36][73] = 284; JPFreq[26][39] = 283; JPFreq[22][54] = 282; JPFreq[33][5] = 281; JPFreq[19][21] = 280; JPFreq[46][31] = 279; JPFreq[20][64] = 278; JPFreq[26][63] = 277; JPFreq[22][23] = 276; JPFreq[25][81] = 275; JPFreq[4][62] = 274; JPFreq[37][31] = 273; JPFreq[40][52] = 272; JPFreq[29][79] = 271; JPFreq[41][48] = 270; JPFreq[31][57] = 269; JPFreq[32][92] = 268; JPFreq[36][36] = 267; JPFreq[27][7] = 266; JPFreq[35][29] = 265; JPFreq[37][34] = 264; JPFreq[34][42] = 263; JPFreq[27][15] = 262; JPFreq[33][27] = 261; JPFreq[31][38] = 260; JPFreq[19][79] = 259; JPFreq[4][31] = 258; JPFreq[4][66] = 257; JPFreq[17][32] = 256; JPFreq[26][67] = 255; JPFreq[16][30] = 254; JPFreq[26][46] = 253; JPFreq[24][26] = 252; JPFreq[35][10] = 251; JPFreq[18][37] = 250; JPFreq[3][19] = 249; JPFreq[33][69] = 248; JPFreq[31][9] = 247; JPFreq[45][29] = 246; JPFreq[3][15] = 245; JPFreq[18][54] = 244; JPFreq[3][44] = 243; JPFreq[31][29] = 242; JPFreq[18][45] = 241; JPFreq[38][28] = 240; JPFreq[24][12] = 239; JPFreq[35][82] = 238; JPFreq[17][43] = 237; JPFreq[28][9] = 236; JPFreq[23][25] = 235; JPFreq[44][37] = 234; JPFreq[23][75] = 233; JPFreq[23][92] = 232; JPFreq[0][24] = 231; JPFreq[19][74] = 230; JPFreq[45][32] = 229; JPFreq[16][72] = 228; JPFreq[16][93] = 227; JPFreq[45][13] = 226; JPFreq[24][8] = 225; JPFreq[25][47] = 224; JPFreq[28][26] = 223; JPFreq[43][81] = 222; JPFreq[32][71] = 221; JPFreq[18][41] = 220; JPFreq[26][62] = 219; JPFreq[41][24] = 218; JPFreq[40][11] = 217; JPFreq[43][57] = 216; JPFreq[34][53] = 215; JPFreq[20][32] = 214; JPFreq[34][43] = 213; JPFreq[41][91] = 212; JPFreq[29][57] = 211; JPFreq[15][43] = 210; JPFreq[22][89] = 209; JPFreq[33][83] = 208; JPFreq[43][20] = 207; JPFreq[25][58] = 206; JPFreq[30][30] = 205; JPFreq[4][56] = 204; JPFreq[17][64] = 203; JPFreq[23][0] = 202; JPFreq[44][12] = 201; JPFreq[25][37] = 200; JPFreq[35][13] = 199; JPFreq[20][30] = 198; JPFreq[21][84] = 197; JPFreq[29][14] = 196; JPFreq[30][5] = 195; JPFreq[37][2] = 194; JPFreq[4][78] = 193; JPFreq[29][78] = 192; JPFreq[29][84] = 191; JPFreq[32][86] = 190; JPFreq[20][68] = 189; JPFreq[30][39] = 188; JPFreq[15][69] = 187; JPFreq[4][60] = 186; JPFreq[20][61] = 185; JPFreq[41][67] = 184; JPFreq[16][35] = 183; JPFreq[36][57] = 182; JPFreq[39][80] = 181; JPFreq[4][59] = 180; JPFreq[4][44] = 179; JPFreq[40][54] = 178; JPFreq[30][8] = 177; JPFreq[44][30] = 176; JPFreq[31][93] = 175; JPFreq[31][47] = 174; JPFreq[16][70] = 173; JPFreq[21][0] = 172; JPFreq[17][35] = 171; JPFreq[21][67] = 170; JPFreq[44][18] = 169; JPFreq[36][29] = 168; JPFreq[18][67] = 167; JPFreq[24][28] = 166; JPFreq[36][24] = 165; JPFreq[23][5] = 164; JPFreq[31][65] = 163; JPFreq[26][59] = 162; JPFreq[28][2] = 161; JPFreq[39][69] = 160; JPFreq[42][40] = 159; JPFreq[37][80] = 158; JPFreq[15][66] = 157; JPFreq[34][38] = 156; JPFreq[28][48] = 155; JPFreq[37][77] = 154; JPFreq[29][34] = 153; JPFreq[33][12] = 152; JPFreq[4][65] = 151; JPFreq[30][31] = 150; JPFreq[27][92] = 149; JPFreq[4][2] = 148; JPFreq[4][51] = 147; JPFreq[23][77] = 146; JPFreq[4][35] = 145; JPFreq[3][13] = 144; JPFreq[26][26] = 143; JPFreq[44][4] = 142; JPFreq[39][53] = 141; JPFreq[20][11] = 140; JPFreq[40][33] = 139; JPFreq[45][7] = 138; JPFreq[4][70] = 137; JPFreq[3][49] = 136; JPFreq[20][59] = 135; JPFreq[21][12] = 134; JPFreq[33][53] = 133; JPFreq[20][14] = 132; JPFreq[37][18] = 131; JPFreq[18][17] = 130; JPFreq[36][23] = 129; JPFreq[18][57] = 128; JPFreq[26][74] = 127; JPFreq[35][2] = 126; JPFreq[38][58] = 125; JPFreq[34][68] = 124; JPFreq[29][81] = 123; JPFreq[20][69] = 122; JPFreq[39][86] = 121; JPFreq[4][16] = 120; JPFreq[16][49] = 119; JPFreq[15][72] = 118; JPFreq[26][35] = 117; JPFreq[32][14] = 116; JPFreq[40][90] = 115; JPFreq[33][79] = 114; JPFreq[35][4] = 113; JPFreq[23][33] = 112; JPFreq[19][19] = 111; JPFreq[31][41] = 110; JPFreq[44][1] = 109; JPFreq[22][56] = 108; JPFreq[31][27] = 107; JPFreq[32][18] = 106; JPFreq[27][32] = 105; JPFreq[37][39] = 104; JPFreq[42][11] = 103; JPFreq[29][71] = 102; JPFreq[32][58] = 101; JPFreq[46][10] = 100; JPFreq[17][30] = 99; JPFreq[38][15] = 98; JPFreq[29][60] = 97; JPFreq[4][11] = 96; JPFreq[38][31] = 95; JPFreq[40][79] = 94; JPFreq[28][49] = 93; JPFreq[28][84] = 92; JPFreq[26][77] = 91; JPFreq[22][32] = 90; JPFreq[33][17] = 89; JPFreq[23][18] = 88; JPFreq[32][64] = 87; JPFreq[4][6] = 86; JPFreq[33][51] = 85; JPFreq[44][77] = 84; JPFreq[29][5] = 83; JPFreq[46][25] = 82; JPFreq[19][58] = 81; JPFreq[4][46] = 80; JPFreq[15][71] = 79; JPFreq[18][58] = 78; JPFreq[26][45] = 77; JPFreq[45][66] = 76; JPFreq[34][10] = 75; JPFreq[19][37] = 74; JPFreq[33][65] = 73; JPFreq[44][52] = 72; JPFreq[16][38] = 71; JPFreq[36][46] = 70; JPFreq[20][26] = 69; JPFreq[30][37] = 68; JPFreq[4][58] = 67; JPFreq[43][2] = 66; JPFreq[30][18] = 65; JPFreq[19][35] = 64; JPFreq[15][68] = 63; JPFreq[3][36] = 62; JPFreq[35][40] = 61; JPFreq[36][32] = 60; JPFreq[37][14] = 59; JPFreq[17][11] = 58; JPFreq[19][78] = 57; JPFreq[37][11] = 56; JPFreq[28][63] = 55; JPFreq[29][61] = 54; JPFreq[33][3] = 53; JPFreq[41][52] = 52; JPFreq[33][63] = 51; JPFreq[22][41] = 50; JPFreq[4][19] = 49; JPFreq[32][41] = 48; JPFreq[24][4] = 47; JPFreq[31][28] = 46; JPFreq[43][30] = 45; JPFreq[17][3] = 44; JPFreq[43][70] = 43; JPFreq[34][19] = 42; JPFreq[20][77] = 41; JPFreq[18][83] = 40; JPFreq[17][15] = 39; JPFreq[23][61] = 38; JPFreq[40][27] = 37; JPFreq[16][48] = 36; JPFreq[39][78] = 35; JPFreq[41][53] = 34; JPFreq[40][91] = 33; JPFreq[40][72] = 32; JPFreq[18][52] = 31; JPFreq[35][66] = 30; JPFreq[39][93] = 29; JPFreq[19][48] = 28; JPFreq[26][36] = 27; JPFreq[27][25] = 26; JPFreq[42][71] = 25; JPFreq[42][85] = 24; JPFreq[26][48] = 23; JPFreq[28][15] = 22; JPFreq[3][66] = 21; JPFreq[25][24] = 20; JPFreq[27][43] = 19; JPFreq[27][78] = 18; JPFreq[45][43] = 17; JPFreq[27][72] = 16; JPFreq[40][29] = 15; JPFreq[41][0] = 14; JPFreq[19][57] = 13; JPFreq[15][59] = 12; JPFreq[29][29] = 11; JPFreq[4][25] = 10; JPFreq[21][42] = 9; JPFreq[23][35] = 8; JPFreq[33][1] = 7; JPFreq[4][57] = 6; JPFreq[17][60] = 5; JPFreq[25][19] = 4; JPFreq[22][65] = 3; JPFreq[42][29] = 2; JPFreq[27][66] = 1; JPFreq[26][89] = 0; } } class Encoding { // Supported Encoding Types public static int GB2312 = 0; public static int GBK = 1; public static int GB18030 = 2; public static int HZ = 3; public static int BIG5 = 4; public static int CNS11643 = 5; public static int UTF8 = 6; public static int UTF8T = 7; public static int UTF8S = 8; public static int UNICODE = 9; public static int UNICODET = 10; public static int UNICODES = 11; public static int ISO2022CN = 12; public static int ISO2022CN_CNS = 13; public static int ISO2022CN_GB = 14; public static int EUC_KR = 15; public static int CP949 = 16; public static int ISO2022KR = 17; public static int JOHAB = 18; public static int SJIS = 19; public static int EUC_JP = 20; public static int ISO2022JP = 21; public static int ASCII = 22; public static int OTHER = 23; public static int TOTALTYPES = 24; public final static int SIMP = 0; public final static int TRAD = 1; // Names of the encodings as understood by Java public static String[] javaname; // Names of the encodings for human viewing public static String[] nicename; // Names of charsets as used in charset parameter of HTML Meta tag public static String[] htmlname; // Constructor public Encoding() { javaname = new String[TOTALTYPES]; nicename = new String[TOTALTYPES]; htmlname = new String[TOTALTYPES]; // Assign encoding names javaname[GB2312] = "GB2312"; javaname[GBK] = "GBK"; javaname[GB18030] = "GB18030"; javaname[HZ] = "ASCII"; // What to put here? Sun doesn't support HZ javaname[ISO2022CN_GB] = "ISO2022CN_GB"; javaname[BIG5] = "BIG5"; javaname[CNS11643] = "EUC-TW"; javaname[ISO2022CN_CNS] = "ISO2022CN_CNS"; javaname[ISO2022CN] = "ISO2022CN"; javaname[UTF8] = "UTF-8"; javaname[UTF8T] = "UTF-8"; javaname[UTF8S] = "UTF-8"; javaname[UNICODE] = "Unicode"; javaname[UNICODET] = "Unicode"; javaname[UNICODES] = "Unicode"; javaname[EUC_KR] = "EUC_KR"; javaname[CP949] = "MS949"; javaname[ISO2022KR] = "ISO2022KR"; javaname[JOHAB] = "Johab"; javaname[SJIS] = "SJIS"; javaname[EUC_JP] = "EUC_JP"; javaname[ISO2022JP] = "ISO2022JP"; javaname[ASCII] = "ASCII"; javaname[OTHER] = "ISO8859_1"; // Assign encoding names htmlname[GB2312] = "GB2312"; htmlname[GBK] = "GBK"; htmlname[GB18030] = "GB18030"; htmlname[HZ] = "HZ-GB-2312"; htmlname[ISO2022CN_GB] = "ISO-2022-CN-EXT"; htmlname[BIG5] = "BIG5"; htmlname[CNS11643] = "EUC-TW"; htmlname[ISO2022CN_CNS] = "ISO-2022-CN-EXT"; htmlname[ISO2022CN] = "ISO-2022-CN"; htmlname[UTF8] = "UTF-8"; htmlname[UTF8T] = "UTF-8"; htmlname[UTF8S] = "UTF-8"; htmlname[UNICODE] = "UTF-16"; htmlname[UNICODET] = "UTF-16"; htmlname[UNICODES] = "UTF-16"; htmlname[EUC_KR] = "EUC-KR"; htmlname[CP949] = "x-windows-949"; htmlname[ISO2022KR] = "ISO-2022-KR"; htmlname[JOHAB] = "x-Johab"; htmlname[SJIS] = "Shift_JIS"; htmlname[EUC_JP] = "EUC-JP"; htmlname[ISO2022JP] = "ISO-2022-JP"; htmlname[ASCII] = "ASCII"; htmlname[OTHER] = "ISO8859-1"; // Assign Human readable names nicename[GB2312] = "GB-2312"; nicename[GBK] = "GBK"; nicename[GB18030] = "GB18030"; nicename[HZ] = "HZ"; nicename[ISO2022CN_GB] = "ISO2022CN-GB"; nicename[BIG5] = "Big5"; nicename[CNS11643] = "CNS11643"; nicename[ISO2022CN_CNS] = "ISO2022CN-CNS"; nicename[ISO2022CN] = "ISO2022 CN"; nicename[UTF8] = "UTF-8"; nicename[UTF8T] = "UTF-8 (Trad)"; nicename[UTF8S] = "UTF-8 (Simp)"; nicename[UNICODE] = "Unicode"; nicename[UNICODET] = "Unicode (Trad)"; nicename[UNICODES] = "Unicode (Simp)"; nicename[EUC_KR] = "EUC-KR"; nicename[CP949] = "CP949"; nicename[ISO2022KR] = "ISO 2022 KR"; nicename[JOHAB] = "Johab"; nicename[SJIS] = "Shift-JIS"; nicename[EUC_JP] = "EUC-JP"; nicename[ISO2022JP] = "ISO 2022 JP"; nicename[ASCII] = "ASCII"; nicename[OTHER] = "OTHER"; } } ================================================ FILE: src/main/java/io/legado/app/help/JsExtensions.kt ================================================ package io.legado.app.help import cn.hutool.crypto.digest.DigestUtil import cn.hutool.crypto.symmetric.AES import cn.hutool.crypto.symmetric.DESede import io.legado.app.utils.Base64 import io.legado.app.constant.AppConst.dateFormat import io.legado.app.help.http.* import io.legado.app.model.Debug import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.QueryTTF import io.legado.app.utils.* import io.legado.app.data.entities.BaseSource import io.legado.app.exception.NoStackTraceException import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking import org.jsoup.Connection import org.jsoup.Jsoup import com.htmake.reader.init.appCtx import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.File import java.net.URLEncoder import java.nio.charset.Charset import java.util.* import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.text.SimpleDateFormat /** * js扩展类, 在js中通过java变量调用 * 所有对于文件的读写删操作都是相对路径,只能操作阅读缓存内的文件 * /android/data/{package}/cache/... */ @Suppress("unused") interface JsExtensions { fun getSource(): BaseSource? /** * 访问网络,返回String */ fun ajax(urlStr: String): String? { return runBlocking { kotlin.runCatching { val analyzeUrl = AnalyzeUrl(urlStr, source = getSource()) analyzeUrl.getStrResponse(urlStr).body }.onFailure { it.printOnDebug() }.getOrElse { it.msg } } } /** * 并发访问网络 */ fun ajaxAll(urlList: Array): Array { return runBlocking { val asyncArray = Array(urlList.size) { async(IO) { val url = urlList[it] val analyzeUrl = AnalyzeUrl(url, source = getSource()) analyzeUrl.getStrResponse(url) } } val resArray = Array(urlList.size) { asyncArray[it].await() } resArray } } /** * 访问网络,返回Response */ fun connect(urlStr: String): StrResponse { return runBlocking { val analyzeUrl = AnalyzeUrl(urlStr, source = getSource()) kotlin.runCatching { analyzeUrl.getStrResponseAwait() }.onFailure { it.printOnDebug() }.getOrElse { StrResponse(analyzeUrl.url, it.localizedMessage) } } } fun connect(urlStr: String, header: String?): StrResponse { return runBlocking { val headerMap = GSON.fromJsonObject>(header).getOrNull() val analyzeUrl = AnalyzeUrl(urlStr, headerMapF = headerMap, source = getSource()) kotlin.runCatching { analyzeUrl.getStrResponseAwait() }.onFailure { it.printOnDebug() }.getOrElse { StrResponse(analyzeUrl.url, it.localizedMessage) } } } /** * 使用webView访问网络 * @param html 直接用webView载入的html, 如果html为空直接访问url * @param url html内如果有相对路径的资源不传入url访问不了 * @param js 用来取返回值的js语句, 没有就返回整个源代码 * @return 返回js获取的内容 */ fun webView(html: String?, url: String?, js: String?): String? { return null } /** * 可从网络,本地文件(阅读私有缓存目录和书籍保存位置支持相对路径)导入JavaScript脚本 */ fun importScript(path: String): String { val result = when { path.startsWith("http") -> cacheFile(path) ?: "" path.startsWith("/storage") -> FileUtils.readText(path) else -> readTxtFile(path) } if (result.isBlank()) throw NoStackTraceException("$path 内容获取失败或者为空") return result } /** * 缓存以文本方式保存的文件 如.js .txt等 */ fun cacheFile(urlStr: String): String? { return cacheFile(urlStr, 0) } /** * 缓存以文本方式保存的文件 如.js .txt等 * @param urlStr 网络文件的链接 * @param saveTime 缓存时间,单位:秒 * @return 返回缓存后的文件内容 */ fun cacheFile(urlStr: String, saveTime: Int = 0): String? { val key = md5Encode16(urlStr) val cache = CacheManager.getFile(key) if (cache.isNullOrBlank()) { log("首次下载 $urlStr") val value = ajax(urlStr) ?: return null CacheManager.putFile(key, value, saveTime) return value } return cache } /** *js实现读取cookie */ fun getCookie(tag: String, key: String? = null): String { val cookie = CookieStore.getCookie(tag) val cookieMap = CookieStore.cookieToMap(cookie) return if (key != null) { cookieMap[key] ?: "" } else { cookie } } /** * 实现16进制字符串转文件 * @param content 需要转成文件的16进制字符串 * @param url 通过url里的参数来判断文件类型 * @return 相对路径 */ fun downloadFile(content: String, url: String): String { val type = AnalyzeUrl(url).type ?: return "" val zipPath = FileUtils.getPath( FileUtils.createFolderIfNotExist(FileUtils.getCachePath()), "${MD5Utils.md5Encode16(url)}.${type}" ) FileUtils.deleteFile(zipPath) val zipFile = FileUtils.createFileIfNotExist(zipPath) StringUtils.hexStringToByte(content).let { if (it.isNotEmpty()) { zipFile.writeBytes(it) } } return zipPath.substring(FileUtils.getCachePath().length) } /** * js实现重定向拦截,网络访问get */ fun get(urlStr: String, headers: Map): Connection.Response { return Jsoup.connect(urlStr) .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory) .ignoreContentType(true) .followRedirects(false) .headers(headers) .method(Connection.Method.GET) .execute() } /** * 网络访问post */ fun post(urlStr: String, body: String, headers: Map): Connection.Response { return Jsoup.connect(urlStr) .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory) .ignoreContentType(true) .followRedirects(false) .requestBody(body) .headers(headers) .method(Connection.Method.POST) .execute() } /** * js实现解码,不能删 */ fun base64Decode(str: String): String { return EncoderUtils.base64Decode(str, Base64.NO_WRAP) } fun base64Decode(str: String, flags: Int): String { return EncoderUtils.base64Decode(str, flags) } fun base64DecodeToByteArray(str: String?): ByteArray? { if (str.isNullOrBlank()) { return null } return Base64.decode(str, Base64.DEFAULT) } fun base64DecodeToByteArray(str: String?, flags: Int): ByteArray? { if (str.isNullOrBlank()) { return null } return Base64.decode(str, flags) } fun base64Encode(str: String): String? { return EncoderUtils.base64Encode(str, Base64.NO_WRAP) } fun base64Encode(str: String, flags: Int): String? { return EncoderUtils.base64Encode(str, flags) } fun md5Encode(str: String): String { return MD5Utils.md5Encode(str) } fun md5Encode16(str: String): String { return MD5Utils.md5Encode16(str) } /** * 格式化时间 */ fun timeFormatUTC(time: Long, format: String, sh: Int): String? { val utc = SimpleTimeZone(sh, "UTC") return SimpleDateFormat(format, Locale.getDefault()).run { timeZone = utc format(Date(time)) } } /** * 时间格式化 */ fun timeFormat(time: Long): String { return dateFormat.format(Date(time)) } /** * utf8编码转gbk编码 */ fun utf8ToGbk(str: String): String { val utf8 = String(str.toByteArray(charset("UTF-8"))) val unicode = String(utf8.toByteArray(), charset("UTF-8")) return String(unicode.toByteArray(charset("GBK"))) } fun encodeURI(str: String): String { return try { URLEncoder.encode(str, "UTF-8") } catch (e: Exception) { "" } } fun encodeURI(str: String, enc: String): String { return try { URLEncoder.encode(str, enc) } catch (e: Exception) { "" } } fun htmlFormat(str: String): String { return HtmlFormatter.formatKeepImg(str) } //****************文件操作******************// /** * 获取本地文件 * @param path 相对路径 * @return File */ fun getFile(path: String): File { val cachePath = appCtx.cacheDir val aPath: String = if (path.startsWith(File.separator)) { cachePath + path } else { cachePath + File.separator + path } return File(aPath) } fun readFile(path: String): ByteArray? { val file = getFile(path) if (file.exists()) { return file.readBytes() } return null } fun readTxtFile(path: String): String { val file = getFile(path) if (file.exists()) { val charsetName = EncodingDetect.getEncode(file) return String(file.readBytes(), charset(charsetName)) } return "" } fun readTxtFile(path: String, charsetName: String): String { val file = getFile(path) if (file.exists()) { return String(file.readBytes(), charset(charsetName)) } return "" } /** * 删除本地文件 */ fun deleteFile(path: String) { val file = getFile(path) FileUtils.delete(file, true) } /** * js实现压缩文件解压 * @param zipPath 相对路径 * @return 相对路径 */ fun unzipFile(zipPath: String): String { if (zipPath.isEmpty()) return "" val unzipPath = FileUtils.getPath( FileUtils.createFolderIfNotExist(FileUtils.getCachePath()), FileUtils.getNameExcludeExtension(zipPath) ) FileUtils.deleteFile(unzipPath) val zipFile = getFile(zipPath) val unzipFolder = FileUtils.createFolderIfNotExist(unzipPath) ZipUtils.unzipFile(zipFile, unzipFolder) FileUtils.deleteFile(zipFile.absolutePath) return unzipPath.substring(FileUtils.getCachePath().length) } /** * js实现文件夹内所有文件读取 */ fun getTxtInFolder(unzipPath: String): String { if (unzipPath.isEmpty()) return "" val unzipFolder = getFile(unzipPath) val contents = StringBuilder() unzipFolder.listFiles().let { if (it != null) { for (f in it) { val charsetName = EncodingDetect.getEncode(f) contents.append(String(f.readBytes(), charset(charsetName))) .append("\n") } contents.deleteCharAt(contents.length - 1) } } FileUtils.deleteFile(unzipFolder.absolutePath) return contents.toString() } /** * 获取网络zip文件里面的数据 * @param url zip文件的链接或十六进制字符串 * @param path 所需获取文件在zip内的路径 * @return zip指定文件的数据 */ fun getZipStringContent(url: String, path: String): String { val byteArray = getZipByteArrayContent(url, path) ?: return "" val charsetName = EncodingDetect.getEncode(byteArray) return String(byteArray, Charset.forName(charsetName)) } fun getZipStringContent(url: String, path: String, charsetName: String): String { val byteArray = getZipByteArrayContent(url, path) ?: return "" return String(byteArray, Charset.forName(charsetName)) } /** * 获取网络zip文件里面的数据 * @param url zip文件的链接或十六进制字符串 * @param path 所需获取文件在zip内的路径 * @return zip指定文件的数据 */ fun getZipByteArrayContent(url: String, path: String): ByteArray? { val bytes = if (url.startsWith("http://") || url.startsWith("https://")) { runBlocking { return@runBlocking okHttpClient.newCall { url(url) }.bytes() } } else { StringUtils.hexStringToByte(url) } val bos = ByteArrayOutputStream() val zis = ZipInputStream(ByteArrayInputStream(bytes)) var entry: ZipEntry? = zis.nextEntry while (entry != null) { if (entry.name.equals(path)) { zis.use { it.copyTo(bos) } return bos.toByteArray() } entry = zis.nextEntry } Debug.log("getZipContent 未发现内容") return null } //******************文件操作************************// /** * 解析字体,返回字体解析类 */ fun queryBase64TTF(base64: String?): QueryTTF? { base64DecodeToByteArray(base64)?.let { return QueryTTF(it) } return null } /** * 返回字体解析类 * @param str 支持url,本地文件,base64,自动判断,自动缓存 */ fun queryTTF(str: String?): QueryTTF? { str ?: return null val key = md5Encode16(str) var qTTF = CacheManager.getQueryTTF(key) if (qTTF != null) return qTTF val font: ByteArray? = when { str.isAbsUrl() -> runBlocking { var x = CacheManager.getByteArray(key) if (x == null) { x = okHttpClient.newCall { url(str) }.bytes() x.let { CacheManager.put(key, it) } } return@runBlocking x } str.indexOf("storage/") > 0 -> File(str).readBytes() else -> base64DecodeToByteArray(str) } font ?: return null qTTF = QueryTTF(font) CacheManager.put(key, qTTF) return qTTF } /** * @param text 包含错误字体的内容 * @param font1 错误的字体 * @param font2 正确的字体 */ fun replaceFont( text: String, font1: QueryTTF?, font2: QueryTTF? ): String { if (font1 == null || font2 == null) return text val contentArray = text.toCharArray() contentArray.forEachIndexed { index, s -> val oldCode = s.code if (font1.inLimit(s)) { val code = font2.getCodeByGlyf(font1.getGlyfByCode(oldCode)) if (code != 0) contentArray[index] = code.toChar() } } return contentArray.joinToString("") } /** * 弹窗提示 */ fun toast(msg: Any?) { Debug.log("toast: " + msg.toString()) } /** * 弹窗提示 停留时间较长 */ fun longToast(msg: Any?) { Debug.log("longToast: " + msg.toString()) } /** * 输出调试日志 */ fun log(msg: String): String { Debug.log(msg) return msg } /** * 输出对象类型 */ fun logType(any: Any?) { if (any == null) { log("null") } else { log(any.javaClass.name) } } /** * 生成UUID */ fun randomUUID(): String { return UUID.randomUUID().toString() } /** * AES 解码为 ByteArray * @param str 传入的AES加密的数据 * @param key AES 解密的key * @param transformation AES加密的方式 * @param iv ECB模式的偏移向量 */ fun aesDecodeToByteArray( str: String, key: String, transformation: String, iv: String ): ByteArray? { return try { EncoderUtils.decryptAES( data = str.encodeToByteArray(), key = key.encodeToByteArray(), transformation, iv.encodeToByteArray() ) } catch (e: Exception) { e.printOnDebug() log(e.localizedMessage ?: "aesDecodeToByteArrayERROR") null } } /** * AES 解码为 String * @param str 传入的AES加密的数据 * @param key AES 解密的key * @param transformation AES加密的方式 * @param iv ECB模式的偏移向量 */ fun aesDecodeToString( str: String, key: String, transformation: String, iv: String ): String? { return aesDecodeToByteArray(str, key, transformation, iv)?.let { String(it) } } /** * 已经base64的AES 解码为 ByteArray * @param str 传入的AES Base64加密的数据 * @param key AES 解密的key * @param transformation AES加密的方式 * @param iv ECB模式的偏移向量 */ fun aesBase64DecodeToByteArray( str: String, key: String, transformation: String, iv: String ): ByteArray? { return try { EncoderUtils.decryptBase64AES( str.encodeToByteArray(), key.encodeToByteArray(), transformation, iv.encodeToByteArray() ) } catch (e: Exception) { e.printOnDebug() log(e.localizedMessage ?: "aesDecodeToByteArrayERROR") null } } /** * 已经base64的AES 解码为 String * @param str 传入的AES Base64加密的数据 * @param key AES 解密的key * @param transformation AES加密的方式 * @param iv ECB模式的偏移向量 */ fun aesBase64DecodeToString( str: String, key: String, transformation: String, iv: String ): String? { return aesBase64DecodeToByteArray(str, key, transformation, iv)?.let { String(it) } } /** * 加密aes为ByteArray * @param data 传入的原始数据 * @param key AES加密的key * @param transformation AES加密的方式 * @param iv ECB模式的偏移向量 */ fun aesEncodeToByteArray( data: String, key: String, transformation: String, iv: String ): ByteArray? { return try { EncoderUtils.encryptAES( data.encodeToByteArray(), key = key.encodeToByteArray(), transformation, iv.encodeToByteArray() ) } catch (e: Exception) { e.printOnDebug() log(e.localizedMessage ?: "aesEncodeToByteArrayERROR") null } } /** * 加密aes为String * @param data 传入的原始数据 * @param key AES加密的key * @param transformation AES加密的方式 * @param iv ECB模式的偏移向量 */ fun aesEncodeToString( data: String, key: String, transformation: String, iv: String ): String? { return aesEncodeToByteArray(data, key, transformation, iv)?.let { String(it) } } /** * 加密aes后Base64化的ByteArray * @param data 传入的原始数据 * @param key AES加密的key * @param transformation AES加密的方式 * @param iv ECB模式的偏移向量 */ fun aesEncodeToBase64ByteArray( data: String, key: String, transformation: String, iv: String ): ByteArray? { return try { EncoderUtils.encryptAES2Base64( data.encodeToByteArray(), key.encodeToByteArray(), transformation, iv.encodeToByteArray() ) } catch (e: Exception) { e.printOnDebug() log(e.localizedMessage ?: "aesEncodeToBase64ByteArrayERROR") null } } /** * 加密aes后Base64化的String * @param data 传入的原始数据 * @param key AES加密的key * @param transformation AES加密的方式 * @param iv ECB模式的偏移向量 */ fun aesEncodeToBase64String( data: String, key: String, transformation: String, iv: String ): String? { return aesEncodeToBase64ByteArray(data, key, transformation, iv)?.let { String(it) } } fun androidId(): String { return "" } /** * AES解密,算法参数经过Base64加密 * * @param data 加密的字符串 * @param key Base64后的密钥 * @param mode 模式 * @param padding 补码方式 * @param iv Base64后的加盐 * @return 解密后的字符串 */ fun aesDecodeArgsBase64Str( data: String, key: String, mode: String, padding: String, iv: String ): String? { return AES( mode, padding, Base64.decode(key, Base64.NO_WRAP), Base64.decode(iv, Base64.NO_WRAP) ).decryptStr(data) } /** * 3DES解密 * * @param data 加密的字符串 * @param key 密钥 * @param mode 模式 * @param padding 补码方式 * @param iv 加盐 * @return 解密后的字符串 */ fun tripleDESDecodeStr( data: String, key: String, mode: String, padding: String, iv: String ): String? { return DESede(mode, padding, key.toByteArray(), iv.toByteArray()).decryptStr(data) } /** * 3DES解密,算法参数经过Base64加密 * * @param data 加密的字符串 * @param key Base64后的密钥 * @param mode 模式 * @param padding 补码方式 * @param iv Base64后的加盐 * @return 解密后的字符串 */ fun tripleDESDecodeArgsBase64Str( data: String, key: String, mode: String, padding: String, iv: String ): String? { return DESede( mode, padding, Base64.decode(key, Base64.NO_WRAP), Base64.decode(iv, Base64.NO_WRAP) ).decryptStr(data) } /** * AES加密并转为Base64,算法参数经过Base64加密 * * @param data 被加密的字符串 * @param key Base64后的密钥 * @param mode 模式 * @param padding 补码方式 * @param iv Base64后的加盐 * @return 加密后的Base64 */ fun aesEncodeArgsBase64Str( data: String, key: String, mode: String, padding: String, iv: String ): String? { return AES( mode, padding, Base64.decode(key, Base64.NO_WRAP), Base64.decode(iv, Base64.NO_WRAP) ).encryptBase64(data) } /////DES fun desDecodeToString( data: String, key: String, transformation: String, iv: String ): String? { return EncoderUtils.decryptDES( data.encodeToByteArray(), key.encodeToByteArray(), transformation, iv.encodeToByteArray() )?.let { String(it) } } fun desBase64DecodeToString( data: String, key: String, transformation: String, iv: String ): String? { return EncoderUtils.decryptBase64DES( data.encodeToByteArray(), key.encodeToByteArray(), transformation, iv.encodeToByteArray() )?.let { String(it) } } fun desEncodeToString( data: String, key: String, transformation: String, iv: String ): String? { return EncoderUtils.encryptDES( data.encodeToByteArray(), key.encodeToByteArray(), transformation, iv.encodeToByteArray() )?.let { String(it) } } fun desEncodeToBase64String( data: String, key: String, transformation: String, iv: String ): String? { return EncoderUtils.encryptDES2Base64( data.encodeToByteArray(), key.encodeToByteArray(), transformation, iv.encodeToByteArray() )?.let { String(it) } } /** * 3DES加密并转为Base64 * * @param data 被加密的字符串 * @param key 密钥 * @param mode 模式 * @param padding 补码方式 * @param iv 加盐 * @return 加密后的Base64 */ fun tripleDESEncodeBase64Str( data: String, key: String, mode: String, padding: String, iv: String ): String? { return DESede(mode, padding, key.toByteArray(), iv.toByteArray()).encryptBase64(data) } /** * 3DES加密并转为Base64,算法参数经过Base64加密 * * @param data 被加密的字符串 * @param key Base64后的密钥 * @param mode 模式 * @param padding 补码方式 * @param iv Base64后的加盐 * @return 加密后的Base64 */ fun tripleDESEncodeArgsBase64Str( data: String, key: String, mode: String, padding: String, iv: String ): String? { return DESede( mode, padding, Base64.decode(key, Base64.NO_WRAP), Base64.decode(iv, Base64.NO_WRAP) ).encryptBase64(data) } /** * 生成摘要,并转为16进制字符串 * * @param data 被摘要数据 * @param algorithm 签名算法 * @return 16进制字符串 */ fun digestHex( data: String, algorithm: String, ): String? { return DigestUtil.digester(algorithm).digestHex(data) } /** * 生成摘要,并转为Base64字符串 * * @param data 被摘要数据 * @param algorithm 签名算法 * @return Base64字符串 */ fun digestBase64Str( data: String, algorithm: String, ): String? { return Base64.encodeToString(DigestUtil.digester(algorithm).digest(data), Base64.NO_WRAP) } } ================================================ FILE: src/main/java/io/legado/app/help/coroutine/CompositeCoroutine.kt ================================================ package io.legado.app.help.coroutine class CompositeCoroutine : CoroutineContainer { private var resources: HashSet>? = null val size: Int get() = resources?.size ?: 0 val isEmpty: Boolean get() = size == 0 constructor() constructor(vararg coroutines: Coroutine<*>) { this.resources = hashSetOf(*coroutines) } constructor(coroutines: Iterable>) { this.resources = hashSetOf() for (d in coroutines) { this.resources?.add(d) } } override fun add(coroutine: Coroutine<*>): Boolean { synchronized(this) { var set: HashSet>? = resources if (resources == null) { set = hashSetOf() resources = set } return set!!.add(coroutine) } } override fun addAll(vararg coroutines: Coroutine<*>): Boolean { synchronized(this) { var set: HashSet>? = resources if (resources == null) { set = hashSetOf() resources = set } for (coroutine in coroutines) { val add = set!!.add(coroutine) if (!add) { return false } } } return true } override fun remove(coroutine: Coroutine<*>): Boolean { if (delete(coroutine)) { coroutine.cancel() return true } return false } override fun delete(coroutine: Coroutine<*>): Boolean { synchronized(this) { val set = resources if (set == null || !set.remove(coroutine)) { return false } } return true } override fun clear() { val set: HashSet>? synchronized(this) { set = resources resources = null } set?.forEachIndexed { _, coroutine -> coroutine.cancel() } } } ================================================ FILE: src/main/java/io/legado/app/help/coroutine/Coroutine.kt ================================================ package io.legado.app.help.coroutine import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext class Coroutine( val scope: CoroutineScope, context: CoroutineContext = Dispatchers.IO, block: suspend CoroutineScope.() -> T ) { companion object { private val DEFAULT = MainScope() fun async( scope: CoroutineScope = DEFAULT, context: CoroutineContext = Dispatchers.IO, block: suspend CoroutineScope.() -> T ): Coroutine { return Coroutine(scope, context, block) } } private val job: Job private var start: VoidCallback? = null private var success: Callback? = null private var error: Callback? = null private var finally: VoidCallback? = null private var cancel: VoidCallback? = null private var timeMillis: Long? = null private var errorReturn: Result? = null val isCancelled: Boolean get() = job.isCancelled val isActive: Boolean get() = job.isActive val isCompleted: Boolean get() = job.isCompleted init { this.job = executeInternal(context, block) } fun timeout(timeMillis: () -> Long): Coroutine { this.timeMillis = timeMillis() return this@Coroutine } fun timeout(timeMillis: Long): Coroutine { this.timeMillis = timeMillis return this@Coroutine } fun onErrorReturn(value: () -> T?): Coroutine { this.errorReturn = Result(value()) return this@Coroutine } fun onErrorReturn(value: T?): Coroutine { this.errorReturn = Result(value) return this@Coroutine } fun onStart( context: CoroutineContext? = null, block: (suspend CoroutineScope.() -> Unit) ): Coroutine { this.start = VoidCallback(context, block) return this@Coroutine } fun onSuccess( context: CoroutineContext? = null, block: suspend CoroutineScope.(T) -> Unit ): Coroutine { this.success = Callback(context, block) return this@Coroutine } fun onError( context: CoroutineContext? = null, block: suspend CoroutineScope.(Throwable) -> Unit ): Coroutine { this.error = Callback(context, block) return this@Coroutine } fun onFinally( context: CoroutineContext? = null, block: suspend CoroutineScope.() -> Unit ): Coroutine { this.finally = VoidCallback(context, block) return this@Coroutine } fun onCancel( context: CoroutineContext? = null, block: suspend CoroutineScope.() -> Unit ): Coroutine { this.cancel = VoidCallback(context, block) return this@Coroutine } //取消当前任务 fun cancel(cause: CancellationException? = null) { job.cancel(cause) cancel?.let { MainScope().launch { if (null == it.context) { it.block.invoke(scope) } else { withContext(scope.coroutineContext.plus(it.context)) { it.block.invoke(this) } } } } } fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle { return job.invokeOnCompletion(handler) } private fun executeInternal( context: CoroutineContext, block: suspend CoroutineScope.() -> T ): Job { return scope.plus(Dispatchers.IO).launch { try { start?.let { dispatchVoidCallback(this, it) } val value = executeBlock(scope, context, timeMillis ?: 0L, block) if (isActive) { success?.let { dispatchCallback(this, value, it) } } } catch (e: Throwable) { e.printStackTrace() val consume: Boolean = errorReturn?.value?.let { value -> if (isActive) { success?.let { dispatchCallback(this, value, it) } } true } ?: false if (!consume && isActive) { error?.let { dispatchCallback(this, e, it) } } } finally { if (isActive) { finally?.let { dispatchVoidCallback(this, it) } } } } } private suspend inline fun dispatchVoidCallback(scope: CoroutineScope, callback: VoidCallback) { if (null == callback.context) { callback.block.invoke(scope) } else { withContext(scope.coroutineContext.plus(callback.context)) { callback.block.invoke(this) } } } private suspend inline fun dispatchCallback( scope: CoroutineScope, value: R, callback: Callback ) { if (!scope.isActive) return if (null == callback.context) { callback.block.invoke(scope, value) } else { withContext(scope.coroutineContext.plus(callback.context)) { callback.block.invoke(this, value) } } } private suspend inline fun executeBlock( scope: CoroutineScope, context: CoroutineContext, timeMillis: Long, noinline block: suspend CoroutineScope.() -> T ): T { return withContext(scope.coroutineContext.plus(context)) { if (timeMillis > 0L) withTimeout(timeMillis) { block() } else { block() } } } private data class Result(val value: T?) private inner class VoidCallback( val context: CoroutineContext?, val block: suspend CoroutineScope.() -> Unit ) private inner class Callback( val context: CoroutineContext?, val block: suspend CoroutineScope.(VALUE) -> Unit ) } ================================================ FILE: src/main/java/io/legado/app/help/coroutine/CoroutineContainer.kt ================================================ package io.legado.app.help.coroutine internal interface CoroutineContainer { fun add(coroutine: Coroutine<*>): Boolean fun addAll(vararg coroutines: Coroutine<*>): Boolean fun remove(coroutine: Coroutine<*>): Boolean fun delete(coroutine: Coroutine<*>): Boolean fun clear() } ================================================ FILE: src/main/java/io/legado/app/help/http/AjaxWebView.kt ================================================ //package io.legado.app.help.http // //import android.annotation.SuppressLint //import android.net.http.SslError //import android.os.Build //import android.os.Handler //import android.os.Looper //import android.os.Message //import android.text.TextUtils //import android.webkit.* //import io.legado.app.App //import io.legado.app.constant.AppConst //import org.apache.commons.text.StringEscapeUtils //import io.legado.app.utils.TextUtils //import java.lang.ref.WeakReference // // //class AjaxWebView { // var callback: Callback? = null // private var mHandler: AjaxHandler // // init { // mHandler = AjaxHandler(this) // } // // class AjaxHandler(private val ajaxWebView: AjaxWebView) : Handler(Looper.getMainLooper()) { // // private var mWebView: WebView? = null // // override fun handleMessage(msg: Message) { // val params: AjaxParams // when (msg.what) { // MSG_AJAX_START -> { // params = msg.obj as AjaxParams // mWebView = createAjaxWebView(params, this) // } // MSG_SNIFF_START -> { // params = msg.obj as AjaxParams // mWebView = createAjaxWebView(params, this) // } // MSG_SUCCESS -> { // ajaxWebView.callback?.onResult(msg.obj as Res) // destroyWebView() // } // MSG_ERROR -> { // ajaxWebView.callback?.onError(msg.obj as Throwable) // destroyWebView() // } // } // } // // @SuppressLint("SetJavaScriptEnabled", "JavascriptInterface") // fun createAjaxWebView(params: AjaxParams, handler: Handler): WebView { // val webView = WebView(App.INSTANCE) // val settings = webView.settings // settings.javaScriptEnabled = true // settings.domStorageEnabled = true // settings.blockNetworkImage = true // settings.userAgentString = params.userAgent // settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW // if (params.isSniff) { // webView.webViewClient = SnifferWebClient(params, handler) // } else { // webView.webViewClient = HtmlWebViewClient(params, handler) // } // when (params.requestMethod) { // RequestMethod.POST -> webView.postUrl(params.url, params.postData) // RequestMethod.GET -> webView.loadUrl( // params.url, // params.headerMap // ) // } // return webView // } // // private fun destroyWebView() { // mWebView?.destroy() // mWebView = null // } // } // // fun load(params: AjaxParams) { // if (params.sourceRegex != "") { // mHandler.obtainMessage(MSG_SNIFF_START, params) // .sendToTarget() // } else { // mHandler.obtainMessage(MSG_AJAX_START, params) // .sendToTarget() // } // } // // fun destroyWebView() { // mHandler.obtainMessage(DESTROY_WEB_VIEW) // } // // class AjaxParams(val url: String) { // var tag: String? = null // var requestMethod = RequestMethod.GET // var postData: ByteArray? = null // var headerMap: Map? = null // var sourceRegex: String? = null // var javaScript: String? = null // // fun getJs(): String { // javaScript?.let { // if (it.isNotEmpty()) { // return it // } // } // return JS // } // // val userAgent: String? // get() = this.headerMap?.get(AppConst.UA_NAME) // // val isSniff: Boolean // get() = !TextUtils.isEmpty(sourceRegex) // // fun setCookie(url: String) { // tag?.let { // val cookie = CookieManager.getInstance().getCookie(url) // CookieStore.setCookie(it, cookie) // } // } // // fun hasJavaScript(): Boolean { // return !TextUtils.isEmpty(javaScript) // } // // fun clearJavaScript() { // javaScript = null // } // // } // // private class HtmlWebViewClient( // private val params: AjaxParams, // private val handler: Handler // ) : WebViewClient() { // // override fun onPageFinished(view: WebView, url: String) { // params.setCookie(url) // val runnable = EvalJsRunnable(view, url, params.getJs(), handler) // handler.postDelayed(runnable, 1000) // } // // override fun onReceivedError( // view: WebView, // errorCode: Int, // description: String, // failingUrl: String // ) { // if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // handler.obtainMessage(MSG_ERROR, Exception(description)) // .sendToTarget() // } // } // // override fun onReceivedError( // view: WebView, // request: WebResourceRequest, // error: WebResourceError // ) { // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // handler.obtainMessage( // MSG_ERROR, // Exception(error.description.toString()) // ).sendToTarget() // } // } // // override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { // handler.proceed() // } // } // // private class EvalJsRunnable( // webView: WebView, // private val url: String, // private val mJavaScript: String, // private val handler: Handler // ) : Runnable { // var retry = 0 // private val mWebView: WeakReference = WeakReference(webView) // override fun run() { // mWebView.get()?.evaluateJavascript(mJavaScript) { // if (it.isNotEmpty() && it != "null") { // val content = StringEscapeUtils.unescapeJson(it) // handler.obtainMessage(MSG_SUCCESS, Res(url, content)) // .sendToTarget() // handler.removeCallbacks(this) // return@evaluateJavascript // } // if (retry > 30) { // handler.obtainMessage(MSG_ERROR, Exception("time out")) // .sendToTarget() // handler.removeCallbacks(this) // return@evaluateJavascript // } // retry++ // handler.removeCallbacks(this) // handler.postDelayed(this, 1000) // } // } // } // // private class SnifferWebClient( // private val params: AjaxParams, // private val handler: Handler // ) : WebViewClient() { // // override fun onLoadResource(view: WebView, url: String) { // params.sourceRegex?.let { // if (url.matches(it.toRegex())) { // handler.obtainMessage(MSG_SUCCESS, Res(view.url ?: params.url, url)) // .sendToTarget() // } // } // } // // override fun onReceivedError( // view: WebView, // errorCode: Int, // description: String, // failingUrl: String // ) { // if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // handler.obtainMessage(MSG_ERROR, Exception(description)) // .sendToTarget() // } // } // // override fun onReceivedError( // view: WebView, // request: WebResourceRequest, // error: WebResourceError // ) { // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // handler.obtainMessage( // MSG_ERROR, // Exception(error.description.toString()) // ).sendToTarget() // } // } // // override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { // handler.proceed() // } // // override fun onPageFinished(view: WebView, url: String) { // params.setCookie(url) // if (params.hasJavaScript()) { // evaluateJavascript(view, params.javaScript) // params.clearJavaScript() // } // } // // private fun evaluateJavascript(webView: WebView, javaScript: String?) { // val runnable = LoadJsRunnable(webView, javaScript) // handler.postDelayed(runnable, 1000L) // } // } // // private class LoadJsRunnable( // webView: WebView, // private val mJavaScript: String? // ) : Runnable { // private val mWebView: WeakReference = WeakReference(webView) // override fun run() { // mWebView.get()?.loadUrl("javascript:${mJavaScript ?: ""}") // } // } // // companion object { // const val MSG_AJAX_START = 0 // const val MSG_SNIFF_START = 1 // const val MSG_SUCCESS = 2 // const val MSG_ERROR = 3 // const val DESTROY_WEB_VIEW = 4 // const val JS = "document.documentElement.outerHTML" // } // // abstract class Callback { // abstract fun onResult(response: Res) // abstract fun onError(error: Throwable) // } //} ================================================ FILE: src/main/java/io/legado/app/help/http/ByteConverter.kt ================================================ package io.legado.app.help.http import okhttp3.ResponseBody import retrofit2.Converter import retrofit2.Retrofit import java.lang.reflect.Type class ByteConverter : Converter.Factory() { override fun responseBodyConverter( type: Type?, annotations: Array?, retrofit: Retrofit? ): Converter? { return Converter { value -> value.bytes() } } } ================================================ FILE: src/main/java/io/legado/app/help/http/CookieStore.kt ================================================ @file:Suppress("unused") package io.legado.app.help.http import io.legado.app.utils.TextUtils import io.legado.app.data.entities.Cookie import io.legado.app.help.http.api.CookieManager import io.legado.app.utils.NetworkUtils // TODO 处理cookie object CookieStore : CookieManager { override fun setCookie(url: String, cookie: String?) { // val cookieBean = Cookie(NetworkUtils.getSubDomain(url), cookie ?: "") // appDb.cookieDao.insert(cookieBean) } override fun replaceCookie(url: String, cookie: String) { if (TextUtils.isEmpty(url) || TextUtils.isEmpty(cookie)) { return } val oldCookie = getCookie(url) if (TextUtils.isEmpty(oldCookie)) { setCookie(url, cookie) } else { val cookieMap = cookieToMap(oldCookie) cookieMap.putAll(cookieToMap(cookie)) val newCookie = mapToCookie(cookieMap) setCookie(url, newCookie) } } override fun getCookie(url: String): String { // val cookieBean = appDb.cookieDao.get(NetworkUtils.getSubDomain(url)) // return cookieBean?.cookie ?: "" return "" } override fun removeCookie(url: String) { // appDb.cookieDao.delete(NetworkUtils.getSubDomain(url)) } override fun cookieToMap(cookie: String): MutableMap { val cookieMap = mutableMapOf() if (cookie.isBlank()) { return cookieMap } val pairArray = cookie.split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() for (pair in pairArray) { val pairs = pair.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() if (pairs.size == 1) { continue } val key = pairs[0].trim { it <= ' ' } val value = pairs[1] if (value.isNotBlank() || value.trim { it <= ' ' } == "null") { cookieMap[key] = value.trim { it <= ' ' } } } return cookieMap } override fun mapToCookie(cookieMap: Map?): String? { if (cookieMap == null || cookieMap.isEmpty()) { return null } val builder = StringBuilder() for (key in cookieMap.keys) { val value = cookieMap[key] if (value?.isNotBlank() == true) { builder.append(key) .append("=") .append(value) .append(";") } } return builder.deleteCharAt(builder.lastIndexOf(";")).toString() } fun clear() { // appDb.cookieDao.deleteOkHttp() } } ================================================ FILE: src/main/java/io/legado/app/help/http/CoroutinesCallAdapterFactory.kt ================================================ package io.legado.app.help.http import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Deferred import retrofit2.* import java.lang.reflect.ParameterizedType import java.lang.reflect.Type class CoroutinesCallAdapterFactory private constructor() : CallAdapter.Factory() { companion object { fun create(): CoroutinesCallAdapterFactory { return CoroutinesCallAdapterFactory() } } override fun get( returnType: Type, annotations: Array, retrofit: Retrofit ): CallAdapter<*, *>? { if (Deferred::class.java != getRawType(returnType)) { return null } check(returnType is ParameterizedType) { "Deferred return type must be parameterized as Deferred or Deferred" } val responseType = getParameterUpperBound(0, returnType) val rawDeferredType = getRawType(responseType) return if (rawDeferredType == Response::class.java) { check(responseType is ParameterizedType) { "Response must be parameterized as Response or Response" } ResponseCallAdapter( getParameterUpperBound( 0, responseType ) ) } else { BodyCallAdapter(responseType) } } private class BodyCallAdapter( private val responseType: Type ) : CallAdapter> { override fun responseType() = responseType override fun adapt(call: Call): Deferred { val deferred = CompletableDeferred() deferred.invokeOnCompletion { if (deferred.isCancelled) { call.cancel() } } call.enqueue(object : Callback { override fun onFailure(call: Call, t: Throwable) { deferred.completeExceptionally(t) } override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { deferred.complete(response.body()!!) } else { deferred.completeExceptionally(HttpException(response)) } } }) return deferred } } private class ResponseCallAdapter( private val responseType: Type ) : CallAdapter>> { override fun responseType() = responseType override fun adapt(call: Call): Deferred> { val deferred = CompletableDeferred>() deferred.invokeOnCompletion { if (deferred.isCancelled) { call.cancel() } } call.enqueue(object : Callback { override fun onFailure(call: Call, t: Throwable) { deferred.completeExceptionally(t) } override fun onResponse(call: Call, response: Response) { deferred.complete(response) } }) return deferred } } } ================================================ FILE: src/main/java/io/legado/app/help/http/EncodeConverter.kt ================================================ package io.legado.app.help.http import io.legado.app.utils.UTF8BOMFighter import okhttp3.ResponseBody import io.legado.app.utils.EncodingDetect import retrofit2.Converter import retrofit2.Retrofit import java.lang.reflect.Type import java.nio.charset.Charset class EncodeConverter(private val encode: String? = null) : Converter.Factory() { override fun responseBodyConverter( type: Type?, annotations: Array?, retrofit: Retrofit? ): Converter? { return Converter { value -> val responseBytes = UTF8BOMFighter.removeUTF8BOM(value.bytes()) encode?.let { return@Converter String(responseBytes, Charset.forName(encode)) } var charsetName: String? = null val mediaType = value.contentType() //根据http头判断 if (mediaType != null) { val charset = mediaType.charset() charsetName = charset?.displayName() } if (charsetName == null) { charsetName = EncodingDetect.getHtmlEncode(responseBytes) } String(responseBytes, Charset.forName(charsetName)) } } } ================================================ FILE: src/main/java/io/legado/app/help/http/HttpHelper.kt ================================================ package io.legado.app.help.http // import io.legado.app.help.http.cronet.CronetInterceptor import kotlinx.coroutines.suspendCancellableCoroutine import okhttp3.ConnectionSpec import okhttp3.Credentials import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Route import okhttp3.Authenticator import okhttp3.Response import okhttp3.Request import okhttp3.logging.HttpLoggingInterceptor import java.net.InetSocketAddress import java.net.Proxy import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit import kotlin.coroutines.resume import java.io.IOException import io.legado.app.model.DebugLog private val proxyClientCache: ConcurrentHashMap by lazy { ConcurrentHashMap() } val okHttpClient: OkHttpClient by lazy { val specs = arrayListOf( ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS, ConnectionSpec.CLEARTEXT ) val builder = OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) .writeTimeout(15, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory, SSLHelper.unsafeTrustManager) .retryOnConnectionFailure(true) .hostnameVerifier(SSLHelper.unsafeHostnameVerifier) .connectionSpecs(specs) .followRedirects(true) .followSslRedirects(true) .addInterceptor(Interceptor { chain -> val request = chain.request() .newBuilder() .addHeader("Keep-Alive", "300") .addHeader("Connection", "Keep-Alive") .addHeader("Cache-Control", "no-cache") .build() chain.proceed(request) }) // if (AppConfig.isCronet) { // builder.addInterceptor(CronetInterceptor()) // } builder.build() } /** * 缓存代理okHttp */ fun getProxyClient(proxy: String? = null, debugLog: DebugLog? = null): OkHttpClient { if (proxy.isNullOrBlank()) { if (debugLog == null) { return okHttpClient } val builder = okHttpClient.newBuilder() val logInterceptor = HttpLoggingInterceptor(debugLog);//创建拦截对象 logInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);//这一句一定要记得写,否则没有数据输出 builder.addNetworkInterceptor(logInterceptor) //设置打印拦截日志 return builder.build() } if (debugLog == null) { proxyClientCache[proxy]?.let { return it } } val r = Regex("(http|socks4|socks5)://(.*):(\\d{2,5})(@.*@.*)?") val ms = r.findAll(proxy) val group = ms.first() var username = "" //代理服务器验证用户名 var password = "" //代理服务器验证密码 val type = if (group.groupValues[1] == "http") "http" else "socks" val host = group.groupValues[2] val port = group.groupValues[3].toInt() if (group.groupValues[4] != "") { username = group.groupValues[4].split("@")[1] password = group.groupValues[4].split("@")[2] } if (type != "direct" && host != "") { val builder = okHttpClient.newBuilder() if (type == "http") { builder.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port))) } else { builder.proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress(host, port))) } if (username != "" && password != "") { val proxyAuthenticator = object: Authenticator { @Throws(IOException::class) override fun authenticate(route: Route?, response: Response): Request { //设置代理服务器账号密码 val credential = Credentials.basic(username, password); return response.request.newBuilder() .header("Proxy-Authorization", credential) .build(); } } builder.proxyAuthenticator(proxyAuthenticator); // builder.proxyAuthenticator { _, response -> //设置代理服务器账号密码 // val credential: String = Credentials.basic(username, password) // response.request.newBuilder() // .header("Proxy-Authorization", credential) // .build() // } } if (debugLog != null) { val logInterceptor = HttpLoggingInterceptor(debugLog);//创建拦截对象 logInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);//这一句一定要记得写,否则没有数据输出 builder.addNetworkInterceptor(logInterceptor) //设置打印拦截日志 return builder.build() } val proxyClient = builder.build() proxyClientCache[proxy] = proxyClient return proxyClient } return okHttpClient } // suspend fun getWebViewSrc(params: AjaxWebView.AjaxParams): StrResponse = // suspendCancellableCoroutine { block -> // val webView = AjaxWebView() // block.invokeOnCancellation { // webView.destroyWebView() // } // webView.callback = object : AjaxWebView.Callback() { // override fun onResult(response: StrResponse) { // if (!block.isCompleted) // block.resume(response) // } // override fun onError(error: Throwable) { // if (!block.isCompleted) // block.cancel(error) // } // } // webView.load(params) // } ================================================ FILE: src/main/java/io/legado/app/help/http/OkHttpUtils.kt ================================================ package io.legado.app.help.http import io.legado.app.constant.AppConst import io.legado.app.utils.EncodingDetect import io.legado.app.utils.GSON import io.legado.app.utils.UTF8BOMFighter import io.legado.app.utils.Utf8BomUtils import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import kotlinx.coroutines.Dispatchers import okhttp3.* import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import java.io.File import java.io.IOException import java.nio.charset.Charset import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException suspend fun OkHttpClient.newCallResponse( retry: Int = 0, builder: Request.Builder.() -> Unit ): Response { return withContext(Dispatchers.IO) { val requestBuilder = Request.Builder() requestBuilder.apply(builder) var response: Response? = null for (i in 0..retry) { response = newCall(requestBuilder.build()).await() if (response.isSuccessful) { return@withContext response } } return@withContext response!! } } suspend fun OkHttpClient.newCallResponseBody( retry: Int = 0, builder: Request.Builder.() -> Unit ): ResponseBody { return newCallResponse(retry, builder).let { it.body ?: throw IOException(it.message) } } suspend fun OkHttpClient.newCall( retry: Int = 0, builder: Request.Builder.() -> Unit ): ResponseBody { val requestBuilder = Request.Builder() requestBuilder.apply(builder) var response: Response? = null for (i in 0..retry) { response = this.newCall(requestBuilder.build()).await() if (response.isSuccessful) { return response.body!! } } return response!!.body ?: throw IOException(response.message) } suspend fun OkHttpClient.newCallStrResponse( retry: Int = 0, builder: Request.Builder.() -> Unit ): StrResponse { val requestBuilder = Request.Builder() requestBuilder.apply(builder) var response: Response? = null for (i in 0..retry) { response = this.newCall(requestBuilder.build()).await() if (response.isSuccessful) { return StrResponse(response, response.body!!.text()) } } return StrResponse(response!!, response.body?.text() ?: response.message) } suspend fun Call.await(): Response = suspendCancellableCoroutine { block -> block.invokeOnCancellation { cancel() } enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { block.resumeWithException(e) } override fun onResponse(call: Call, response: Response) { block.resume(response) } }) } fun ResponseBody.text(encode: String? = null): String { val responseBytes = Utf8BomUtils.removeUTF8BOM(bytes()) var charsetName: String? = encode charsetName?.let { return String(responseBytes, Charset.forName(charsetName)) } //根据http头判断 contentType()?.charset()?.let { return String(responseBytes, it) } //根据内容判断 charsetName = EncodingDetect.getHtmlEncode(responseBytes) return String(responseBytes, Charset.forName(charsetName)) } fun Request.Builder.addHeaders(headers: Map) { headers.forEach { addHeader(it.key, it.value) } } fun Request.Builder.get(url: String, queryMap: Map, encoded: Boolean = false) { val httpBuilder = url.toHttpUrl().newBuilder() queryMap.forEach { if (encoded) { httpBuilder.addEncodedQueryParameter(it.key, it.value) } else { httpBuilder.addQueryParameter(it.key, it.value) } } url(httpBuilder.build()) } fun Request.Builder.postForm(form: Map, encoded: Boolean = false) { val formBody = FormBody.Builder() form.forEach { if (encoded) { formBody.addEncoded(it.key, it.value) } else { formBody.add(it.key, it.value) } } post(formBody.build()) } fun Request.Builder.postMultipart(type: String?, form: Map) { val multipartBody = MultipartBody.Builder() type?.let { multipartBody.setType(type.toMediaType()) } form.forEach { when (val value = it.value) { is Map<*, *> -> { val fileName = value["fileName"] as String val file = value["file"] val mediaType = (value["contentType"] as? String)?.toMediaType() val requestBody = when (file) { is File -> { file.asRequestBody(mediaType) } is ByteArray -> { file.toRequestBody(mediaType) } is String -> { file.toRequestBody(mediaType) } else -> { GSON.toJson(file).toRequestBody(mediaType) } } multipartBody.addFormDataPart(it.key, fileName, requestBody) } else -> multipartBody.addFormDataPart(it.key, it.value.toString()) } } post(multipartBody.build()) } fun Request.Builder.postJson(json: String?) { json?.let { val requestBody = json.toRequestBody("application/json; charset=UTF-8".toMediaType()) post(requestBody) } } ================================================ FILE: src/main/java/io/legado/app/help/http/RequestMethod.kt ================================================ package io.legado.app.help.http enum class RequestMethod { GET, POST } ================================================ FILE: src/main/java/io/legado/app/help/http/Res.kt ================================================ package io.legado.app.help.http data class Res(val url: String, val body: String?) ================================================ FILE: src/main/java/io/legado/app/help/http/SSLHelper.kt ================================================ package io.legado.app.help.http //import android.annotation.SuppressLint import java.io.IOException import java.io.InputStream import java.security.KeyManagementException import java.security.KeyStore import java.security.NoSuchAlgorithmException import java.security.SecureRandom import java.security.cert.CertificateException import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import javax.net.ssl.* object SSLHelper { val sslSocketFactory: SSLParams? get() = getSslSocketFactoryBase(null, null, null) /** * 为了解决客户端不信任服务器数字证书的问题,网络上大部分的解决方案都是让客户端不对证书做任何检查, * 这是一种有很大安全漏洞的办法 */ val unsafeTrustManager: X509TrustManager = object : X509TrustManager { // @SuppressLint("TrustAllX509TrustManager") @Throws(CertificateException::class) override fun checkClientTrusted(chain: Array, authType: String) { } // @SuppressLint("TrustAllX509TrustManager") @Throws(CertificateException::class) override fun checkServerTrusted(chain: Array, authType: String) { } override fun getAcceptedIssuers(): Array { return arrayOf() } } val unsafeSSLSocketFactory: SSLSocketFactory by lazy { try { val sslContext = SSLContext.getInstance("SSL") sslContext.init(null, arrayOf(unsafeTrustManager), SecureRandom()) sslContext.socketFactory } catch (e: Exception) { throw RuntimeException(e) } } /** * 此类是用于主机名验证的基接口。 在握手期间,如果 URL 的主机名和服务器的标识主机名不匹配, * 则验证机制可以回调此接口的实现程序来确定是否应该允许此连接。策略可以是基于证书的或依赖于其他验证方案。 * 当验证 URL 主机名使用的默认规则失败时使用这些回调。如果主机名是可接受的,则返回 true */ val unsafeHostnameVerifier: HostnameVerifier = HostnameVerifier { _, _ -> true } class SSLParams { lateinit var sSLSocketFactory: SSLSocketFactory lateinit var trustManager: X509TrustManager } /** * https单向认证 * 可以额外配置信任服务端的证书策略,否则默认是按CA证书去验证的,若不是CA可信任的证书,则无法通过验证 */ fun getSslSocketFactory(trustManager: X509TrustManager): SSLParams? { return getSslSocketFactoryBase(trustManager, null, null) } /** * https单向认证 * 用含有服务端公钥的证书校验服务端证书 */ fun getSslSocketFactory(vararg certificates: InputStream): SSLParams? { return getSslSocketFactoryBase(null, null, null, *certificates) } /** * https双向认证 * bksFile 和 password -> 客户端使用bks证书校验服务端证书 * certificates -> 用含有服务端公钥的证书校验服务端证书 */ fun getSslSocketFactory(bksFile: InputStream, password: String, vararg certificates: InputStream): SSLParams? { return getSslSocketFactoryBase(null, bksFile, password, *certificates) } /** * https双向认证 * bksFile 和 password -> 客户端使用bks证书校验服务端证书 * X509TrustManager -> 如果需要自己校验,那么可以自己实现相关校验,如果不需要自己校验,那么传null即可 */ fun getSslSocketFactory(bksFile: InputStream, password: String, trustManager: X509TrustManager): SSLParams? { return getSslSocketFactoryBase(trustManager, bksFile, password) } private fun getSslSocketFactoryBase( trustManager: X509TrustManager?, bksFile: InputStream?, password: String?, vararg certificates: InputStream ): SSLParams? { val sslParams = SSLParams() try { val keyManagers = prepareKeyManager(bksFile, password) val trustManagers = prepareTrustManager(*certificates) val manager: X509TrustManager = trustManager ?: chooseTrustManager(trustManagers) // 创建TLS类型的SSLContext对象, that uses our TrustManager val sslContext = SSLContext.getInstance("TLS") // 用上面得到的trustManagers初始化SSLContext,这样sslContext就会信任keyStore中的证书 // 第一个参数是授权的密钥管理器,用来授权验证,比如授权自签名的证书验证。第二个是被授权的证书管理器,用来验证服务器端的证书 sslContext.init(keyManagers, arrayOf(manager), null) // 通过sslContext获取SSLSocketFactory对象 sslParams.sSLSocketFactory = sslContext.socketFactory sslParams.trustManager = manager return sslParams } catch (e: NoSuchAlgorithmException) { e.printStackTrace() } catch (e: KeyManagementException) { e.printStackTrace() } return null } private fun prepareKeyManager(bksFile: InputStream?, password: String?): Array? { try { if (bksFile == null || password == null) return null val clientKeyStore = KeyStore.getInstance("BKS") clientKeyStore.load(bksFile, password.toCharArray()) val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) kmf.init(clientKeyStore, password.toCharArray()) return kmf.keyManagers } catch (e: Exception) { e.printStackTrace() } return null } private fun prepareTrustManager(vararg certificates: InputStream): Array { val certificateFactory = CertificateFactory.getInstance("X.509") // 创建一个默认类型的KeyStore,存储我们信任的证书 val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) keyStore.load(null) for ((index, certStream) in certificates.withIndex()) { val certificateAlias = Integer.toString(index) // 证书工厂根据证书文件的流生成证书 cert val cert = certificateFactory.generateCertificate(certStream) // 将 cert 作为可信证书放入到keyStore中 keyStore.setCertificateEntry(certificateAlias, cert) try { certStream.close() } catch (e: IOException) { e.printStackTrace() } } //我们创建一个默认类型的TrustManagerFactory val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) //用我们之前的keyStore实例初始化TrustManagerFactory,这样tmf就会信任keyStore中的证书 tmf.init(keyStore) //通过tmf获取TrustManager数组,TrustManager也会信任keyStore中的证书 return tmf.trustManagers } private fun chooseTrustManager(trustManagers: Array): X509TrustManager { for (trustManager in trustManagers) { if (trustManager is X509TrustManager) { return trustManager } } throw NullPointerException() } } ================================================ FILE: src/main/java/io/legado/app/help/http/StrResponse.kt ================================================ package io.legado.app.help.http import okhttp3.* import okhttp3.Response.Builder /** * An HTTP response. */ @Suppress("unused", "MemberVisibilityCanBePrivate") class StrResponse { var raw: Response private set var body: String? = null private set var errorBody: ResponseBody? = null private set constructor(rawResponse: Response, body: String?) { this.raw = rawResponse this.body = body } constructor(url: String, body: String?) { raw = Builder() .code(200) .message("OK") .protocol(Protocol.HTTP_1_1) .request(Request.Builder().url(url).build()) .build() this.body = body } constructor(rawResponse: Response, errorBody: ResponseBody?) { this.raw = rawResponse this.errorBody = errorBody } fun raw() = raw fun url(): String { raw.networkResponse?.let { return it.request.url.toString() } return raw.request.url.toString() } val url: String get() = url() fun body() = body fun code(): Int { return raw.code } fun message(): String { return raw.message } fun headers(): Headers { return raw.headers } fun isSuccessful(): Boolean = raw.isSuccessful fun errorBody(): ResponseBody? { return errorBody } override fun toString(): String { return raw.toString() } } ================================================ FILE: src/main/java/io/legado/app/help/http/api/CookieManager.kt ================================================ package io.legado.app.help.http.api interface CookieManager { /** * 保存cookie */ fun setCookie(url: String, cookie: String?) /** * 替换cookie */ fun replaceCookie(url: String, cookie: String) /** * 获取cookie */ fun getCookie(url: String): String /** * 移除cookie */ fun removeCookie(url: String) fun cookieToMap(cookie: String): MutableMap fun mapToCookie(cookieMap: Map?): String? } ================================================ FILE: src/main/java/io/legado/app/lib/icu4j/CharsetDetector.java ================================================ // © 2016 and later: Unicode, Inc. and others. // License & terms of use: http://www.unicode.org/copyright.html /* ****************************************************************************** Copyright (C) 2005-2016, International Business Machines Corporation and * others. All Rights Reserved. * ****************************************************************************** */ package io.legado.app.lib.icu4j; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * CharsetDetector provides a facility for detecting the * charset or encoding of character data in an unknown format. * The input data can either be from an input stream or an array of bytes. * The result of the detection operation is a list of possibly matching * charsets, or, for simple use, you can just ask for a Java Reader that * will will work over the input data. *

* Character set detection is at best an imprecise operation. The detection * process will attempt to identify the charset that best matches the characteristics * of the byte data, but the process is partly statistical in nature, and * the results can not be guaranteed to always be correct. *

* For best accuracy in charset detection, the input data should be primarily * in a single language, and a minimum of a few hundred bytes worth of plain text * in the language are needed. The detection process will attempt to * ignore html or xml style markup that could otherwise obscure the content. *

* * @stable ICU 3.4 */ @SuppressWarnings({"JavaDoc", "unused", "RedundantSuppression"}) public class CharsetDetector { // Question: Should we have getters corresponding to the setters for input text // and declared encoding? // A thought: If we were to create our own type of Java Reader, we could defer // figuring out an actual charset for data that starts out with too much English // only ASCII until the user actually read through to something that didn't look // like 7 bit English. If nothing else ever appeared, we would never need to // actually choose the "real" charset. All assuming that the application just // wants the data, and doesn't care about a char set name. /** * Constructor * * @stable ICU 3.4 */ public CharsetDetector() { } /** * Set the declared encoding for charset detection. * The declared encoding of an input text is an encoding obtained * from an http header or xml declaration or similar source that * can be provided as additional information to the charset detector. * A match between a declared encoding and a possible detected encoding * will raise the quality of that detected encoding by a small delta, * and will also appear as a "reason" for the match. *

* A declared encoding that is incompatible with the input data being * analyzed will not be added to the list of possible encodings. * * @param encoding The declared encoding * @stable ICU 3.4 */ public CharsetDetector setDeclaredEncoding(String encoding) { fDeclaredEncoding = encoding; return this; } /** * Set the input text (byte) data whose charset is to be detected. * * @param in the input text of unknown encoding * @return This CharsetDetector * @stable ICU 3.4 */ public CharsetDetector setText(byte[] in) { fRawInput = in; fRawLength = in.length; return this; } private static final int kBufSize = 8000; /** * Set the input text (byte) data whose charset is to be detected. *

* The input stream that supplies the character data must have markSupported() * == true; the charset detection process will read a small amount of data, * then return the stream to its original position via * the InputStream.reset() operation. The exact amount that will * be read depends on the characteristics of the data itself. * * @param in the input text of unknown encoding * @return This CharsetDetector * @stable ICU 3.4 */ public CharsetDetector setText(InputStream in) throws IOException { fInputStream = in; fInputStream.mark(kBufSize); fRawInput = new byte[kBufSize]; // Always make a new buffer because the // previous one may have come from the caller, // in which case we can't touch it. fRawLength = 0; int remainingLength = kBufSize; while (remainingLength > 0) { // read() may give data in smallish chunks, esp. for remote sources. Hence, this loop. int bytesRead = fInputStream.read(fRawInput, fRawLength, remainingLength); if (bytesRead <= 0) { break; } fRawLength += bytesRead; remainingLength -= bytesRead; } fInputStream.reset(); return this; } /** * Return the charset that best matches the supplied input data. *

* Note though, that because the detection * only looks at the start of the input data, * there is a possibility that the returned charset will fail to handle * the full set of input data. * p> * aise an exception if *

    *
  • no charset appears to match the data.
  • *
  • no input text has been provided
  • *
* * @return a CharsetMatch object representing the best matching charset, or * null if there are no matches. * @stable ICU 3.4 */ public CharsetMatch detect() { // TODO: A better implementation would be to copy the detect loop from // detectAll(), and cut it short as soon as a match with a high confidence // is found. This is something to be done later, after things are otherwise // working. CharsetMatch[] matches = detectAll(); if (matches == null || matches.length == 0) { return null; } return matches[0]; } /** * Return an array of all charsets that appear to be plausible * matches with the input data. The array is ordered with the * best quality match first. *

* aise an exception if *

    *
  • no charsets appear to match the input data.
  • *
  • no input text has been provided
  • *
* * @return An array of CharsetMatch objects representing possibly matching charsets. * @stable ICU 3.4 */ public CharsetMatch[] detectAll() { ArrayList matches = new ArrayList<>(); MungeInput(); // Strip html markup, collect byte stats. // Iterate over all possible charsets, remember all that // give a match quality > 0. for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) { CSRecognizerInfo rcinfo = ALL_CS_RECOGNIZERS.get(i); boolean active = (fEnabledRecognizers != null) ? fEnabledRecognizers[i] : rcinfo.isDefaultEnabled; if (active) { CharsetMatch m = rcinfo.recognizer.match(this); if (m != null) { matches.add(m); } } } Collections.sort(matches); // CharsetMatch compares on confidence Collections.reverse(matches); // Put best match first. CharsetMatch[] resultArray = new CharsetMatch[matches.size()]; resultArray = matches.toArray(resultArray); return resultArray; } /** * Autodetect the charset of an inputStream, and return a Java Reader * to access the converted input data. *

* This is a convenience method that is equivalent to * this.setDeclaredEncoding(declaredEncoding).setText(in).detect().getReader(); *

* For the input stream that supplies the character data, markSupported() * must be true; the charset detection will read a small amount of data, * then return the stream to its original position via * the InputStream.reset() operation. The exact amount that will * be read depends on the characteristics of the data itself. *

* Raise an exception if no charsets appear to match the input data. * * @param in The source of the byte data in the unknown charset. * @param declaredEncoding A declared encoding for the data, if available, * or null or an empty string if none is available. * @stable ICU 3.4 */ public Reader getReader(InputStream in, String declaredEncoding) { fDeclaredEncoding = declaredEncoding; try { setText(in); CharsetMatch match = detect(); if (match == null) { return null; } return match.getReader(); } catch (IOException e) { return null; } } /** * Autodetect the charset of an inputStream, and return a String * containing the converted input data. *

* This is a convenience method that is equivalent to * this.setDeclaredEncoding(declaredEncoding).setText(in).detect().getString(); *

* Raise an exception if no charsets appear to match the input data. * * @param in The source of the byte data in the unknown charset. * @param declaredEncoding A declared encoding for the data, if available, * or null or an empty string if none is available. * @stable ICU 3.4 */ public String getString(byte[] in, String declaredEncoding) { fDeclaredEncoding = declaredEncoding; try { setText(in); CharsetMatch match = detect(); if (match == null) { return null; } return match.getString(-1); } catch (IOException e) { return null; } } /** * Get the names of all charsets supported by CharsetDetector class. *

* Note: Multiple different charset encodings in a same family may use * a single shared name in this implementation. For example, this method returns * an array including "ISO-8859-1" (ISO Latin 1), but not including "windows-1252" * (Windows Latin 1). However, actual detection result could be "windows-1252" * when the input data matches Latin 1 code points with any points only available * in "windows-1252". * * @return an array of the names of all charsets supported by * CharsetDetector class. * @stable ICU 3.4 */ public static String[] getAllDetectableCharsets() { String[] allCharsetNames = new String[ALL_CS_RECOGNIZERS.size()]; for (int i = 0; i < allCharsetNames.length; i++) { allCharsetNames[i] = ALL_CS_RECOGNIZERS.get(i).recognizer.getName(); } return allCharsetNames; } /** * Test whether or not input filtering is enabled. * * @return true if input text will be filtered. * @stable ICU 3.4 * @see #enableInputFilter */ public boolean inputFilterEnabled() { return fStripTags; } /** * Enable filtering of input text. If filtering is enabled, * text within angle brackets ("<" and ">") will be removed * before detection. * * @param filter true to enable input text filtering. * @return The previous setting. * @stable ICU 3.4 */ public boolean enableInputFilter(boolean filter) { boolean previous = fStripTags; fStripTags = filter; return previous; } /* * MungeInput - after getting a set of raw input data to be analyzed, preprocess * it by removing what appears to be html markup. */ private void MungeInput() { int srci; int dsti = 0; byte b; boolean inMarkup = false; int openTags = 0; int badTags = 0; // // html / xml markup stripping. // quick and dirty, not 100% accurate, but hopefully good enough, statistically. // discard everything within < brackets > // Count how many total '<' and illegal (nested) '<' occur, so we can make some // guess as to whether the input was actually marked up at all. if (fStripTags) { for (srci = 0; srci < fRawLength && dsti < fInputBytes.length; srci++) { b = fRawInput[srci]; if (b == (byte) '<') { if (inMarkup) { badTags++; } inMarkup = true; openTags++; } if (!inMarkup) { fInputBytes[dsti++] = b; } if (b == (byte) '>') { inMarkup = false; } } fInputLen = dsti; } // // If it looks like this input wasn't marked up, or if it looks like it's // essentially nothing but markup abandon the markup stripping. // Detection will have to work on the unstripped input. // if (openTags < 5 || openTags / 5 < badTags || (fInputLen < 100 && fRawLength > 600)) { int limit = fRawLength; if (limit > kBufSize) { limit = kBufSize; } for (srci = 0; srci < limit; srci++) { fInputBytes[srci] = fRawInput[srci]; } fInputLen = srci; } // // Tally up the byte occurence statistics. // These are available for use by the various detectors. // Arrays.fill(fByteStats, (short) 0); for (srci = 0; srci < fInputLen; srci++) { int val = fInputBytes[srci] & 0x00ff; fByteStats[val]++; } fC1Bytes = false; for (int i = 0x80; i <= 0x9F; i += 1) { if (fByteStats[i] != 0) { fC1Bytes = true; break; } } } /* * The following items are accessed by individual CharsetRecongizers during * the recognition process * */ byte[] fInputBytes = // The text to be checked. Markup will have been new byte[kBufSize]; // removed if appropriate. int fInputLen; // Length of the byte data in fInputBytes. short[] fByteStats = // byte frequency statistics for the input text. new short[256]; // Value is percent, not absolute. // Value is rounded up, so zero really means zero occurences. boolean fC1Bytes = // True if any bytes in the range 0x80 - 0x9F are in the input; false; String fDeclaredEncoding; byte[] fRawInput; // Original, untouched input bytes. // If user gave us a byte array, this is it. // If user gave us a stream, it's read to a // buffer here. int fRawLength; // Length of data in fRawInput array. InputStream fInputStream; // User's input stream, or null if the user // gave us a byte array. // // Stuff private to CharsetDetector // private boolean fStripTags = // If true, setText() will strip tags from input text. false; private boolean[] fEnabledRecognizers; // If not null, active set of charset recognizers had // been changed from the default. The array index is // corresponding to ALL_RECOGNIZER. See setDetectableCharset(). private static class CSRecognizerInfo { CharsetRecognizer recognizer; boolean isDefaultEnabled; CSRecognizerInfo(CharsetRecognizer recognizer, boolean isDefaultEnabled) { this.recognizer = recognizer; this.isDefaultEnabled = isDefaultEnabled; } } /* * List of recognizers for all charsets known to the implementation. */ private static final List ALL_CS_RECOGNIZERS; static { List list = new ArrayList<>(); list.add(new CSRecognizerInfo(new CharsetRecog_UTF8(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_16_BE(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_16_LE(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_32_BE(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_32_LE(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_sjis(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_2022.CharsetRecog_2022JP(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_2022.CharsetRecog_2022CN(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_2022.CharsetRecog_2022KR(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_euc.CharsetRecog_gb_18030(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_euc.CharsetRecog_euc_jp(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_euc.CharsetRecog_euc_kr(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_big5(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_1(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_2(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_5_ru(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_6_ar(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_7_el(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_8_I_he(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_8_he(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_windows_1251(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_windows_1256(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_KOI8_R(), true)); list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_9_tr(), true)); // IBM 420/424 recognizers are disabled by default list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM424_he_rtl(), false)); list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM424_he_ltr(), false)); list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM420_ar_rtl(), false)); list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM420_ar_ltr(), false)); //noinspection Java9CollectionFactory ALL_CS_RECOGNIZERS = Collections.unmodifiableList(list); } /** * Get the names of charsets that can be recognized by this CharsetDetector instance. * * @return an array of the names of charsets that can be recognized by this CharsetDetector * instance. * @internal * @deprecated This API is ICU internal only. */ @Deprecated public String[] getDetectableCharsets() { List csnames = new ArrayList<>(ALL_CS_RECOGNIZERS.size()); for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) { CSRecognizerInfo rcinfo = ALL_CS_RECOGNIZERS.get(i); boolean active = (fEnabledRecognizers == null) ? rcinfo.isDefaultEnabled : fEnabledRecognizers[i]; if (active) { csnames.add(rcinfo.recognizer.getName()); } } return csnames.toArray(new String[0]); } /** * Enable or disable individual charset encoding. * A name of charset encoding must be included in the names returned by * {@link #getAllDetectableCharsets()}. * * @param encoding the name of charset encoding. * @param enabled true to enable, or false to disable the * charset encoding. * @return A reference to this CharsetDetector. * @throws IllegalArgumentException when the name of charset encoding is * not supported. * @internal * @deprecated This API is ICU internal only. */ @Deprecated public CharsetDetector setDetectableCharset(String encoding, boolean enabled) { int modIdx = -1; boolean isDefaultVal = false; for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) { CSRecognizerInfo csrinfo = ALL_CS_RECOGNIZERS.get(i); if (csrinfo.recognizer.getName().equals(encoding)) { modIdx = i; isDefaultVal = (csrinfo.isDefaultEnabled == enabled); break; } } if (modIdx < 0) { // No matching encoding found throw new IllegalArgumentException("Invalid encoding: " + "\"" + encoding + "\""); } if (fEnabledRecognizers == null && !isDefaultVal) { // Create an array storing the non default setting fEnabledRecognizers = new boolean[ALL_CS_RECOGNIZERS.size()]; // Initialize the array with default info for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) { fEnabledRecognizers[i] = ALL_CS_RECOGNIZERS.get(i).isDefaultEnabled; } } if (fEnabledRecognizers != null) { fEnabledRecognizers[modIdx] = enabled; } return this; } } ================================================ FILE: src/main/java/io/legado/app/lib/icu4j/CharsetMatch.java ================================================ // © 2016 and later: Unicode, Inc. and others. // License & terms of use: http://www.unicode.org/copyright.html /* * ****************************************************************************** * Copyright (C) 2005-2016, International Business Machines Corporation and * * others. All Rights Reserved. * * ****************************************************************************** */ package io.legado.app.lib.icu4j; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; /** * This class represents a charset that has been identified by a CharsetDetector * as a possible encoding for a set of input data. From an instance of this * class, you can ask for a confidence level in the charset identification, * or for Java Reader or String to access the original byte data in Unicode form. *

* Instances of this class are created only by CharsetDetectors. *

* Note: this class has a natural ordering that is inconsistent with equals. * The natural ordering is based on the match confidence value. * * @stable ICU 3.4 */ @SuppressWarnings({"JavaDoc", "unused"}) public class CharsetMatch implements Comparable { /** * Create a java.io.Reader for reading the Unicode character data corresponding * to the original byte data supplied to the Charset detect operation. *

* CAUTION: if the source of the byte data was an InputStream, a Reader * can be created for only one matching char set using this method. If more * than one charset needs to be tried, the caller will need to reset * the InputStream and create InputStreamReaders itself, based on the charset name. * * @return the Reader for the Unicode character data. * @stable ICU 3.4 */ public Reader getReader() { InputStream inputStream = fInputStream; if (inputStream == null) { inputStream = new ByteArrayInputStream(fRawInput, 0, fRawLength); } try { inputStream.reset(); return new InputStreamReader(inputStream, getName()); } catch (IOException e) { return null; } } /** * Create a Java String from Unicode character data corresponding * to the original byte data supplied to the Charset detect operation. * * @return a String created from the converted input data. * @stable ICU 3.4 */ public String getString() throws java.io.IOException { return getString(-1); } /** * Create a Java String from Unicode character data corresponding * to the original byte data supplied to the Charset detect operation. * The length of the returned string is limited to the specified size; * the string will be trunctated to this length if necessary. A limit value of * zero or less is ignored, and treated as no limit. * * @param maxLength The maximium length of the String to be created when the * source of the data is an input stream, or -1 for * unlimited length. * @return a String created from the converted input data. * @stable ICU 3.4 */ public String getString(int maxLength) throws java.io.IOException { String result; if (fInputStream != null) { StringBuilder sb = new StringBuilder(); char[] buffer = new char[1024]; Reader reader = getReader(); int max = maxLength < 0 ? Integer.MAX_VALUE : maxLength; int bytesRead; while ((bytesRead = reader.read(buffer, 0, Math.min(max, 1024))) >= 0) { sb.append(buffer, 0, bytesRead); max -= bytesRead; } reader.close(); return sb.toString(); } else { String name = getName(); /* * getName() may return a name with a suffix 'rtl' or 'ltr'. This cannot * be used to open a charset (e.g. IBM424_rtl). The ending '_rtl' or 'ltr' * should be stripped off before creating the string. */ int startSuffix = !name.contains("_rtl") ? name.indexOf("_ltr") : name.indexOf("_rtl"); if (startSuffix > 0) { name = name.substring(0, startSuffix); } result = new String(fRawInput, name); } return result; } /** * Get an indication of the confidence in the charset detected. * Confidence values range from 0-100, with larger numbers indicating * a better match of the input data to the characteristics of the * charset. * * @return the confidence in the charset match * @stable ICU 3.4 */ public int getConfidence() { return fConfidence; } /** * Get the name of the detected charset. * The name will be one that can be used with other APIs on the * platform that accept charset names. It is the "Canonical name" * as defined by the class java.nio.charset.Charset; for * charsets that are registered with the IANA charset registry, * this is the MIME-preferred registerd name. * * @return The name of the charset. * @stable ICU 3.4 * @see java.nio.charset.Charset * @see java.io.InputStreamReader */ public String getName() { return fCharsetName; } /** * Get the ISO code for the language of the detected charset. * * @return The ISO code for the language or null if the language cannot be determined. * @stable ICU 3.4 */ public String getLanguage() { return fLang; } /** * Compare to other CharsetMatch objects. * Comparison is based on the match confidence value, which * allows CharsetDetector.detectAll() to order its results. * * @param other the CharsetMatch object to compare against. * @return a negative integer, zero, or a positive integer as the * confidence level of this CharsetMatch * is less than, equal to, or greater than that of * the argument. * @throws ClassCastException if the argument is not a CharsetMatch. * @stable ICU 4.4 */ @Override public int compareTo(CharsetMatch other) { int compareResult = 0; if (this.fConfidence > other.fConfidence) { compareResult = 1; } else if (this.fConfidence < other.fConfidence) { compareResult = -1; } return compareResult; } /* * Constructor. Implementation internal */ CharsetMatch(CharsetDetector det, CharsetRecognizer rec, int conf) { fConfidence = conf; // The references to the original application input data must be copied out // of the charset recognizer to here, in case the application resets the // recognizer before using this CharsetMatch. if (det.fInputStream == null) { // We only want the existing input byte data if it came straight from the user, // not if is just the head of a stream. fRawInput = det.fRawInput; fRawLength = det.fRawLength; } fInputStream = det.fInputStream; fCharsetName = rec.getName(); fLang = rec.getLanguage(); } /* * Constructor. Implementation internal */ CharsetMatch(CharsetDetector det, CharsetRecognizer rec, int conf, String csName, String lang) { fConfidence = conf; // The references to the original application input data must be copied out // of the charset recognizer to here, in case the application resets the // recognizer before using this CharsetMatch. if (det.fInputStream == null) { // We only want the existing input byte data if it came straight from the user, // not if is just the head of a stream. fRawInput = det.fRawInput; fRawLength = det.fRawLength; } fInputStream = det.fInputStream; fCharsetName = csName; fLang = lang; } // // Private Data // private final int fConfidence; private byte[] fRawInput = null; // Original, untouched input bytes. // If user gave us a byte array, this is it. private int fRawLength; // Length of data in fRawInput array. private final InputStream fInputStream; // User's input stream, or null if the user // gave us a byte array. private final String fCharsetName; // The name of the charset this CharsetMatch // represents. Filled in by the recognizer. private final String fLang; // The language, if one was determined by // the recognizer during the detect operation. } ================================================ FILE: src/main/java/io/legado/app/lib/icu4j/CharsetRecog_2022.java ================================================ // © 2016 and later: Unicode, Inc. and others. // License & terms of use: http://www.unicode.org/copyright.html /* ******************************************************************************* * Copyright (C) 2005 - 2012, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* */ package io.legado.app.lib.icu4j; /** * class CharsetRecog_2022 part of the ICU charset detection imlementation. * This is a superclass for the individual detectors for * each of the detectable members of the ISO 2022 family * of encodings. *

* The separate classes are nested within this class. */ abstract class CharsetRecog_2022 extends CharsetRecognizer { /** * Matching function shared among the 2022 detectors JP, CN and KR * Counts up the number of legal an unrecognized escape sequences in * the sample of text, and computes a score based on the total number & * the proportion that fit the encoding. * * @param text the byte buffer containing text to analyse * @param textLen the size of the text in the byte. * @param escapeSequences the byte escape sequences to test for. * @return match quality, in the range of 0-100. */ int match(byte[] text, int textLen, byte[][] escapeSequences) { int i, j; int escN; int hits = 0; int misses = 0; int shifts = 0; int quality; scanInput: for (i = 0; i < textLen; i++) { if (text[i] == 0x1b) { checkEscapes: for (escN = 0; escN < escapeSequences.length; escN++) { byte[] seq = escapeSequences[escN]; if ((textLen - i) < seq.length) { continue; } for (j = 1; j < seq.length; j++) { if (seq[j] != text[i + j]) { continue checkEscapes; } } hits++; i += seq.length - 1; continue scanInput; } misses++; } if (text[i] == 0x0e || text[i] == 0x0f) { // Shift in/out shifts++; } } if (hits == 0) { return 0; } // // Initial quality is based on relative proportion of recongized vs. // unrecognized escape sequences. // All good: quality = 100; // half or less good: quality = 0; // linear inbetween. quality = (100 * hits - 100 * misses) / (hits + misses); // Back off quality if there were too few escape sequences seen. // Include shifts in this computation, so that KR does not get penalized // for having only a single Escape sequence, but many shifts. if (hits + shifts < 5) { quality -= (5 - (hits + shifts)) * 10; } if (quality < 0) { quality = 0; } return quality; } static class CharsetRecog_2022JP extends CharsetRecog_2022 { private final byte[][] escapeSequences = { {0x1b, 0x24, 0x28, 0x43}, // KS X 1001:1992 {0x1b, 0x24, 0x28, 0x44}, // JIS X 212-1990 {0x1b, 0x24, 0x40}, // JIS C 6226-1978 {0x1b, 0x24, 0x41}, // GB 2312-80 {0x1b, 0x24, 0x42}, // JIS X 208-1983 {0x1b, 0x26, 0x40}, // JIS X 208 1990, 1997 {0x1b, 0x28, 0x42}, // ASCII {0x1b, 0x28, 0x48}, // JIS-Roman {0x1b, 0x28, 0x49}, // Half-width katakana {0x1b, 0x28, 0x4a}, // JIS-Roman {0x1b, 0x2e, 0x41}, // ISO 8859-1 {0x1b, 0x2e, 0x46} // ISO 8859-7 }; @Override String getName() { return "ISO-2022-JP"; } @Override CharsetMatch match(CharsetDetector det) { int confidence = match(det.fInputBytes, det.fInputLen, escapeSequences); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } static class CharsetRecog_2022KR extends CharsetRecog_2022 { private final byte[][] escapeSequences = { {0x1b, 0x24, 0x29, 0x43} }; @Override String getName() { return "ISO-2022-KR"; } @Override CharsetMatch match(CharsetDetector det) { int confidence = match(det.fInputBytes, det.fInputLen, escapeSequences); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } static class CharsetRecog_2022CN extends CharsetRecog_2022 { private final byte[][] escapeSequences = { {0x1b, 0x24, 0x29, 0x41}, // GB 2312-80 {0x1b, 0x24, 0x29, 0x47}, // CNS 11643-1992 Plane 1 {0x1b, 0x24, 0x2A, 0x48}, // CNS 11643-1992 Plane 2 {0x1b, 0x24, 0x29, 0x45}, // ISO-IR-165 {0x1b, 0x24, 0x2B, 0x49}, // CNS 11643-1992 Plane 3 {0x1b, 0x24, 0x2B, 0x4A}, // CNS 11643-1992 Plane 4 {0x1b, 0x24, 0x2B, 0x4B}, // CNS 11643-1992 Plane 5 {0x1b, 0x24, 0x2B, 0x4C}, // CNS 11643-1992 Plane 6 {0x1b, 0x24, 0x2B, 0x4D}, // CNS 11643-1992 Plane 7 {0x1b, 0x4e}, // SS2 {0x1b, 0x4f}, // SS3 }; @Override String getName() { return "ISO-2022-CN"; } @Override CharsetMatch match(CharsetDetector det) { int confidence = match(det.fInputBytes, det.fInputLen, escapeSequences); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } } ================================================ FILE: src/main/java/io/legado/app/lib/icu4j/CharsetRecog_UTF8.java ================================================ // © 2016 and later: Unicode, Inc. and others. // License & terms of use: http://www.unicode.org/copyright.html /** * ****************************************************************************** * Copyright (C) 2005 - 2014, International Business Machines Corporation and * * others. All Rights Reserved. * * ****************************************************************************** */ package io.legado.app.lib.icu4j; /** * Charset recognizer for UTF-8 */ class CharsetRecog_UTF8 extends CharsetRecognizer { @Override String getName() { return "UTF-8"; } /* (non-Javadoc) * @see com.ibm.icu.text.CharsetRecognizer#match(com.ibm.icu.text.CharsetDetector) */ @Override CharsetMatch match(CharsetDetector det) { boolean hasBOM = false; int numValid = 0; int numInvalid = 0; byte[] input = det.fRawInput; int i; int trailBytes = 0; int confidence; if (det.fRawLength >= 3 && (input[0] & 0xFF) == 0xef && (input[1] & 0xFF) == 0xbb && (input[2] & 0xFF) == 0xbf) { hasBOM = true; } // Scan for multi-byte sequences for (i = 0; i < det.fRawLength; i++) { int b = input[i]; if ((b & 0x80) == 0) { continue; // ASCII } // Hi bit on char found. Figure out how long the sequence should be if ((b & 0x0e0) == 0x0c0) { trailBytes = 1; } else if ((b & 0x0f0) == 0x0e0) { trailBytes = 2; } else if ((b & 0x0f8) == 0xf0) { trailBytes = 3; } else { numInvalid++; continue; } // Verify that we've got the right number of trail bytes in the sequence for (; ; ) { i++; if (i >= det.fRawLength) { break; } b = input[i]; if ((b & 0xc0) != 0x080) { numInvalid++; break; } if (--trailBytes == 0) { numValid++; break; } } } // Cook up some sort of confidence score, based on presense of a BOM // and the existence of valid and/or invalid multi-byte sequences. confidence = 0; if (hasBOM && numInvalid == 0) { confidence = 100; } else if (hasBOM && numValid > numInvalid * 10) { confidence = 80; } else if (numValid > 3 && numInvalid == 0) { confidence = 100; } else if (numValid > 0 && numInvalid == 0) { confidence = 80; } else if (numValid == 0 && numInvalid == 0) { // Plain ASCII. Confidence must be > 10, it's more likely than UTF-16, which // accepts ASCII with confidence = 10. // TODO: add plain ASCII as an explicitly detected type. confidence = 15; } else if (numValid > numInvalid * 10) { // Probably corruput utf-8 data. Valid sequences aren't likely by chance. confidence = 25; } return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } ================================================ FILE: src/main/java/io/legado/app/lib/icu4j/CharsetRecog_Unicode.java ================================================ // © 2016 and later: Unicode, Inc. and others. // License & terms of use: http://www.unicode.org/copyright.html /* ******************************************************************************* * Copyright (C) 1996-2013, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* * */ package io.legado.app.lib.icu4j; /** * This class matches UTF-16 and UTF-32, both big- and little-endian. The * BOM will be used if it is present. */ abstract class CharsetRecog_Unicode extends CharsetRecognizer { /* (non-Javadoc) * @see com.ibm.icu.text.CharsetRecognizer#getName() */ @Override abstract String getName(); /* (non-Javadoc) * @see com.ibm.icu.text.CharsetRecognizer#match(com.ibm.icu.text.CharsetDetector) */ @Override abstract CharsetMatch match(CharsetDetector det); static int codeUnit16FromBytes(byte hi, byte lo) { return ((hi & 0xff) << 8) | (lo & 0xff); } // UTF-16 confidence calculation. Very simple minded, but better than nothing. // Any 8 bit non-control characters bump the confidence up. These have a zero high byte, // and are very likely to be UTF-16, although they could also be part of a UTF-32 code. // NULs are a contra-indication, they will appear commonly if the actual encoding is UTF-32. // NULs should be rare in actual text. static int adjustConfidence(int codeUnit, int confidence) { if (codeUnit == 0) { confidence -= 10; } else if ((codeUnit >= 0x20 && codeUnit <= 0xff) || codeUnit == 0x0a) { confidence += 10; } if (confidence < 0) { confidence = 0; } else if (confidence > 100) { confidence = 100; } return confidence; } static class CharsetRecog_UTF_16_BE extends CharsetRecog_Unicode { @Override String getName() { return "UTF-16BE"; } @Override CharsetMatch match(CharsetDetector det) { byte[] input = det.fRawInput; int confidence = 10; int bytesToCheck = Math.min(input.length, 30); for (int charIndex = 0; charIndex < bytesToCheck - 1; charIndex += 2) { int codeUnit = codeUnit16FromBytes(input[charIndex], input[charIndex + 1]); if (charIndex == 0 && codeUnit == 0xFEFF) { confidence = 100; break; } confidence = adjustConfidence(codeUnit, confidence); if (confidence == 0 || confidence == 100) { break; } } if (bytesToCheck < 4 && confidence < 100) { confidence = 0; } if (confidence > 0) { return new CharsetMatch(det, this, confidence); } return null; } } static class CharsetRecog_UTF_16_LE extends CharsetRecog_Unicode { @Override String getName() { return "UTF-16LE"; } @Override CharsetMatch match(CharsetDetector det) { byte[] input = det.fRawInput; int confidence = 10; int bytesToCheck = Math.min(input.length, 30); for (int charIndex = 0; charIndex < bytesToCheck - 1; charIndex += 2) { int codeUnit = codeUnit16FromBytes(input[charIndex + 1], input[charIndex]); if (charIndex == 0 && codeUnit == 0xFEFF) { confidence = 100; break; } confidence = adjustConfidence(codeUnit, confidence); if (confidence == 0 || confidence == 100) { break; } } if (bytesToCheck < 4 && confidence < 100) { confidence = 0; } if (confidence > 0) { return new CharsetMatch(det, this, confidence); } return null; } } static abstract class CharsetRecog_UTF_32 extends CharsetRecog_Unicode { abstract int getChar(byte[] input, int index); @Override abstract String getName(); @Override CharsetMatch match(CharsetDetector det) { byte[] input = det.fRawInput; int limit = (det.fRawLength / 4) * 4; int numValid = 0; int numInvalid = 0; boolean hasBOM = false; int confidence = 0; if (limit == 0) { return null; } if (getChar(input, 0) == 0x0000FEFF) { hasBOM = true; } for (int i = 0; i < limit; i += 4) { int ch = getChar(input, i); if (ch < 0 || ch >= 0x10FFFF || (ch >= 0xD800 && ch <= 0xDFFF)) { numInvalid += 1; } else { numValid += 1; } } // Cook up some sort of confidence score, based on presence of a BOM // and the existence of valid and/or invalid multi-byte sequences. if (hasBOM && numInvalid == 0) { confidence = 100; } else if (hasBOM && numValid > numInvalid * 10) { confidence = 80; } else if (numValid > 3 && numInvalid == 0) { confidence = 100; } else if (numValid > 0 && numInvalid == 0) { confidence = 80; } else if (numValid > numInvalid * 10) { // Probably corrupt UTF-32BE data. Valid sequences aren't likely by chance. confidence = 25; } return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } static class CharsetRecog_UTF_32_BE extends CharsetRecog_UTF_32 { @Override int getChar(byte[] input, int index) { return (input[index + 0] & 0xFF) << 24 | (input[index + 1] & 0xFF) << 16 | (input[index + 2] & 0xFF) << 8 | (input[index + 3] & 0xFF); } @Override String getName() { return "UTF-32BE"; } } static class CharsetRecog_UTF_32_LE extends CharsetRecog_UTF_32 { @Override int getChar(byte[] input, int index) { return (input[index + 3] & 0xFF) << 24 | (input[index + 2] & 0xFF) << 16 | (input[index + 1] & 0xFF) << 8 | (input[index + 0] & 0xFF); } @Override String getName() { return "UTF-32LE"; } } } ================================================ FILE: src/main/java/io/legado/app/lib/icu4j/CharsetRecog_mbcs.java ================================================ // © 2016 and later: Unicode, Inc. and others. // License & terms of use: http://www.unicode.org/copyright.html /* **************************************************************************** * Copyright (C) 2005-2012, International Business Machines Corporation and * * others. All Rights Reserved. * **************************************************************************** * */ package io.legado.app.lib.icu4j; import java.util.Arrays; /** * CharsetRecognizer implemenation for Asian - double or multi-byte - charsets. * Match is determined mostly by the input data adhering to the * encoding scheme for the charset, and, optionally, * frequency-of-occurence of characters. *

* Instances of this class are singletons, one per encoding * being recognized. They are created in the main * CharsetDetector class and kept in the global list of available * encodings to be checked. The specific encoding being recognized * is determined by subclass. */ abstract class CharsetRecog_mbcs extends CharsetRecognizer { /** * Get the IANA name of this charset. * * @return the charset name. */ @Override abstract String getName(); /** * Test the match of this charset with the input text data * which is obtained via the CharsetDetector object. * * @param det The CharsetDetector, which contains the input text * to be checked for being in this charset. * @return Two values packed into one int (Damn java, anyhow) *
* bits 0-7: the match confidence, ranging from 0-100 *
* bits 8-15: The match reason, an enum-like value. */ int match(CharsetDetector det, int[] commonChars) { @SuppressWarnings("unused") int singleByteCharCount = 0; //TODO Do we really need this? int doubleByteCharCount = 0; int commonCharCount = 0; int badCharCount = 0; int totalCharCount = 0; int confidence = 0; iteratedChar iter = new iteratedChar(); detectBlock: { for (iter.reset(); nextChar(iter, det); ) { totalCharCount++; if (iter.error) { badCharCount++; } else { long cv = iter.charValue & 0xFFFFFFFFL; if (cv <= 0xff) { singleByteCharCount++; } else { doubleByteCharCount++; if (commonChars != null) { // NOTE: This assumes that there are no 4-byte common chars. if (Arrays.binarySearch(commonChars, (int) cv) >= 0) { commonCharCount++; } } } } if (badCharCount >= 2 && badCharCount * 5 >= doubleByteCharCount) { // Bail out early if the byte data is not matching the encoding scheme. break detectBlock; } } if (doubleByteCharCount <= 10 && badCharCount == 0) { // Not many multi-byte chars. if (doubleByteCharCount == 0 && totalCharCount < 10) { // There weren't any multibyte sequences, and there was a low density of non-ASCII single bytes. // We don't have enough data to have any confidence. // Statistical analysis of single byte non-ASCII charcters would probably help here. confidence = 0; } else { // ASCII or ISO file? It's probably not our encoding, // but is not incompatible with our encoding, so don't give it a zero. confidence = 10; } break detectBlock; } // // No match if there are too many characters that don't fit the encoding scheme. // (should we have zero tolerance for these?) // if (doubleByteCharCount < 20 * badCharCount) { confidence = 0; break detectBlock; } if (commonChars == null) { // We have no statistics on frequently occuring characters. // Assess confidence purely on having a reasonable number of // multi-byte characters (the more the better confidence = 30 + doubleByteCharCount - 20 * badCharCount; if (confidence > 100) { confidence = 100; } } else { // // Frequency of occurence statistics exist. // double maxVal = Math.log((float) doubleByteCharCount / 4); double scaleFactor = 90.0 / maxVal; confidence = (int) (Math.log(commonCharCount + 1) * scaleFactor + 10); confidence = Math.min(confidence, 100); } } // end of detectBlock: return confidence; } // "Character" iterated character class. // Recognizers for specific mbcs encodings make their "characters" available // by providing a nextChar() function that fills in an instance of iteratedChar // with the next char from the input. // The returned characters are not converted to Unicode, but remain as the raw // bytes (concatenated into an int) from the codepage data. // // For Asian charsets, use the raw input rather than the input that has been // stripped of markup. Detection only considers multi-byte chars, effectively // stripping markup anyway, and double byte chars do occur in markup too. // static class iteratedChar { int charValue = 0; // 1-4 bytes from the raw input data int nextIndex = 0; boolean error = false; boolean done = false; void reset() { charValue = 0; nextIndex = 0; error = false; done = false; } int nextByte(CharsetDetector det) { if (nextIndex >= det.fRawLength) { done = true; return -1; } return det.fRawInput[nextIndex++] & 0x00ff; } } /** * Get the next character (however many bytes it is) from the input data * Subclasses for specific charset encodings must implement this function * to get characters according to the rules of their encoding scheme. *

* This function is not a method of class iteratedChar only because * that would require a lot of extra derived classes, which is awkward. * * @param it The iteratedChar "struct" into which the returned char is placed. * @param det The charset detector, which is needed to get at the input byte data * being iterated over. * @return True if a character was returned, false at end of input. */ abstract boolean nextChar(iteratedChar it, CharsetDetector det); /** * Shift-JIS charset recognizer. */ static class CharsetRecog_sjis extends CharsetRecog_mbcs { static int[] commonChars = // TODO: This set of data comes from the character frequency- // of-occurence analysis tool. The data needs to be moved // into a resource and loaded from there. {0x8140, 0x8141, 0x8142, 0x8145, 0x815b, 0x8169, 0x816a, 0x8175, 0x8176, 0x82a0, 0x82a2, 0x82a4, 0x82a9, 0x82aa, 0x82ab, 0x82ad, 0x82af, 0x82b1, 0x82b3, 0x82b5, 0x82b7, 0x82bd, 0x82be, 0x82c1, 0x82c4, 0x82c5, 0x82c6, 0x82c8, 0x82c9, 0x82cc, 0x82cd, 0x82dc, 0x82e0, 0x82e7, 0x82e8, 0x82e9, 0x82ea, 0x82f0, 0x82f1, 0x8341, 0x8343, 0x834e, 0x834f, 0x8358, 0x835e, 0x8362, 0x8367, 0x8375, 0x8376, 0x8389, 0x838a, 0x838b, 0x838d, 0x8393, 0x8e96, 0x93fa, 0x95aa}; @Override boolean nextChar(iteratedChar it, CharsetDetector det) { it.error = false; int firstByte; firstByte = it.charValue = it.nextByte(det); if (firstByte < 0) { return false; } if (firstByte <= 0x7f || (firstByte > 0xa0 && firstByte <= 0xdf)) { return true; } int secondByte = it.nextByte(det); if (secondByte < 0) { return false; } it.charValue = (firstByte << 8) | secondByte; if (!((secondByte >= 0x40 && secondByte <= 0x7f) || (secondByte >= 0x80 && secondByte <= 0xff))) { // Illegal second byte value. it.error = true; } return true; } @Override CharsetMatch match(CharsetDetector det) { int confidence = match(det, commonChars); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } @Override String getName() { return "Shift_JIS"; } @Override public String getLanguage() { return "ja"; } } /** * Big5 charset recognizer. */ static class CharsetRecog_big5 extends CharsetRecog_mbcs { static int[] commonChars = // TODO: This set of data comes from the character frequency- // of-occurence analysis tool. The data needs to be moved // into a resource and loaded from there. {0xa140, 0xa141, 0xa142, 0xa143, 0xa147, 0xa149, 0xa175, 0xa176, 0xa440, 0xa446, 0xa447, 0xa448, 0xa451, 0xa454, 0xa457, 0xa464, 0xa46a, 0xa46c, 0xa477, 0xa4a3, 0xa4a4, 0xa4a7, 0xa4c1, 0xa4ce, 0xa4d1, 0xa4df, 0xa4e8, 0xa4fd, 0xa540, 0xa548, 0xa558, 0xa569, 0xa5cd, 0xa5e7, 0xa657, 0xa661, 0xa662, 0xa668, 0xa670, 0xa6a8, 0xa6b3, 0xa6b9, 0xa6d3, 0xa6db, 0xa6e6, 0xa6f2, 0xa740, 0xa751, 0xa759, 0xa7da, 0xa8a3, 0xa8a5, 0xa8ad, 0xa8d1, 0xa8d3, 0xa8e4, 0xa8fc, 0xa9c0, 0xa9d2, 0xa9f3, 0xaa6b, 0xaaba, 0xaabe, 0xaacc, 0xaafc, 0xac47, 0xac4f, 0xacb0, 0xacd2, 0xad59, 0xaec9, 0xafe0, 0xb0ea, 0xb16f, 0xb2b3, 0xb2c4, 0xb36f, 0xb44c, 0xb44e, 0xb54c, 0xb5a5, 0xb5bd, 0xb5d0, 0xb5d8, 0xb671, 0xb7ed, 0xb867, 0xb944, 0xbad8, 0xbb44, 0xbba1, 0xbdd1, 0xc2c4, 0xc3b9, 0xc440, 0xc45f}; @Override boolean nextChar(iteratedChar it, CharsetDetector det) { it.error = false; int firstByte; firstByte = it.charValue = it.nextByte(det); if (firstByte < 0) { return false; } if (firstByte <= 0x7f || firstByte == 0xff) { // single byte character. return true; } int secondByte = it.nextByte(det); if (secondByte < 0) { return false; } it.charValue = (it.charValue << 8) | secondByte; if (secondByte < 0x40 || secondByte == 0x7f || secondByte == 0xff) { it.error = true; } return true; } @Override CharsetMatch match(CharsetDetector det) { int confidence = match(det, commonChars); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } @Override String getName() { return "Big5"; } @Override public String getLanguage() { return "zh"; } } /** * EUC charset recognizers. One abstract class that provides the common function * for getting the next character according to the EUC encoding scheme, * and nested derived classes for EUC_KR, EUC_JP, EUC_CN. */ abstract static class CharsetRecog_euc extends CharsetRecog_mbcs { /* * (non-Javadoc) * Get the next character value for EUC based encodings. * Character "value" is simply the raw bytes that make up the character * packed into an int. */ @Override boolean nextChar(iteratedChar it, CharsetDetector det) { it.error = false; int firstByte; int secondByte; int thirdByte; //int fourthByte = 0; buildChar: { firstByte = it.charValue = it.nextByte(det); if (firstByte < 0) { // Ran off the end of the input data it.done = true; break buildChar; } if (firstByte <= 0x8d) { // single byte char break buildChar; } secondByte = it.nextByte(det); it.charValue = (it.charValue << 8) | secondByte; if (firstByte >= 0xA1 && firstByte <= 0xfe) { // Two byte Char if (secondByte < 0xa1) { it.error = true; } break buildChar; } if (firstByte == 0x8e) { // Code Set 2. // In EUC-JP, total char size is 2 bytes, only one byte of actual char value. // In EUC-TW, total char size is 4 bytes, three bytes contribute to char value. // We don't know which we've got. // Treat it like EUC-JP. If the data really was EUC-TW, the following two // bytes will look like a well formed 2 byte char. if (secondByte < 0xa1) { it.error = true; } break buildChar; } if (firstByte == 0x8f) { // Code set 3. // Three byte total char size, two bytes of actual char value. thirdByte = it.nextByte(det); it.charValue = (it.charValue << 8) | thirdByte; if (thirdByte < 0xa1) { it.error = true; } } } return (!it.done); } /** * The charset recognize for EUC-JP. A singleton instance of this class * is created and kept by the public CharsetDetector class */ static class CharsetRecog_euc_jp extends CharsetRecog_euc { static int[] commonChars = // TODO: This set of data comes from the character frequency- // of-occurence analysis tool. The data needs to be moved // into a resource and loaded from there. {0xa1a1, 0xa1a2, 0xa1a3, 0xa1a6, 0xa1bc, 0xa1ca, 0xa1cb, 0xa1d6, 0xa1d7, 0xa4a2, 0xa4a4, 0xa4a6, 0xa4a8, 0xa4aa, 0xa4ab, 0xa4ac, 0xa4ad, 0xa4af, 0xa4b1, 0xa4b3, 0xa4b5, 0xa4b7, 0xa4b9, 0xa4bb, 0xa4bd, 0xa4bf, 0xa4c0, 0xa4c1, 0xa4c3, 0xa4c4, 0xa4c6, 0xa4c7, 0xa4c8, 0xa4c9, 0xa4ca, 0xa4cb, 0xa4ce, 0xa4cf, 0xa4d0, 0xa4de, 0xa4df, 0xa4e1, 0xa4e2, 0xa4e4, 0xa4e8, 0xa4e9, 0xa4ea, 0xa4eb, 0xa4ec, 0xa4ef, 0xa4f2, 0xa4f3, 0xa5a2, 0xa5a3, 0xa5a4, 0xa5a6, 0xa5a7, 0xa5aa, 0xa5ad, 0xa5af, 0xa5b0, 0xa5b3, 0xa5b5, 0xa5b7, 0xa5b8, 0xa5b9, 0xa5bf, 0xa5c3, 0xa5c6, 0xa5c7, 0xa5c8, 0xa5c9, 0xa5cb, 0xa5d0, 0xa5d5, 0xa5d6, 0xa5d7, 0xa5de, 0xa5e0, 0xa5e1, 0xa5e5, 0xa5e9, 0xa5ea, 0xa5eb, 0xa5ec, 0xa5ed, 0xa5f3, 0xb8a9, 0xb9d4, 0xbaee, 0xbbc8, 0xbef0, 0xbfb7, 0xc4ea, 0xc6fc, 0xc7bd, 0xcab8, 0xcaf3, 0xcbdc, 0xcdd1}; @Override String getName() { return "EUC-JP"; } @Override CharsetMatch match(CharsetDetector det) { int confidence = match(det, commonChars); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } @Override public String getLanguage() { return "ja"; } } /** * The charset recognize for EUC-KR. A singleton instance of this class * is created and kept by the public CharsetDetector class */ static class CharsetRecog_euc_kr extends CharsetRecog_euc { static int[] commonChars = // TODO: This set of data comes from the character frequency- // of-occurence analysis tool. The data needs to be moved // into a resource and loaded from there. {0xb0a1, 0xb0b3, 0xb0c5, 0xb0cd, 0xb0d4, 0xb0e6, 0xb0ed, 0xb0f8, 0xb0fa, 0xb0fc, 0xb1b8, 0xb1b9, 0xb1c7, 0xb1d7, 0xb1e2, 0xb3aa, 0xb3bb, 0xb4c2, 0xb4cf, 0xb4d9, 0xb4eb, 0xb5a5, 0xb5b5, 0xb5bf, 0xb5c7, 0xb5e9, 0xb6f3, 0xb7af, 0xb7c2, 0xb7ce, 0xb8a6, 0xb8ae, 0xb8b6, 0xb8b8, 0xb8bb, 0xb8e9, 0xb9ab, 0xb9ae, 0xb9cc, 0xb9ce, 0xb9fd, 0xbab8, 0xbace, 0xbad0, 0xbaf1, 0xbbe7, 0xbbf3, 0xbbfd, 0xbcad, 0xbcba, 0xbcd2, 0xbcf6, 0xbdba, 0xbdc0, 0xbdc3, 0xbdc5, 0xbec6, 0xbec8, 0xbedf, 0xbeee, 0xbef8, 0xbefa, 0xbfa1, 0xbfa9, 0xbfc0, 0xbfe4, 0xbfeb, 0xbfec, 0xbff8, 0xc0a7, 0xc0af, 0xc0b8, 0xc0ba, 0xc0bb, 0xc0bd, 0xc0c7, 0xc0cc, 0xc0ce, 0xc0cf, 0xc0d6, 0xc0da, 0xc0e5, 0xc0fb, 0xc0fc, 0xc1a4, 0xc1a6, 0xc1b6, 0xc1d6, 0xc1df, 0xc1f6, 0xc1f8, 0xc4a1, 0xc5cd, 0xc6ae, 0xc7cf, 0xc7d1, 0xc7d2, 0xc7d8, 0xc7e5, 0xc8ad}; @Override String getName() { return "EUC-KR"; } @Override CharsetMatch match(CharsetDetector det) { int confidence = match(det, commonChars); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } @Override public String getLanguage() { return "ko"; } } } /** * GB-18030 recognizer. Uses simplified Chinese statistics. */ static class CharsetRecog_gb_18030 extends CharsetRecog_mbcs { /* * (non-Javadoc) * Get the next character value for EUC based encodings. * Character "value" is simply the raw bytes that make up the character * packed into an int. */ @Override boolean nextChar(iteratedChar it, CharsetDetector det) { it.error = false; int firstByte; int secondByte; int thirdByte; int fourthByte; buildChar: { firstByte = it.charValue = it.nextByte(det); if (firstByte < 0) { // Ran off the end of the input data it.done = true; break buildChar; } if (firstByte <= 0x80) { // single byte char break buildChar; } secondByte = it.nextByte(det); it.charValue = (it.charValue << 8) | secondByte; if (firstByte >= 0x81 && firstByte <= 0xFE) { // Two byte Char if ((secondByte >= 0x40 && secondByte <= 0x7E) || (secondByte >= 80 && secondByte <= 0xFE)) { break buildChar; } // Four byte char if (secondByte >= 0x30 && secondByte <= 0x39) { thirdByte = it.nextByte(det); if (thirdByte >= 0x81 && thirdByte <= 0xFE) { fourthByte = it.nextByte(det); if (fourthByte >= 0x30 && fourthByte <= 0x39) { it.charValue = (it.charValue << 16) | (thirdByte << 8) | fourthByte; break buildChar; } } } it.error = true; } } return (!it.done); } static int[] commonChars = // TODO: This set of data comes from the character frequency- // of-occurence analysis tool. The data needs to be moved // into a resource and loaded from there. {0xa1a1, 0xa1a2, 0xa1a3, 0xa1a4, 0xa1b0, 0xa1b1, 0xa1f1, 0xa1f3, 0xa3a1, 0xa3ac, 0xa3ba, 0xb1a8, 0xb1b8, 0xb1be, 0xb2bb, 0xb3c9, 0xb3f6, 0xb4f3, 0xb5bd, 0xb5c4, 0xb5e3, 0xb6af, 0xb6d4, 0xb6e0, 0xb7a2, 0xb7a8, 0xb7bd, 0xb7d6, 0xb7dd, 0xb8b4, 0xb8df, 0xb8f6, 0xb9ab, 0xb9c9, 0xb9d8, 0xb9fa, 0xb9fd, 0xbacd, 0xbba7, 0xbbd6, 0xbbe1, 0xbbfa, 0xbcbc, 0xbcdb, 0xbcfe, 0xbdcc, 0xbecd, 0xbedd, 0xbfb4, 0xbfc6, 0xbfc9, 0xc0b4, 0xc0ed, 0xc1cb, 0xc2db, 0xc3c7, 0xc4dc, 0xc4ea, 0xc5cc, 0xc6f7, 0xc7f8, 0xc8ab, 0xc8cb, 0xc8d5, 0xc8e7, 0xc9cf, 0xc9fa, 0xcab1, 0xcab5, 0xcac7, 0xcad0, 0xcad6, 0xcaf5, 0xcafd, 0xccec, 0xcdf8, 0xceaa, 0xcec4, 0xced2, 0xcee5, 0xcfb5, 0xcfc2, 0xcfd6, 0xd0c2, 0xd0c5, 0xd0d0, 0xd0d4, 0xd1a7, 0xd2aa, 0xd2b2, 0xd2b5, 0xd2bb, 0xd2d4, 0xd3c3, 0xd3d0, 0xd3fd, 0xd4c2, 0xd4da, 0xd5e2, 0xd6d0}; @Override String getName() { return "GB18030"; } @Override CharsetMatch match(CharsetDetector det) { int confidence = match(det, commonChars); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } @Override public String getLanguage() { return "zh"; } } } ================================================ FILE: src/main/java/io/legado/app/lib/icu4j/CharsetRecog_sbcs.java ================================================ // © 2016 and later: Unicode, Inc. and others. // License & terms of use: http://www.unicode.org/copyright.html /* **************************************************************************** * Copyright (C) 2005-2013, International Business Machines Corporation and * * others. All Rights Reserved. * ************************************************************************** * * */ package io.legado.app.lib.icu4j; /** * This class recognizes single-byte encodings. Because the encoding scheme is so * simple, language statistics are used to do the matching. */ abstract class CharsetRecog_sbcs extends CharsetRecognizer { /* (non-Javadoc) * @see com.ibm.icu.text.CharsetRecognizer#getName() */ @Override abstract String getName(); static class NGramParser { // private static final int N_GRAM_SIZE = 3; private static final int N_GRAM_MASK = 0xFFFFFF; protected int byteIndex = 0; private int ngram = 0; private final int[] ngramList; protected byte[] byteMap; private int ngramCount; private int hitCount; protected byte spaceChar; public NGramParser(int[] theNgramList, byte[] theByteMap) { ngramList = theNgramList; byteMap = theByteMap; ngram = 0; ngramCount = hitCount = 0; } /* * Binary search for value in table, which must have exactly 64 entries. */ private static int search(int[] table, int value) { int index = 0; if (table[index + 32] <= value) { index += 32; } if (table[index + 16] <= value) { index += 16; } if (table[index + 8] <= value) { index += 8; } if (table[index + 4] <= value) { index += 4; } if (table[index + 2] <= value) { index += 2; } if (table[index + 1] <= value) { index += 1; } if (table[index] > value) { index -= 1; } if (index < 0 || table[index] != value) { return -1; } return index; } private void lookup(int thisNgram) { ngramCount += 1; if (search(ngramList, thisNgram) >= 0) { hitCount += 1; } } protected void addByte(int b) { ngram = ((ngram << 8) + (b & 0xFF)) & N_GRAM_MASK; lookup(ngram); } private int nextByte(CharsetDetector det) { if (byteIndex >= det.fInputLen) { return -1; } return det.fInputBytes[byteIndex++] & 0xFF; } protected void parseCharacters(CharsetDetector det) { int b; boolean ignoreSpace = false; while ((b = nextByte(det)) >= 0) { byte mb = byteMap[b]; // TODO: 0x20 might not be a space in all character sets... if (mb != 0) { if (!(mb == spaceChar && ignoreSpace)) { addByte(mb); } ignoreSpace = (mb == spaceChar); } } } public int parse(CharsetDetector det) { return parse(det, (byte) 0x20); } public int parse(CharsetDetector det, byte spaceCh) { this.spaceChar = spaceCh; parseCharacters(det); // TODO: Is this OK? The buffer could have ended in the middle of a word... addByte(spaceChar); double rawPercent = (double) hitCount / (double) ngramCount; // if (rawPercent <= 2.0) { // return 0; // } // TODO - This is a bit of a hack to take care of a case // were we were getting a confidence of 135... if (rawPercent > 0.33) { return 98; } return (int) (rawPercent * 300.0); } } static class NGramParser_IBM420 extends NGramParser { private byte alef = 0x00; protected static byte[] unshapeMap = { /* -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F */ /* 0- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 1- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 2- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 3- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 4- */ (byte) 0x40, (byte) 0x40, (byte) 0x42, (byte) 0x42, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x47, (byte) 0x49, (byte) 0x4A, (byte) 0x4B, (byte) 0x4C, (byte) 0x4D, (byte) 0x4E, (byte) 0x4F, /* 5- */ (byte) 0x50, (byte) 0x49, (byte) 0x52, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x56, (byte) 0x58, (byte) 0x58, (byte) 0x5A, (byte) 0x5B, (byte) 0x5C, (byte) 0x5D, (byte) 0x5E, (byte) 0x5F, /* 6- */ (byte) 0x60, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x63, (byte) 0x65, (byte) 0x65, (byte) 0x67, (byte) 0x67, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, /* 7- */ (byte) 0x69, (byte) 0x71, (byte) 0x71, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x77, (byte) 0x79, (byte) 0x7A, (byte) 0x7B, (byte) 0x7C, (byte) 0x7D, (byte) 0x7E, (byte) 0x7F, /* 8- */ (byte) 0x80, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x80, (byte) 0x8B, (byte) 0x8B, (byte) 0x8D, (byte) 0x8D, (byte) 0x8F, /* 9- */ (byte) 0x90, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x9A, (byte) 0x9A, (byte) 0x9A, (byte) 0x9A, (byte) 0x9E, (byte) 0x9E, /* A- */ (byte) 0x9E, (byte) 0xA1, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0x9E, (byte) 0xAB, (byte) 0xAB, (byte) 0xAD, (byte) 0xAD, (byte) 0xAF, /* B- */ (byte) 0xAF, (byte) 0xB1, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0xB6, (byte) 0xB7, (byte) 0xB8, (byte) 0xB9, (byte) 0xB1, (byte) 0xBB, (byte) 0xBB, (byte) 0xBD, (byte) 0xBD, (byte) 0xBF, /* C- */ (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xBF, (byte) 0xCC, (byte) 0xBF, (byte) 0xCE, (byte) 0xCF, /* D- */ (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDA, (byte) 0xDC, (byte) 0xDC, (byte) 0xDC, (byte) 0xDF, /* E- */ (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, /* F- */ (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, }; public NGramParser_IBM420(int[] theNgramList, byte[] theByteMap) { super(theNgramList, theByteMap); } private byte isLamAlef(byte b) { if (b == (byte) 0xb2 || b == (byte) 0xb3) { return (byte) 0x47; } else if (b == (byte) 0xb4 || b == (byte) 0xb5) { return (byte) 0x49; } else if (b == (byte) 0xb8 || b == (byte) 0xb9) { return (byte) 0x56; } else return (byte) 0x00; } /* * Arabic shaping needs to be done manually. Cannot call ArabicShaping class * because CharsetDetector is dealing with bytes not Unicode code points. We could * convert the bytes to Unicode code points but that would leave us dependent * on CharsetICU which we try to avoid. IBM420 converter amongst different versions * of JDK can produce different results and therefore is also avoided. */ private int nextByte(CharsetDetector det) { if (byteIndex >= det.fInputLen || det.fInputBytes[byteIndex] == 0) { return -1; } int next; alef = isLamAlef(det.fInputBytes[byteIndex]); if (alef != (byte) 0x00) next = 0xB1 & 0xFF; else next = unshapeMap[det.fInputBytes[byteIndex] & 0xFF] & 0xFF; byteIndex++; return next; } @Override protected void parseCharacters(CharsetDetector det) { int b; boolean ignoreSpace = false; while ((b = nextByte(det)) >= 0) { byte mb = byteMap[b]; // TODO: 0x20 might not be a space in all character sets... if (mb != 0) { if (!(mb == spaceChar && ignoreSpace)) { addByte(mb); } ignoreSpace = (mb == spaceChar); } if (alef != (byte) 0x00) { mb = byteMap[alef & 0xFF]; // TODO: 0x20 might not be a space in all character sets... if (mb != 0) { if (!(mb == spaceChar && ignoreSpace)) { addByte(mb); } ignoreSpace = (mb == spaceChar); } } } } } int match(CharsetDetector det, int[] ngrams, byte[] byteMap) { return match(det, ngrams, byteMap, (byte) 0x20); } int match(CharsetDetector det, int[] ngrams, byte[] byteMap, byte spaceChar) { NGramParser parser = new NGramParser(ngrams, byteMap); return parser.parse(det, spaceChar); } @SuppressWarnings("SameParameterValue") int matchIBM420(CharsetDetector det, int[] ngrams, byte[] byteMap, byte spaceChar) { NGramParser_IBM420 parser = new NGramParser_IBM420(ngrams, byteMap); return parser.parse(det, spaceChar); } static class NGramsPlusLang { int[] fNGrams; String fLang; NGramsPlusLang(String la, int[] ng) { fLang = la; fNGrams = ng; } } static class CharsetRecog_8859_1 extends CharsetRecog_sbcs { protected static byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xAA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xDF, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, }; private static final NGramsPlusLang[] ngrams_8859_1 = new NGramsPlusLang[]{ new NGramsPlusLang( "da", new int[]{ 0x206166, 0x206174, 0x206465, 0x20656E, 0x206572, 0x20666F, 0x206861, 0x206920, 0x206D65, 0x206F67, 0x2070E5, 0x207369, 0x207374, 0x207469, 0x207669, 0x616620, 0x616E20, 0x616E64, 0x617220, 0x617420, 0x646520, 0x64656E, 0x646572, 0x646574, 0x652073, 0x656420, 0x656465, 0x656E20, 0x656E64, 0x657220, 0x657265, 0x657320, 0x657420, 0x666F72, 0x676520, 0x67656E, 0x676572, 0x696765, 0x696C20, 0x696E67, 0x6B6520, 0x6B6B65, 0x6C6572, 0x6C6967, 0x6C6C65, 0x6D6564, 0x6E6465, 0x6E6520, 0x6E6720, 0x6E6765, 0x6F6720, 0x6F6D20, 0x6F7220, 0x70E520, 0x722064, 0x722065, 0x722073, 0x726520, 0x737465, 0x742073, 0x746520, 0x746572, 0x74696C, 0x766572, }), new NGramsPlusLang( "de", new int[]{ 0x20616E, 0x206175, 0x206265, 0x206461, 0x206465, 0x206469, 0x206569, 0x206765, 0x206861, 0x20696E, 0x206D69, 0x207363, 0x207365, 0x20756E, 0x207665, 0x20766F, 0x207765, 0x207A75, 0x626572, 0x636820, 0x636865, 0x636874, 0x646173, 0x64656E, 0x646572, 0x646965, 0x652064, 0x652073, 0x65696E, 0x656974, 0x656E20, 0x657220, 0x657320, 0x67656E, 0x68656E, 0x687420, 0x696368, 0x696520, 0x696E20, 0x696E65, 0x697420, 0x6C6963, 0x6C6C65, 0x6E2061, 0x6E2064, 0x6E2073, 0x6E6420, 0x6E6465, 0x6E6520, 0x6E6720, 0x6E6765, 0x6E7465, 0x722064, 0x726465, 0x726569, 0x736368, 0x737465, 0x742064, 0x746520, 0x74656E, 0x746572, 0x756E64, 0x756E67, 0x766572, }), new NGramsPlusLang( "en", new int[]{ 0x206120, 0x20616E, 0x206265, 0x20636F, 0x20666F, 0x206861, 0x206865, 0x20696E, 0x206D61, 0x206F66, 0x207072, 0x207265, 0x207361, 0x207374, 0x207468, 0x20746F, 0x207768, 0x616964, 0x616C20, 0x616E20, 0x616E64, 0x617320, 0x617420, 0x617465, 0x617469, 0x642061, 0x642074, 0x652061, 0x652073, 0x652074, 0x656420, 0x656E74, 0x657220, 0x657320, 0x666F72, 0x686174, 0x686520, 0x686572, 0x696420, 0x696E20, 0x696E67, 0x696F6E, 0x697320, 0x6E2061, 0x6E2074, 0x6E6420, 0x6E6720, 0x6E7420, 0x6F6620, 0x6F6E20, 0x6F7220, 0x726520, 0x727320, 0x732061, 0x732074, 0x736169, 0x737420, 0x742074, 0x746572, 0x746861, 0x746865, 0x74696F, 0x746F20, 0x747320, }), new NGramsPlusLang( "es", new int[]{ 0x206120, 0x206361, 0x20636F, 0x206465, 0x20656C, 0x20656E, 0x206573, 0x20696E, 0x206C61, 0x206C6F, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207265, 0x207365, 0x20756E, 0x207920, 0x612063, 0x612064, 0x612065, 0x61206C, 0x612070, 0x616369, 0x61646F, 0x616C20, 0x617220, 0x617320, 0x6369F3, 0x636F6E, 0x646520, 0x64656C, 0x646F20, 0x652064, 0x652065, 0x65206C, 0x656C20, 0x656E20, 0x656E74, 0x657320, 0x657374, 0x69656E, 0x69F36E, 0x6C6120, 0x6C6F73, 0x6E2065, 0x6E7465, 0x6F2064, 0x6F2065, 0x6F6E20, 0x6F7220, 0x6F7320, 0x706172, 0x717565, 0x726120, 0x726573, 0x732064, 0x732065, 0x732070, 0x736520, 0x746520, 0x746F20, 0x756520, 0xF36E20, }), new NGramsPlusLang( "fr", new int[]{ 0x206175, 0x20636F, 0x206461, 0x206465, 0x206475, 0x20656E, 0x206574, 0x206C61, 0x206C65, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207365, 0x20736F, 0x20756E, 0x20E020, 0x616E74, 0x617469, 0x636520, 0x636F6E, 0x646520, 0x646573, 0x647520, 0x652061, 0x652063, 0x652064, 0x652065, 0x65206C, 0x652070, 0x652073, 0x656E20, 0x656E74, 0x657220, 0x657320, 0x657420, 0x657572, 0x696F6E, 0x697320, 0x697420, 0x6C6120, 0x6C6520, 0x6C6573, 0x6D656E, 0x6E2064, 0x6E6520, 0x6E7320, 0x6E7420, 0x6F6E20, 0x6F6E74, 0x6F7572, 0x717565, 0x72206C, 0x726520, 0x732061, 0x732064, 0x732065, 0x73206C, 0x732070, 0x742064, 0x746520, 0x74696F, 0x756520, 0x757220, }), new NGramsPlusLang( "it", new int[]{ 0x20616C, 0x206368, 0x20636F, 0x206465, 0x206469, 0x206520, 0x20696C, 0x20696E, 0x206C61, 0x207065, 0x207072, 0x20756E, 0x612063, 0x612064, 0x612070, 0x612073, 0x61746F, 0x636865, 0x636F6E, 0x64656C, 0x646920, 0x652061, 0x652063, 0x652064, 0x652069, 0x65206C, 0x652070, 0x652073, 0x656C20, 0x656C6C, 0x656E74, 0x657220, 0x686520, 0x692061, 0x692063, 0x692064, 0x692073, 0x696120, 0x696C20, 0x696E20, 0x696F6E, 0x6C6120, 0x6C6520, 0x6C6920, 0x6C6C61, 0x6E6520, 0x6E6920, 0x6E6F20, 0x6E7465, 0x6F2061, 0x6F2064, 0x6F2069, 0x6F2073, 0x6F6E20, 0x6F6E65, 0x706572, 0x726120, 0x726520, 0x736920, 0x746120, 0x746520, 0x746920, 0x746F20, 0x7A696F, }), new NGramsPlusLang( "nl", new int[]{ 0x20616C, 0x206265, 0x206461, 0x206465, 0x206469, 0x206565, 0x20656E, 0x206765, 0x206865, 0x20696E, 0x206D61, 0x206D65, 0x206F70, 0x207465, 0x207661, 0x207665, 0x20766F, 0x207765, 0x207A69, 0x61616E, 0x616172, 0x616E20, 0x616E64, 0x617220, 0x617420, 0x636874, 0x646520, 0x64656E, 0x646572, 0x652062, 0x652076, 0x65656E, 0x656572, 0x656E20, 0x657220, 0x657273, 0x657420, 0x67656E, 0x686574, 0x696520, 0x696E20, 0x696E67, 0x697320, 0x6E2062, 0x6E2064, 0x6E2065, 0x6E2068, 0x6E206F, 0x6E2076, 0x6E6465, 0x6E6720, 0x6F6E64, 0x6F6F72, 0x6F7020, 0x6F7220, 0x736368, 0x737465, 0x742064, 0x746520, 0x74656E, 0x746572, 0x76616E, 0x766572, 0x766F6F, }), new NGramsPlusLang( "no", new int[]{ 0x206174, 0x206176, 0x206465, 0x20656E, 0x206572, 0x20666F, 0x206861, 0x206920, 0x206D65, 0x206F67, 0x2070E5, 0x207365, 0x20736B, 0x20736F, 0x207374, 0x207469, 0x207669, 0x20E520, 0x616E64, 0x617220, 0x617420, 0x646520, 0x64656E, 0x646574, 0x652073, 0x656420, 0x656E20, 0x656E65, 0x657220, 0x657265, 0x657420, 0x657474, 0x666F72, 0x67656E, 0x696B6B, 0x696C20, 0x696E67, 0x6B6520, 0x6B6B65, 0x6C6520, 0x6C6C65, 0x6D6564, 0x6D656E, 0x6E2073, 0x6E6520, 0x6E6720, 0x6E6765, 0x6E6E65, 0x6F6720, 0x6F6D20, 0x6F7220, 0x70E520, 0x722073, 0x726520, 0x736F6D, 0x737465, 0x742073, 0x746520, 0x74656E, 0x746572, 0x74696C, 0x747420, 0x747465, 0x766572, }), new NGramsPlusLang( "pt", new int[]{ 0x206120, 0x20636F, 0x206461, 0x206465, 0x20646F, 0x206520, 0x206573, 0x206D61, 0x206E6F, 0x206F20, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207265, 0x207365, 0x20756D, 0x612061, 0x612063, 0x612064, 0x612070, 0x616465, 0x61646F, 0x616C20, 0x617220, 0x617261, 0x617320, 0x636F6D, 0x636F6E, 0x646120, 0x646520, 0x646F20, 0x646F73, 0x652061, 0x652064, 0x656D20, 0x656E74, 0x657320, 0x657374, 0x696120, 0x696361, 0x6D656E, 0x6E7465, 0x6E746F, 0x6F2061, 0x6F2063, 0x6F2064, 0x6F2065, 0x6F2070, 0x6F7320, 0x706172, 0x717565, 0x726120, 0x726573, 0x732061, 0x732064, 0x732065, 0x732070, 0x737461, 0x746520, 0x746F20, 0x756520, 0xE36F20, 0xE7E36F, }), new NGramsPlusLang( "sv", new int[]{ 0x206174, 0x206176, 0x206465, 0x20656E, 0x2066F6, 0x206861, 0x206920, 0x20696E, 0x206B6F, 0x206D65, 0x206F63, 0x2070E5, 0x20736B, 0x20736F, 0x207374, 0x207469, 0x207661, 0x207669, 0x20E472, 0x616465, 0x616E20, 0x616E64, 0x617220, 0x617474, 0x636820, 0x646520, 0x64656E, 0x646572, 0x646574, 0x656420, 0x656E20, 0x657220, 0x657420, 0x66F672, 0x67656E, 0x696C6C, 0x696E67, 0x6B6120, 0x6C6C20, 0x6D6564, 0x6E2073, 0x6E6120, 0x6E6465, 0x6E6720, 0x6E6765, 0x6E696E, 0x6F6368, 0x6F6D20, 0x6F6E20, 0x70E520, 0x722061, 0x722073, 0x726120, 0x736B61, 0x736F6D, 0x742073, 0x746120, 0x746520, 0x746572, 0x74696C, 0x747420, 0x766172, 0xE47220, 0xF67220, }), }; @Override public CharsetMatch match(CharsetDetector det) { String name = det.fC1Bytes ? "windows-1252" : "ISO-8859-1"; int bestConfidenceSoFar = -1; String lang = null; for (NGramsPlusLang ngl : ngrams_8859_1) { int confidence = match(det, ngl.fNGrams, byteMap); if (confidence > bestConfidenceSoFar) { bestConfidenceSoFar = confidence; lang = ngl.fLang; } } return bestConfidenceSoFar <= 0 ? null : new CharsetMatch(det, this, bestConfidenceSoFar, name, lang); } @Override public String getName() { return "ISO-8859-1"; } } static class CharsetRecog_8859_2 extends CharsetRecog_sbcs { protected static byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB1, (byte) 0x20, (byte) 0xB3, (byte) 0x20, (byte) 0xB5, (byte) 0xB6, (byte) 0x20, (byte) 0x20, (byte) 0xB9, (byte) 0xBA, (byte) 0xBB, (byte) 0xBC, (byte) 0x20, (byte) 0xBE, (byte) 0xBF, (byte) 0x20, (byte) 0xB1, (byte) 0x20, (byte) 0xB3, (byte) 0x20, (byte) 0xB5, (byte) 0xB6, (byte) 0xB7, (byte) 0x20, (byte) 0xB9, (byte) 0xBA, (byte) 0xBB, (byte) 0xBC, (byte) 0x20, (byte) 0xBE, (byte) 0xBF, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xDF, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0x20, }; private static final NGramsPlusLang[] ngrams_8859_2 = new NGramsPlusLang[]{ new NGramsPlusLang( "cs", new int[]{ 0x206120, 0x206279, 0x20646F, 0x206A65, 0x206E61, 0x206E65, 0x206F20, 0x206F64, 0x20706F, 0x207072, 0x2070F8, 0x20726F, 0x207365, 0x20736F, 0x207374, 0x20746F, 0x207620, 0x207679, 0x207A61, 0x612070, 0x636520, 0x636820, 0x652070, 0x652073, 0x652076, 0x656D20, 0x656EED, 0x686F20, 0x686F64, 0x697374, 0x6A6520, 0x6B7465, 0x6C6520, 0x6C6920, 0x6E6120, 0x6EE920, 0x6EEC20, 0x6EED20, 0x6F2070, 0x6F646E, 0x6F6A69, 0x6F7374, 0x6F7520, 0x6F7661, 0x706F64, 0x706F6A, 0x70726F, 0x70F865, 0x736520, 0x736F75, 0x737461, 0x737469, 0x73746E, 0x746572, 0x746EED, 0x746F20, 0x752070, 0xBE6520, 0xE16EED, 0xE9686F, 0xED2070, 0xED2073, 0xED6D20, 0xF86564, }), new NGramsPlusLang( "hu", new int[]{ 0x206120, 0x20617A, 0x206265, 0x206567, 0x20656C, 0x206665, 0x206861, 0x20686F, 0x206973, 0x206B65, 0x206B69, 0x206BF6, 0x206C65, 0x206D61, 0x206D65, 0x206D69, 0x206E65, 0x20737A, 0x207465, 0x20E973, 0x612061, 0x61206B, 0x61206D, 0x612073, 0x616B20, 0x616E20, 0x617A20, 0x62616E, 0x62656E, 0x656779, 0x656B20, 0x656C20, 0x656C65, 0x656D20, 0x656E20, 0x657265, 0x657420, 0x657465, 0x657474, 0x677920, 0x686F67, 0x696E74, 0x697320, 0x6B2061, 0x6BF67A, 0x6D6567, 0x6D696E, 0x6E2061, 0x6E616B, 0x6E656B, 0x6E656D, 0x6E7420, 0x6F6779, 0x732061, 0x737A65, 0x737A74, 0x737AE1, 0x73E967, 0x742061, 0x747420, 0x74E173, 0x7A6572, 0xE16E20, 0xE97320, }), new NGramsPlusLang( "pl", new int[]{ 0x20637A, 0x20646F, 0x206920, 0x206A65, 0x206B6F, 0x206D61, 0x206D69, 0x206E61, 0x206E69, 0x206F64, 0x20706F, 0x207072, 0x207369, 0x207720, 0x207769, 0x207779, 0x207A20, 0x207A61, 0x612070, 0x612077, 0x616E69, 0x636820, 0x637A65, 0x637A79, 0x646F20, 0x647A69, 0x652070, 0x652073, 0x652077, 0x65207A, 0x65676F, 0x656A20, 0x656D20, 0x656E69, 0x676F20, 0x696120, 0x696520, 0x69656A, 0x6B6120, 0x6B6920, 0x6B6965, 0x6D6965, 0x6E6120, 0x6E6961, 0x6E6965, 0x6F2070, 0x6F7761, 0x6F7769, 0x706F6C, 0x707261, 0x70726F, 0x70727A, 0x727A65, 0x727A79, 0x7369EA, 0x736B69, 0x737461, 0x776965, 0x796368, 0x796D20, 0x7A6520, 0x7A6965, 0x7A7920, 0xF37720, }), new NGramsPlusLang( "ro", new int[]{ 0x206120, 0x206163, 0x206361, 0x206365, 0x20636F, 0x206375, 0x206465, 0x206469, 0x206C61, 0x206D61, 0x207065, 0x207072, 0x207365, 0x2073E3, 0x20756E, 0x20BA69, 0x20EE6E, 0x612063, 0x612064, 0x617265, 0x617420, 0x617465, 0x617520, 0x636172, 0x636F6E, 0x637520, 0x63E320, 0x646520, 0x652061, 0x652063, 0x652064, 0x652070, 0x652073, 0x656120, 0x656920, 0x656C65, 0x656E74, 0x657374, 0x692061, 0x692063, 0x692064, 0x692070, 0x696520, 0x696920, 0x696E20, 0x6C6120, 0x6C6520, 0x6C6F72, 0x6C7569, 0x6E6520, 0x6E7472, 0x6F7220, 0x70656E, 0x726520, 0x726561, 0x727520, 0x73E320, 0x746520, 0x747275, 0x74E320, 0x756920, 0x756C20, 0xBA6920, 0xEE6E20, }) }; @Override public CharsetMatch match(CharsetDetector det) { String name = det.fC1Bytes ? "windows-1250" : "ISO-8859-2"; int bestConfidenceSoFar = -1; String lang = null; for (NGramsPlusLang ngl : ngrams_8859_2) { int confidence = match(det, ngl.fNGrams, byteMap); if (confidence > bestConfidenceSoFar) { bestConfidenceSoFar = confidence; lang = ngl.fLang; } } return bestConfidenceSoFar <= 0 ? null : new CharsetMatch(det, this, bestConfidenceSoFar, name, lang); } @Override public String getName() { return "ISO-8859-2"; } } abstract static class CharsetRecog_8859_5 extends CharsetRecog_sbcs { protected static byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0x20, (byte) 0xFE, (byte) 0xFF, (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0x20, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0x20, (byte) 0xFE, (byte) 0xFF, }; @Override public String getName() { return "ISO-8859-5"; } } static class CharsetRecog_8859_5_ru extends CharsetRecog_8859_5 { private static final int[] ngrams = { 0x20D220, 0x20D2DE, 0x20D4DE, 0x20D7D0, 0x20D820, 0x20DAD0, 0x20DADE, 0x20DDD0, 0x20DDD5, 0x20DED1, 0x20DFDE, 0x20DFE0, 0x20E0D0, 0x20E1DE, 0x20E1E2, 0x20E2DE, 0x20E7E2, 0x20EDE2, 0xD0DDD8, 0xD0E2EC, 0xD3DE20, 0xD5DBEC, 0xD5DDD8, 0xD5E1E2, 0xD5E220, 0xD820DF, 0xD8D520, 0xD8D820, 0xD8EF20, 0xDBD5DD, 0xDBD820, 0xDBECDD, 0xDDD020, 0xDDD520, 0xDDD8D5, 0xDDD8EF, 0xDDDE20, 0xDDDED2, 0xDE20D2, 0xDE20DF, 0xDE20E1, 0xDED220, 0xDED2D0, 0xDED3DE, 0xDED920, 0xDEDBEC, 0xDEDC20, 0xDEE1E2, 0xDFDEDB, 0xDFE0D5, 0xDFE0D8, 0xDFE0DE, 0xE0D0D2, 0xE0D5D4, 0xE1E2D0, 0xE1E2D2, 0xE1E2D8, 0xE1EF20, 0xE2D5DB, 0xE2DE20, 0xE2DEE0, 0xE2EC20, 0xE7E2DE, 0xEBE520, }; @Override public String getLanguage() { return "ru"; } @Override public CharsetMatch match(CharsetDetector det) { int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } abstract static class CharsetRecog_8859_6 extends CharsetRecog_sbcs { protected static byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF, (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, }; @Override public String getName() { return "ISO-8859-6"; } } static class CharsetRecog_8859_6_ar extends CharsetRecog_8859_6 { private static final int[] ngrams = { 0x20C7E4, 0x20C7E6, 0x20C8C7, 0x20D9E4, 0x20E1EA, 0x20E4E4, 0x20E5E6, 0x20E8C7, 0xC720C7, 0xC7C120, 0xC7CA20, 0xC7D120, 0xC7E420, 0xC7E4C3, 0xC7E4C7, 0xC7E4C8, 0xC7E4CA, 0xC7E4CC, 0xC7E4CD, 0xC7E4CF, 0xC7E4D3, 0xC7E4D9, 0xC7E4E2, 0xC7E4E5, 0xC7E4E8, 0xC7E4EA, 0xC7E520, 0xC7E620, 0xC7E6CA, 0xC820C7, 0xC920C7, 0xC920E1, 0xC920E4, 0xC920E5, 0xC920E8, 0xCA20C7, 0xCF20C7, 0xCFC920, 0xD120C7, 0xD1C920, 0xD320C7, 0xD920C7, 0xD9E4E9, 0xE1EA20, 0xE420C7, 0xE4C920, 0xE4E920, 0xE4EA20, 0xE520C7, 0xE5C720, 0xE5C920, 0xE5E620, 0xE620C7, 0xE720C7, 0xE7C720, 0xE8C7E4, 0xE8E620, 0xE920C7, 0xEA20C7, 0xEA20E5, 0xEA20E8, 0xEAC920, 0xEAD120, 0xEAE620, }; @Override public String getLanguage() { return "ar"; } @Override public CharsetMatch match(CharsetDetector det) { int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } abstract static class CharsetRecog_8859_7 extends CharsetRecog_sbcs { protected static byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xA1, (byte) 0xA2, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xDC, (byte) 0x20, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, (byte) 0x20, (byte) 0xFC, (byte) 0x20, (byte) 0xFD, (byte) 0xFE, (byte) 0xC0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0x20, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0x20, }; @Override public String getName() { return "ISO-8859-7"; } } static class CharsetRecog_8859_7_el extends CharsetRecog_8859_7 { private static final int[] ngrams = { 0x20E1ED, 0x20E1F0, 0x20E3E9, 0x20E4E9, 0x20E5F0, 0x20E720, 0x20EAE1, 0x20ECE5, 0x20EDE1, 0x20EF20, 0x20F0E1, 0x20F0EF, 0x20F0F1, 0x20F3F4, 0x20F3F5, 0x20F4E7, 0x20F4EF, 0xDFE120, 0xE120E1, 0xE120F4, 0xE1E920, 0xE1ED20, 0xE1F0FC, 0xE1F220, 0xE3E9E1, 0xE5E920, 0xE5F220, 0xE720F4, 0xE7ED20, 0xE7F220, 0xE920F4, 0xE9E120, 0xE9EADE, 0xE9F220, 0xEAE1E9, 0xEAE1F4, 0xECE520, 0xED20E1, 0xED20E5, 0xED20F0, 0xEDE120, 0xEFF220, 0xEFF520, 0xF0EFF5, 0xF0F1EF, 0xF0FC20, 0xF220E1, 0xF220E5, 0xF220EA, 0xF220F0, 0xF220F4, 0xF3E520, 0xF3E720, 0xF3F4EF, 0xF4E120, 0xF4E1E9, 0xF4E7ED, 0xF4E7F2, 0xF4E9EA, 0xF4EF20, 0xF4EFF5, 0xF4F9ED, 0xF9ED20, 0xFEED20, }; @Override public String getLanguage() { return "el"; } @Override public CharsetMatch match(CharsetDetector det) { String name = det.fC1Bytes ? "windows-1253" : "ISO-8859-7"; int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, this, confidence, name, "el"); } } abstract static class CharsetRecog_8859_8 extends CharsetRecog_sbcs { protected static byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, }; @Override public String getName() { return "ISO-8859-8"; } } static class CharsetRecog_8859_8_I_he extends CharsetRecog_8859_8 { private static final int[] ngrams = { 0x20E0E5, 0x20E0E7, 0x20E0E9, 0x20E0FA, 0x20E1E9, 0x20E1EE, 0x20E4E0, 0x20E4E5, 0x20E4E9, 0x20E4EE, 0x20E4F2, 0x20E4F9, 0x20E4FA, 0x20ECE0, 0x20ECE4, 0x20EEE0, 0x20F2EC, 0x20F9EC, 0xE0FA20, 0xE420E0, 0xE420E1, 0xE420E4, 0xE420EC, 0xE420EE, 0xE420F9, 0xE4E5E0, 0xE5E020, 0xE5ED20, 0xE5EF20, 0xE5F820, 0xE5FA20, 0xE920E4, 0xE9E420, 0xE9E5FA, 0xE9E9ED, 0xE9ED20, 0xE9EF20, 0xE9F820, 0xE9FA20, 0xEC20E0, 0xEC20E4, 0xECE020, 0xECE420, 0xED20E0, 0xED20E1, 0xED20E4, 0xED20EC, 0xED20EE, 0xED20F9, 0xEEE420, 0xEF20E4, 0xF0E420, 0xF0E920, 0xF0E9ED, 0xF2EC20, 0xF820E4, 0xF8E9ED, 0xF9EC20, 0xFA20E0, 0xFA20E1, 0xFA20E4, 0xFA20EC, 0xFA20EE, 0xFA20F9, }; @Override public String getName() { return "ISO-8859-8-I"; } @Override public String getLanguage() { return "he"; } @Override public CharsetMatch match(CharsetDetector det) { String name = det.fC1Bytes ? "windows-1255" : "ISO-8859-8-I"; int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, this, confidence, name, "he"); } } static class CharsetRecog_8859_8_he extends CharsetRecog_8859_8 { private static final int[] ngrams = { 0x20E0E5, 0x20E0EC, 0x20E4E9, 0x20E4EC, 0x20E4EE, 0x20E4F0, 0x20E9F0, 0x20ECF2, 0x20ECF9, 0x20EDE5, 0x20EDE9, 0x20EFE5, 0x20EFE9, 0x20F8E5, 0x20F8E9, 0x20FAE0, 0x20FAE5, 0x20FAE9, 0xE020E4, 0xE020EC, 0xE020ED, 0xE020FA, 0xE0E420, 0xE0E5E4, 0xE0EC20, 0xE0EE20, 0xE120E4, 0xE120ED, 0xE120FA, 0xE420E4, 0xE420E9, 0xE420EC, 0xE420ED, 0xE420EF, 0xE420F8, 0xE420FA, 0xE4EC20, 0xE5E020, 0xE5E420, 0xE7E020, 0xE9E020, 0xE9E120, 0xE9E420, 0xEC20E4, 0xEC20ED, 0xEC20FA, 0xECF220, 0xECF920, 0xEDE9E9, 0xEDE9F0, 0xEDE9F8, 0xEE20E4, 0xEE20ED, 0xEE20FA, 0xEEE120, 0xEEE420, 0xF2E420, 0xF920E4, 0xF920ED, 0xF920FA, 0xF9E420, 0xFAE020, 0xFAE420, 0xFAE5E9, }; @Override public String getLanguage() { return "he"; } @Override public CharsetMatch match(CharsetDetector det) { String name = det.fC1Bytes ? "windows-1255" : "ISO-8859-8"; int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, this, confidence, name, "he"); } } abstract static class CharsetRecog_8859_9 extends CharsetRecog_sbcs { protected static byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xAA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0x69, (byte) 0xFE, (byte) 0xDF, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, }; @Override public String getName() { return "ISO-8859-9"; } } static class CharsetRecog_8859_9_tr extends CharsetRecog_8859_9 { private static final int[] ngrams = { 0x206261, 0x206269, 0x206275, 0x206461, 0x206465, 0x206765, 0x206861, 0x20696C, 0x206B61, 0x206B6F, 0x206D61, 0x206F6C, 0x207361, 0x207461, 0x207665, 0x207961, 0x612062, 0x616B20, 0x616C61, 0x616D61, 0x616E20, 0x616EFD, 0x617220, 0x617261, 0x6172FD, 0x6173FD, 0x617961, 0x626972, 0x646120, 0x646520, 0x646920, 0x652062, 0x65206B, 0x656469, 0x656E20, 0x657220, 0x657269, 0x657369, 0x696C65, 0x696E20, 0x696E69, 0x697220, 0x6C616E, 0x6C6172, 0x6C6520, 0x6C6572, 0x6E2061, 0x6E2062, 0x6E206B, 0x6E6461, 0x6E6465, 0x6E6520, 0x6E6920, 0x6E696E, 0x6EFD20, 0x72696E, 0x72FD6E, 0x766520, 0x796120, 0x796F72, 0xFD6E20, 0xFD6E64, 0xFD6EFD, 0xFDF0FD, }; @Override public String getLanguage() { return "tr"; } @Override public CharsetMatch match(CharsetDetector det) { String name = det.fC1Bytes ? "windows-1254" : "ISO-8859-9"; int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, this, confidence, name, "tr"); } } static class CharsetRecog_windows_1251 extends CharsetRecog_sbcs { private static final int[] ngrams = { 0x20E220, 0x20E2EE, 0x20E4EE, 0x20E7E0, 0x20E820, 0x20EAE0, 0x20EAEE, 0x20EDE0, 0x20EDE5, 0x20EEE1, 0x20EFEE, 0x20EFF0, 0x20F0E0, 0x20F1EE, 0x20F1F2, 0x20F2EE, 0x20F7F2, 0x20FDF2, 0xE0EDE8, 0xE0F2FC, 0xE3EE20, 0xE5EBFC, 0xE5EDE8, 0xE5F1F2, 0xE5F220, 0xE820EF, 0xE8E520, 0xE8E820, 0xE8FF20, 0xEBE5ED, 0xEBE820, 0xEBFCED, 0xEDE020, 0xEDE520, 0xEDE8E5, 0xEDE8FF, 0xEDEE20, 0xEDEEE2, 0xEE20E2, 0xEE20EF, 0xEE20F1, 0xEEE220, 0xEEE2E0, 0xEEE3EE, 0xEEE920, 0xEEEBFC, 0xEEEC20, 0xEEF1F2, 0xEFEEEB, 0xEFF0E5, 0xEFF0E8, 0xEFF0EE, 0xF0E0E2, 0xF0E5E4, 0xF1F2E0, 0xF1F2E2, 0xF1F2E8, 0xF1FF20, 0xF2E5EB, 0xF2EE20, 0xF2EEF0, 0xF2FC20, 0xF7F2EE, 0xFBF520, }; private static final byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x90, (byte) 0x83, (byte) 0x20, (byte) 0x83, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x9A, (byte) 0x20, (byte) 0x9C, (byte) 0x9D, (byte) 0x9E, (byte) 0x9F, (byte) 0x90, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x9A, (byte) 0x20, (byte) 0x9C, (byte) 0x9D, (byte) 0x9E, (byte) 0x9F, (byte) 0x20, (byte) 0xA2, (byte) 0xA2, (byte) 0xBC, (byte) 0x20, (byte) 0xB4, (byte) 0x20, (byte) 0x20, (byte) 0xB8, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xBF, (byte) 0x20, (byte) 0x20, (byte) 0xB3, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0x20, (byte) 0x20, (byte) 0xB8, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0xBC, (byte) 0xBE, (byte) 0xBE, (byte) 0xBF, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, }; @Override public String getName() { return "windows-1251"; } @Override public String getLanguage() { return "ru"; } @Override public CharsetMatch match(CharsetDetector det) { int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } static class CharsetRecog_windows_1256 extends CharsetRecog_sbcs { private static final int[] ngrams = { 0x20C7E1, 0x20C7E4, 0x20C8C7, 0x20DAE1, 0x20DDED, 0x20E1E1, 0x20E3E4, 0x20E6C7, 0xC720C7, 0xC7C120, 0xC7CA20, 0xC7D120, 0xC7E120, 0xC7E1C3, 0xC7E1C7, 0xC7E1C8, 0xC7E1CA, 0xC7E1CC, 0xC7E1CD, 0xC7E1CF, 0xC7E1D3, 0xC7E1DA, 0xC7E1DE, 0xC7E1E3, 0xC7E1E6, 0xC7E1ED, 0xC7E320, 0xC7E420, 0xC7E4CA, 0xC820C7, 0xC920C7, 0xC920DD, 0xC920E1, 0xC920E3, 0xC920E6, 0xCA20C7, 0xCF20C7, 0xCFC920, 0xD120C7, 0xD1C920, 0xD320C7, 0xDA20C7, 0xDAE1EC, 0xDDED20, 0xE120C7, 0xE1C920, 0xE1EC20, 0xE1ED20, 0xE320C7, 0xE3C720, 0xE3C920, 0xE3E420, 0xE420C7, 0xE520C7, 0xE5C720, 0xE6C7E1, 0xE6E420, 0xEC20C7, 0xED20C7, 0xED20E3, 0xED20E6, 0xEDC920, 0xEDD120, 0xEDE420, }; private static final byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x81, (byte) 0x20, (byte) 0x83, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x88, (byte) 0x20, (byte) 0x8A, (byte) 0x20, (byte) 0x9C, (byte) 0x8D, (byte) 0x8E, (byte) 0x8F, (byte) 0x90, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x98, (byte) 0x20, (byte) 0x9A, (byte) 0x20, (byte) 0x9C, (byte) 0x20, (byte) 0x20, (byte) 0x9F, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xAA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF, (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0x20, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xF4, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xF9, (byte) 0x20, (byte) 0xFB, (byte) 0xFC, (byte) 0x20, (byte) 0x20, (byte) 0xFF, }; @Override public String getName() { return "windows-1256"; } @Override public String getLanguage() { return "ar"; } @Override public CharsetMatch match(CharsetDetector det) { int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } static class CharsetRecog_KOI8_R extends CharsetRecog_sbcs { private static final int[] ngrams = { 0x20C4CF, 0x20C920, 0x20CBC1, 0x20CBCF, 0x20CEC1, 0x20CEC5, 0x20CFC2, 0x20D0CF, 0x20D0D2, 0x20D2C1, 0x20D3CF, 0x20D3D4, 0x20D4CF, 0x20D720, 0x20D7CF, 0x20DAC1, 0x20DCD4, 0x20DED4, 0xC1CEC9, 0xC1D4D8, 0xC5CCD8, 0xC5CEC9, 0xC5D3D4, 0xC5D420, 0xC7CF20, 0xC920D0, 0xC9C520, 0xC9C920, 0xC9D120, 0xCCC5CE, 0xCCC920, 0xCCD8CE, 0xCEC120, 0xCEC520, 0xCEC9C5, 0xCEC9D1, 0xCECF20, 0xCECFD7, 0xCF20D0, 0xCF20D3, 0xCF20D7, 0xCFC7CF, 0xCFCA20, 0xCFCCD8, 0xCFCD20, 0xCFD3D4, 0xCFD720, 0xCFD7C1, 0xD0CFCC, 0xD0D2C5, 0xD0D2C9, 0xD0D2CF, 0xD2C1D7, 0xD2C5C4, 0xD3D120, 0xD3D4C1, 0xD3D4C9, 0xD3D4D7, 0xD4C5CC, 0xD4CF20, 0xD4CFD2, 0xD4D820, 0xD9C820, 0xDED4CF, }; private static final byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xA3, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xA3, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF, (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF, (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, }; @Override public String getName() { return "KOI8-R"; } @Override public String getLanguage() { return "ru"; } @Override public CharsetMatch match(CharsetDetector det) { int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } abstract static class CharsetRecog_IBM424_he extends CharsetRecog_sbcs { protected static byte[] byteMap = { /* -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F */ /* 0- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 1- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 2- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 3- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 4- */ (byte) 0x40, (byte) 0x41, (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x48, (byte) 0x49, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 5- */ (byte) 0x40, (byte) 0x51, (byte) 0x52, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58, (byte) 0x59, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 6- */ (byte) 0x40, (byte) 0x40, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 7- */ (byte) 0x40, (byte) 0x71, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x00, (byte) 0x40, (byte) 0x40, /* 8- */ (byte) 0x40, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 9- */ (byte) 0x40, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* A- */ (byte) 0xA0, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* B- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* C- */ (byte) 0x40, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* D- */ (byte) 0x40, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* E- */ (byte) 0x40, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* F- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, }; @Override public String getLanguage() { return "he"; } } static class CharsetRecog_IBM424_he_rtl extends CharsetRecog_IBM424_he { @Override public String getName() { return "IBM424_rtl"; } private static final int[] ngrams = { 0x404146, 0x404148, 0x404151, 0x404171, 0x404251, 0x404256, 0x404541, 0x404546, 0x404551, 0x404556, 0x404562, 0x404569, 0x404571, 0x405441, 0x405445, 0x405641, 0x406254, 0x406954, 0x417140, 0x454041, 0x454042, 0x454045, 0x454054, 0x454056, 0x454069, 0x454641, 0x464140, 0x465540, 0x465740, 0x466840, 0x467140, 0x514045, 0x514540, 0x514671, 0x515155, 0x515540, 0x515740, 0x516840, 0x517140, 0x544041, 0x544045, 0x544140, 0x544540, 0x554041, 0x554042, 0x554045, 0x554054, 0x554056, 0x554069, 0x564540, 0x574045, 0x584540, 0x585140, 0x585155, 0x625440, 0x684045, 0x685155, 0x695440, 0x714041, 0x714042, 0x714045, 0x714054, 0x714056, 0x714069, }; @Override public CharsetMatch match(CharsetDetector det) { int confidence = match(det, ngrams, byteMap, (byte) 0x40); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } static class CharsetRecog_IBM424_he_ltr extends CharsetRecog_IBM424_he { @Override public String getName() { return "IBM424_ltr"; } private static final int[] ngrams = { 0x404146, 0x404154, 0x404551, 0x404554, 0x404556, 0x404558, 0x405158, 0x405462, 0x405469, 0x405546, 0x405551, 0x405746, 0x405751, 0x406846, 0x406851, 0x407141, 0x407146, 0x407151, 0x414045, 0x414054, 0x414055, 0x414071, 0x414540, 0x414645, 0x415440, 0x415640, 0x424045, 0x424055, 0x424071, 0x454045, 0x454051, 0x454054, 0x454055, 0x454057, 0x454068, 0x454071, 0x455440, 0x464140, 0x464540, 0x484140, 0x514140, 0x514240, 0x514540, 0x544045, 0x544055, 0x544071, 0x546240, 0x546940, 0x555151, 0x555158, 0x555168, 0x564045, 0x564055, 0x564071, 0x564240, 0x564540, 0x624540, 0x694045, 0x694055, 0x694071, 0x694540, 0x714140, 0x714540, 0x714651 }; @Override public CharsetMatch match(CharsetDetector det) { int confidence = match(det, ngrams, byteMap, (byte) 0x40); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } abstract static class CharsetRecog_IBM420_ar extends CharsetRecog_sbcs { protected static byte[] byteMap = { /* -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F */ /* 0- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 1- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 2- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 3- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 4- */ (byte) 0x40, (byte) 0x40, (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x48, (byte) 0x49, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 5- */ (byte) 0x40, (byte) 0x51, (byte) 0x52, (byte) 0x40, (byte) 0x40, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58, (byte) 0x59, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 6- */ (byte) 0x40, (byte) 0x40, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 7- */ (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 8- */ (byte) 0x80, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x8A, (byte) 0x8B, (byte) 0x8C, (byte) 0x8D, (byte) 0x8E, (byte) 0x8F, /* 9- */ (byte) 0x90, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x9A, (byte) 0x9B, (byte) 0x9C, (byte) 0x9D, (byte) 0x9E, (byte) 0x9F, /* A- */ (byte) 0xA0, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xAA, (byte) 0xAB, (byte) 0xAC, (byte) 0xAD, (byte) 0xAE, (byte) 0xAF, /* B- */ (byte) 0xB0, (byte) 0xB1, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0x40, (byte) 0x40, (byte) 0xB8, (byte) 0xB9, (byte) 0xBA, (byte) 0xBB, (byte) 0xBC, (byte) 0xBD, (byte) 0xBE, (byte) 0xBF, /* C- */ (byte) 0x40, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x40, (byte) 0xCB, (byte) 0x40, (byte) 0xCD, (byte) 0x40, (byte) 0xCF, /* D- */ (byte) 0x40, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, /* E- */ (byte) 0x40, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xEA, (byte) 0xEB, (byte) 0x40, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, /* F- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0x40, }; @Override public String getLanguage() { return "ar"; } } static class CharsetRecog_IBM420_ar_rtl extends CharsetRecog_IBM420_ar { private static final int[] ngrams = { 0x4056B1, 0x4056BD, 0x405856, 0x409AB1, 0x40ABDC, 0x40B1B1, 0x40BBBD, 0x40CF56, 0x564056, 0x564640, 0x566340, 0x567540, 0x56B140, 0x56B149, 0x56B156, 0x56B158, 0x56B163, 0x56B167, 0x56B169, 0x56B173, 0x56B178, 0x56B19A, 0x56B1AD, 0x56B1BB, 0x56B1CF, 0x56B1DC, 0x56BB40, 0x56BD40, 0x56BD63, 0x584056, 0x624056, 0x6240AB, 0x6240B1, 0x6240BB, 0x6240CF, 0x634056, 0x734056, 0x736240, 0x754056, 0x756240, 0x784056, 0x9A4056, 0x9AB1DA, 0xABDC40, 0xB14056, 0xB16240, 0xB1DA40, 0xB1DC40, 0xBB4056, 0xBB5640, 0xBB6240, 0xBBBD40, 0xBD4056, 0xBF4056, 0xBF5640, 0xCF56B1, 0xCFBD40, 0xDA4056, 0xDC4056, 0xDC40BB, 0xDC40CF, 0xDC6240, 0xDC7540, 0xDCBD40, }; @Override public String getName() { return "IBM420_rtl"; } @Override public CharsetMatch match(CharsetDetector det) { int confidence = matchIBM420(det, ngrams, byteMap, (byte) 0x40); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } static class CharsetRecog_IBM420_ar_ltr extends CharsetRecog_IBM420_ar { private static final int[] ngrams = { 0x404656, 0x4056BB, 0x4056BF, 0x406273, 0x406275, 0x4062B1, 0x4062BB, 0x4062DC, 0x406356, 0x407556, 0x4075DC, 0x40B156, 0x40BB56, 0x40BD56, 0x40BDBB, 0x40BDCF, 0x40BDDC, 0x40DAB1, 0x40DCAB, 0x40DCB1, 0x49B156, 0x564056, 0x564058, 0x564062, 0x564063, 0x564073, 0x564075, 0x564078, 0x56409A, 0x5640B1, 0x5640BB, 0x5640BD, 0x5640BF, 0x5640DA, 0x5640DC, 0x565840, 0x56B156, 0x56CF40, 0x58B156, 0x63B156, 0x63BD56, 0x67B156, 0x69B156, 0x73B156, 0x78B156, 0x9AB156, 0xAB4062, 0xADB156, 0xB14062, 0xB15640, 0xB156CF, 0xB19A40, 0xB1B140, 0xBB4062, 0xBB40DC, 0xBBB156, 0xBD5640, 0xBDBB40, 0xCF4062, 0xCF40DC, 0xCFB156, 0xDAB19A, 0xDCAB40, 0xDCB156 }; @Override public String getName() { return "IBM420_ltr"; } @Override public CharsetMatch match(CharsetDetector det) { int confidence = matchIBM420(det, ngrams, byteMap, (byte) 0x40); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } } ================================================ FILE: src/main/java/io/legado/app/lib/icu4j/CharsetRecognizer.java ================================================ // © 2016 and later: Unicode, Inc. and others. // License & terms of use: http://www.unicode.org/copyright.html /** * ****************************************************************************** * Copyright (C) 2005-2012, International Business Machines Corporation and * * others. All Rights Reserved. * * ****************************************************************************** */ package io.legado.app.lib.icu4j; /** * Abstract class for recognizing a single charset. * Part of the implementation of ICU's CharsetDetector. *

* Each specific charset that can be recognized will have an instance * of some subclass of this class. All interaction between the overall * CharsetDetector and the stuff specific to an individual charset happens * via the interface provided here. *

* Instances of CharsetDetector DO NOT have or maintain * state pertaining to a specific match or detect operation. * The WILL be shared by multiple instances of CharsetDetector. * They encapsulate const charset-specific information. */ abstract class CharsetRecognizer { /** * Get the IANA name of this charset. * * @return the charset name. */ abstract String getName(); /** * Get the ISO language code for this charset. * * @return the language code, or null if the language cannot be determined. */ public String getLanguage() { return null; } /** * Test the match of this charset with the input text data * which is obtained via the CharsetDetector object. * * @param det The CharsetDetector, which contains the input text * to be checked for being in this charset. * @return A CharsetMatch object containing details of match * with this charset, or null if there was no match. */ abstract CharsetMatch match(CharsetDetector det); } ================================================ FILE: src/main/java/io/legado/app/model/Debug.kt ================================================ package io.legado.app.model import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.model.webBook.WebBook import mu.KotlinLogging private val logger = KotlinLogging.logger {} object Debug : DebugLog{ } ================================================ FILE: src/main/java/io/legado/app/model/DebugLog.kt ================================================ package io.legado.app.model import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import mu.KotlinLogging import okhttp3.logging.HttpLoggingInterceptor private val logger = KotlinLogging.logger {} interface DebugLog: HttpLoggingInterceptor.Logger { fun log( sourceUrl: String? = "", msg: String? = "", isHtml: Boolean = false ) { logger.info("sourceUrl: {}, msg: {}", sourceUrl, msg) } override fun log(message: String) { logger.debug(message) } } ================================================ FILE: src/main/java/io/legado/app/model/Debugger.kt ================================================ package io.legado.app.model import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.model.webBook.WebBook import io.legado.app.utils.isAbsUrl import io.legado.app.utils.HtmlFormatter import mu.KotlinLogging import java.text.SimpleDateFormat import java.util.Locale import java.util.Date private val logger = KotlinLogging.logger {} class Debugger(val logMsg: (String) -> Unit) : DebugLog { private val debugTimeFormat = SimpleDateFormat("[mm:ss.SSS]", Locale.getDefault()) private var startTime: Long = System.currentTimeMillis() fun log( sourceUrl: String?, msg: String? ) { log(sourceUrl, msg, false) } override fun log(message: String) { val time = debugTimeFormat.format(Date(System.currentTimeMillis() - startTime)) logMsg("$time $message") } override fun log( sourceUrl: String?, msg: String?, isHtml: Boolean ) { if (sourceUrl == null || msg == null) return logger.info("sourceUrl: {}, msg: {}", sourceUrl, msg) var printMsg = msg if (isHtml) { printMsg = HtmlFormatter.format(msg) } val time = debugTimeFormat.format(Date(System.currentTimeMillis() - startTime)) printMsg = "$time $printMsg" logMsg(printMsg) } suspend fun startDebug(webBook: WebBook, key: String) { val bookSource = webBook.bookSource webBook.debugLogger = this@Debugger startTime = System.currentTimeMillis() when { key.isAbsUrl() -> { val book = Book() book.origin = bookSource.bookSourceUrl book.bookUrl = key log(bookSource.bookSourceUrl, "⇒开始访问详情页:$key") infoDebug(webBook, book) } key.contains("::") -> { val url = key.substringAfter("::") log(bookSource.bookSourceUrl, "⇒开始访问发现页:$url") exploreDebug(webBook, url) } key.startsWith("++") -> { val url = key.substring(2) val book = Book() book.origin = bookSource.bookSourceUrl book.tocUrl = url log(bookSource.bookSourceUrl, "⇒开始访目录页:$url") tocDebug(webBook, book) } key.startsWith("--") -> { val url = key.substring(2) val book = Book() book.origin = bookSource.bookSourceUrl log(bookSource.bookSourceUrl, "⇒开始访正文页:$url") val chapter = BookChapter() chapter.title = "调试" chapter.url = url contentDebug(webBook, book, chapter, null) } else -> { log(bookSource.bookSourceUrl, "⇒开始搜索关键字:$key") searchDebug(webBook, key) } } } private suspend fun exploreDebug(webBook: WebBook, url: String) { webBook.debugLogger = this@Debugger log("︾开始解析发现页") runCatching { webBook.exploreBook(url, 1) }.onSuccess { exploreBooks -> exploreBooks.let { if (exploreBooks.isNotEmpty()) { log(webBook.sourceUrl, "︽发现页解析完成") infoDebug(webBook, exploreBooks[0].toBook()) } else { log(webBook.sourceUrl, "︽未获取到书籍") } } }.onFailure { log(webBook.sourceUrl, "Error: " + it.localizedMessage) throw it } } private suspend fun searchDebug(webBook: WebBook, key: String) { webBook.debugLogger = this@Debugger log(msg = "︾开始解析搜索页") runCatching { webBook.searchBook(key, 1) }.onSuccess { searchBooks -> searchBooks.let { if (searchBooks.isNotEmpty()) { log(webBook.sourceUrl, "︽搜索页解析完成") infoDebug(webBook, searchBooks[0].toBook()) } else { log(webBook.sourceUrl, "︽未获取到书籍") } } }.onFailure { log(webBook.sourceUrl, "Error: " + it.localizedMessage) throw it } } private suspend fun infoDebug(webBook: WebBook, book: Book) { webBook.debugLogger = this@Debugger log(msg = "︾开始解析详情页") runCatching { webBook.getBookInfo(book.bookUrl) } .onSuccess { log(webBook.sourceUrl, "︽详情页解析完成") tocDebug(webBook, it) } .onFailure { log(webBook.sourceUrl, "Error: " + it.localizedMessage) throw it } } private suspend fun tocDebug(webBook: WebBook, book: Book) { webBook.debugLogger = this@Debugger log(msg = "︾开始解析目录页") runCatching { webBook.getChapterList(book) } .onSuccess { chapterList -> chapterList?.let { if (it.isNotEmpty()) { log(webBook.sourceUrl, "︽目录页解析完成") val nextChapterUrl = if (it.size > 1) it[1].url else null contentDebug(webBook, book, it[0], nextChapterUrl) } else { log(webBook.sourceUrl, "︽目录列表为空") } } } .onFailure { log(webBook.sourceUrl, "Error: " + it.localizedMessage) throw it } } private suspend fun contentDebug( webBook: WebBook, book: Book, bookChapter: BookChapter, nextChapterUrl: String? ) { webBook.debugLogger = this@Debugger log(webBook.sourceUrl, "︾开始解析正文页") runCatching { webBook.getBookContent(book, bookChapter, nextChapterUrl) } .onSuccess { log(webBook.sourceUrl, "︽正文页解析完成") } .onFailure { log(webBook.sourceUrl, "Error: " + it.localizedMessage) } } } ================================================ FILE: src/main/java/io/legado/app/model/README.md ================================================ # 放置一些模块类 * analyzeRule 书源规则解析 * localBook 本地书籍解析 * rss 订阅规则解析 * webBook 获取网络书籍 ================================================ FILE: src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt ================================================ package io.legado.app.model.analyzeRule import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.ReadContext import java.util.* @Suppress("RegExpRedundantEscape") class AnalyzeByJSonPath(json: Any) { companion object { fun parse(json: Any): ReadContext { return when (json) { is ReadContext -> json is String -> JsonPath.parse(json) //JsonPath.parse(json) else -> JsonPath.parse(json) //JsonPath.parse(json) } } } private var ctx: ReadContext = parse(json) /** * 改进解析方法 * 解决阅读”&&“、”||“与jsonPath支持的”&&“、”||“之间的冲突 * 解决{$.rule}形式规则可能匹配错误的问题,旧规则用正则解析内容含‘}’的json文本时,用规则中的字段去匹配这种内容会匹配错误.现改用平衡嵌套方法解决这个问题 * */ fun getString(rule: String): String? { if (rule.isEmpty()) return null var result: String val ruleAnalyzes = RuleAnalyzer(rule, true) //设置平衡组为代码平衡 val rules = ruleAnalyzes.splitRule("&&", "||") if (rules.size == 1) { ruleAnalyzes.reSetPos() //将pos重置为0,复用解析器 result = ruleAnalyzes.innerRule("{$.") { getString(it) } //替换所有{$.rule...} if (result.isEmpty()) { //st为空,表明无成功替换的内嵌规则 try { val ob = ctx.read(rule) result = if (ob is List<*>) { ob.joinToString("\n") } else { ob.toString() } } catch (e: Exception) { e.printStackTrace() } } return result } else { val textList = arrayListOf() for (rl in rules) { val temp = getString(rl) if (!temp.isNullOrEmpty()) { textList.add(temp) if (ruleAnalyzes.elementsType == "||") { break } } } return textList.joinToString("\n") } } internal fun getStringList(rule: String): List { val result = ArrayList() if (rule.isEmpty()) return result val ruleAnalyzes = RuleAnalyzer(rule, true) //设置平衡组为代码平衡 val rules = ruleAnalyzes.splitRule("&&", "||", "%%") if (rules.size == 1) { ruleAnalyzes.reSetPos() //将pos重置为0,复用解析器 val st = ruleAnalyzes.innerRule("{$.") { getString(it) } //替换所有{$.rule...} if (st.isEmpty()) { //st为空,表明无成功替换的内嵌规则 try { val obj = ctx.read(rule) //kotlin的Any型返回值不包含null ,删除赘余 ?: return result if (obj is List<*>) { for (o in obj) result.add(o.toString()) } else { result.add(obj.toString()) } } catch (e: Exception) { e.printStackTrace() } } else { result.add(st) } return result } else { val results = ArrayList>() for (rl in rules) { val temp = getStringList(rl) if (temp.isNotEmpty()) { results.add(temp) if (temp.isNotEmpty() && ruleAnalyzes.elementsType == "||") { break } } } if (results.size > 0) { if ("%%" == ruleAnalyzes.elementsType) { for (i in results[0].indices) { for (temp in results) { if (i < temp.size) { result.add(temp[i]) } } } } else { for (temp in results) { result.addAll(temp) } } } return result } } internal fun getObject(rule: String): Any { return ctx.read(rule) } internal fun getList(rule: String): ArrayList? { val result = ArrayList() if (rule.isEmpty()) return result val ruleAnalyzes = RuleAnalyzer(rule, true) //设置平衡组为代码平衡 val rules = ruleAnalyzes.splitRule("&&", "||", "%%") if (rules.size == 1) { ctx.let { try { return it.read>(rules[0]) } catch (e: Exception) { e.printStackTrace() } } } else { val results = ArrayList>() for (rl in rules) { val temp = getList(rl) if (temp != null && temp.isNotEmpty()) { results.add(temp) if (temp.isNotEmpty() && ruleAnalyzes.elementsType == "||") { break } } } if (results.size > 0) { if ("%%" == ruleAnalyzes.elementsType) { for (i in 0 until results[0].size) { for (temp in results) { if (i < temp.size) { temp[i]?.let { result.add(it) } } } } } else { for (temp in results) { result.addAll(temp) } } } } return result } } ================================================ FILE: src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt ================================================ package io.legado.app.model.analyzeRule import org.jsoup.Jsoup import org.jsoup.nodes.Element import org.jsoup.select.Collector import org.jsoup.select.Elements import org.jsoup.select.Evaluator import org.seimicrawler.xpath.JXNode import java.util.* /** * Created by GKF on 2018/1/25. * 书源规则解析 */ class AnalyzeByJSoup(doc: Any) { companion object { /** * "class", "id", "tag", "text", "children" */ val validKeys = arrayOf("class", "id", "tag", "text", "children") fun parse(doc: Any): Element { return when (doc) { is Element -> doc is JXNode -> if (doc.isElement) doc.asElement() else Jsoup.parse(doc.toString()) else -> Jsoup.parse(doc.toString()) } } } private var element: Element = parse(doc) /** * 获取列表 */ internal fun getElements(rule: String) = getElements(element, rule) /** * 合并内容列表,得到内容 */ internal fun getString(ruleStr: String) = if (ruleStr.isEmpty()) null else getStringList(ruleStr).takeIf { it.isNotEmpty() }?.joinToString("\n") /** * 获取一个字符串 */ internal fun getString0(ruleStr: String) = getStringList(ruleStr).let { if (it.isEmpty()) "" else it[0] } /** * 获取所有内容列表 */ internal fun getStringList(ruleStr: String): List { val textS = ArrayList() if (ruleStr.isEmpty()) return textS //拆分规则 val sourceRule = SourceRule(ruleStr) if (sourceRule.elementsRule.isEmpty()) { textS.add(element.data() ?: "") } else { val ruleAnalyzes = RuleAnalyzer(sourceRule.elementsRule) val ruleStrS = ruleAnalyzes.splitRule("&&", "||", "%%") val results = ArrayList>() for (ruleStrX in ruleStrS) { val temp: List? = if (sourceRule.isCss) { val lastIndex = ruleStrX.lastIndexOf('@') getResultLast( element.select(ruleStrX.substring(0, lastIndex)), ruleStrX.substring(lastIndex + 1) ) } else { getResultList(ruleStrX) } if (!temp.isNullOrEmpty()) { results.add(temp) if (ruleAnalyzes.elementsType == "||") break } } if (results.size > 0) { if ("%%" == ruleAnalyzes.elementsType) { for (i in results[0].indices) { for (temp in results) { if (i < temp.size) { textS.add(temp[i]) } } } } else { for (temp in results) { textS.addAll(temp) } } } } return textS } /** * 获取Elements */ private fun getElements(temp: Element?, rule: String): Elements { if (temp == null || rule.isEmpty()) return Elements() val elements = Elements() val sourceRule = SourceRule(rule) val ruleAnalyzes = RuleAnalyzer(sourceRule.elementsRule) val ruleStrS = ruleAnalyzes.splitRule("&&", "||", "%%") val elementsList = ArrayList() if (sourceRule.isCss) { for (ruleStr in ruleStrS) { val tempS = temp.select(ruleStr) elementsList.add(tempS) if (tempS.size > 0 && ruleAnalyzes.elementsType == "||") { break } } } else { for (ruleStr in ruleStrS) { val rsRule = RuleAnalyzer(ruleStr) rsRule.trim() // 修剪当前规则之前的"@"或者空白符 val rs = rsRule.splitRule("@") val el = if (rs.size > 1) { val el = Elements() el.add(temp) for (rl in rs) { val es = Elements() for (et in el) { es.addAll(getElements(et, rl)) } el.clear() el.addAll(es) } el } else ElementsSingle().getElementsSingle(temp, ruleStr) elementsList.add(el) if (el.size > 0 && ruleAnalyzes.elementsType == "||") { break } } } if (elementsList.size > 0) { if ("%%" == ruleAnalyzes.elementsType) { for (i in 0 until elementsList[0].size) { for (es in elementsList) { if (i < es.size) { elements.add(es[i]) } } } } else { for (es in elementsList) { elements.addAll(es) } } } return elements } /** * 获取内容列表 */ private fun getResultList(ruleStr: String): List? { if (ruleStr.isEmpty()) return null var elements = Elements() elements.add(element) val rule = RuleAnalyzer(ruleStr) //创建解析 rule.trim() //修建前置赘余符号 val rules = rule.splitRule("@") // 切割成列表 val last = rules.size - 1 for (i in 0 until last) { val es = Elements() for (elt in elements) { es.addAll(ElementsSingle().getElementsSingle(elt, rules[i])) } elements.clear() elements = es } return if (elements.isEmpty()) null else getResultLast(elements, rules[last]) } /** * 根据最后一个规则获取内容 */ private fun getResultLast(elements: Elements, lastRule: String): List { val textS = ArrayList() when (lastRule) { "text" -> for (element in elements) { val text = element.text() if (text.isNotEmpty()) { textS.add(text) } } "textNodes" -> for (element in elements) { val tn = arrayListOf() val contentEs = element.textNodes() for (item in contentEs) { val text = item.text().trim { it <= ' ' } if (text.isNotEmpty()) { tn.add(text) } } if (tn.isNotEmpty()) { textS.add(tn.joinToString("\n")) } } "ownText" -> for (element in elements) { val text = element.ownText() if (text.isNotEmpty()) { textS.add(text) } } "html" -> { elements.select("script").remove() elements.select("style").remove() val html = elements.outerHtml() if (html.isNotEmpty()) { textS.add(html) } } "all" -> textS.add(elements.outerHtml()) else -> for (element in elements) { val url = element.attr(lastRule) if (url.isBlank() || textS.contains(url)) continue textS.add(url) } } return textS } /** * 1.支持阅读原有写法,':'分隔索引,!或.表示筛选方式,索引可为负数 * 例如 tag.div.-1:10:2 或 tag.div!0:3 * * 2. 支持与jsonPath类似的[]索引写法 * 格式形如 [it,it,。。。] 或 [!it,it,。。。] 其中[!开头表示筛选方式为排除,it为单个索引或区间。 * 区间格式为 start:end 或 start:end:step,其中start为0可省略,end为-1可省略。 * 索引,区间两端及间隔都支持负数 * 例如 tag.div[-1, 3:-2:-10, 2] * 特殊用法 tag.div[-1:0] 可在任意地方让列表反向 * */ data class ElementsSingle( var split: Char = '.', var beforeRule: String = "", val indexDefault: MutableList = mutableListOf(), val indexes: MutableList = mutableListOf() ) { /** * 获取Elements按照一个规则 */ fun getElementsSingle(temp: Element, rule: String): Elements { findIndexSet(rule) //执行索引列表处理器 /** * 获取所有元素 * */ var elements = if (beforeRule.isEmpty()) temp.children() //允许索引直接作为根元素,此时前置规则为空,效果与children相同 else { val rules = beforeRule.split(".") when (rules[0]) { "children" -> temp.children() //允许索引直接作为根元素,此时前置规则为空,效果与children相同 "class" -> temp.getElementsByClass(rules[1]) "tag" -> temp.getElementsByTag(rules[1]) "id" -> Collector.collect(Evaluator.Id(rules[1]), temp) "text" -> temp.getElementsContainingOwnText(rules[1]) else -> temp.select(beforeRule) } } val len = elements.size val lastIndexes = (indexDefault.size - 1).takeIf { it != -1 } ?: indexes.size - 1 val indexSet = mutableSetOf() /** * 获取无重且不越界的索引集合 * */ if (indexes.isEmpty()) for (ix in lastIndexes downTo 0) { //indexes为空,表明是非[]式索引,集合是逆向遍历插入的,所以这里也逆向遍历,好还原顺序 val it = indexDefault[ix] if (it in 0 until len) indexSet.add(it) //将正数不越界的索引添加到集合 else if (it < 0 && len >= -it) indexSet.add(it + len) //将负数不越界的索引添加到集合 } else for (ix in lastIndexes downTo 0) { //indexes不空,表明是[]式索引,集合是逆向遍历插入的,所以这里也逆向遍历,好还原顺序 if (indexes[ix] is Triple<*, *, *>) { //区间 val (startX, endX, stepX) = indexes[ix] as Triple //还原储存时的类型 val start = if (startX == null) 0 //左端省略表示0 else if (startX >= 0) if (startX < len) startX else len - 1 //右端越界,设置为最大索引 else if (-startX <= len) len + startX /* 将负索引转正 */ else 0 //左端越界,设置为最小索引 val end = if (endX == null) len - 1 //右端省略表示 len - 1 else if (endX >= 0) if (endX < len) endX else len - 1 //右端越界,设置为最大索引 else if (-endX <= len) len + endX /* 将负索引转正 */ else 0 //左端越界,设置为最小索引 if (start == end || stepX >= len) { //两端相同,区间里只有一个数。或间隔过大,区间实际上仅有首位 indexSet.add(start) continue } val step = if (stepX > 0) stepX else if (-stepX < len) stepX + len else 1 //最小正数间隔为1 //将区间展开到集合中,允许列表反向。 indexSet.addAll(if (end > start) start..end step step else start downTo end step step) } else {//单个索引 val it = indexes[ix] as Int //还原储存时的类型 if (it in 0 until len) indexSet.add(it) //将正数不越界的索引添加到集合 else if (it < 0 && len >= -it) indexSet.add(it + len) //将负数不越界的索引添加到集合 } } /** * 根据索引集合筛选元素 * */ if (split == '!') { //排除 for (pcInt in indexSet) elements[pcInt] = null elements.removeAll(listOf(null)) //测试过,这样就行 } else if (split == '.') { //选择 val es = Elements() for (pcInt in indexSet) es.add(elements[pcInt]) elements = es } return elements //返回筛选结果 } private fun findIndexSet(rule: String) { val rus = rule.trim { it <= ' ' } var len = rus.length var curInt: Int? //当前数字 var curMinus = false //当前数字是否为负 val curList = mutableListOf() //当前数字区间 var l = "" //暂存数字字符串 val head = rus.last() == ']' //是否为常规索引写法 if (head) { //常规索引写法[index...] len-- //跳过尾部']' while (len-- >= 0) { //逆向遍历,可以无前置规则 var rl = rus[len] if (rl == ' ') continue //跳过空格 if (rl in '0'..'9') l = rl + l //将数值累接入临时字串中,遇到分界符才取出 else if (rl == '-') curMinus = true else { curInt = if (l.isEmpty()) null else if (curMinus) -l.toInt() else l.toInt() //当前数字 when (rl) { ':' -> curList.add(curInt) //区间右端或区间间隔 else -> { //为保证查找顺序,区间和单个索引都添加到同一集合 if (curList.isEmpty()) { if (curInt == null) break //是jsoup选择器而非索引列表,跳出 indexes.add(curInt) } else { //列表最后压入的是区间右端,若列表有两位则最先压入的是间隔 indexes.add( Triple( curInt, curList.last(), if (curList.size == 2) curList.first() else 1 ) ) curList.clear() //重置临时列表,避免影响到下个区间的处理 } if (rl == '!') { split = '!' do { rl = rus[--len] } while (len > 0 && rl == ' ')//跳过所有空格 } if (rl == '[') { beforeRule = rus.substring(0, len) //遇到索引边界,返回结果 return } if (rl != ',') break //非索引结构,跳出 } } l = "" //清空 curMinus = false //重置 } } } else while (len-- >= 0) { //阅读原本写法,逆向遍历,可以无前置规则 val rl = rus[len] if (rl == ' ') continue //跳过空格 if (rl in '0'..'9') l = rl + l //将数值累接入临时字串中,遇到分界符才取出 else if (rl == '-') curMinus = true else { if (rl == '!' || rl == '.' || rl == ':') { //分隔符或起始符 indexDefault.add(if (curMinus) -l.toInt() else l.toInt()) // 当前数字追加到列表 if (rl != ':') { //rl == '!' || rl == '.' split = rl beforeRule = rus.substring(0, len) return } } else break //非索引结构,跳出循环 l = "" //清空 curMinus = false //重置 } } split = ' ' beforeRule = rus } } internal inner class SourceRule(ruleStr: String) { var isCss = false var elementsRule: String = if (ruleStr.startsWith("@CSS:", true)) { isCss = true ruleStr.substring(5).trim { it <= ' ' } } else { ruleStr } } } ================================================ FILE: src/main/java/io/legado/app/model/analyzeRule/AnalyzeByRegex.kt ================================================ package io.legado.app.model.analyzeRule import java.util.* import java.util.regex.Pattern object AnalyzeByRegex { fun getElement(res: String, regs: Array, index: Int = 0): List? { var vIndex = index val resM = Pattern.compile(regs[vIndex]).matcher(res) if (!resM.find()) { return null } // 判断索引的规则是最后一个规则 return if (vIndex + 1 == regs.size) { // 新建容器 val info = arrayListOf() for (groupIndex in 0..resM.groupCount()) { info.add(resM.group(groupIndex)!!) } info } else { val result = StringBuilder() do { result.append(resM.group()) } while (resM.find()) getElement(result.toString(), regs, ++vIndex) } } fun getElements(res: String, regs: Array, index: Int = 0): List> { var vIndex = index val resM = Pattern.compile(regs[vIndex]).matcher(res) if (!resM.find()) { return arrayListOf() } // 判断索引的规则是最后一个规则 if (vIndex + 1 == regs.size) { // 创建书息缓存数组 val books = ArrayList>() // 提取列表 do { // 新建容器 val info = arrayListOf() for (groupIndex in 0..resM.groupCount()) { info.add(resM.group(groupIndex) ?: "") } books.add(info) } while (resM.find()) return books } else { val result = StringBuilder() do { result.append(resM.group()) } while (resM.find()) return getElements(result.toString(), regs, ++vIndex) } } } ================================================ FILE: src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt ================================================ package io.legado.app.model.analyzeRule import io.legado.app.utils.splitNotBlank import io.legado.app.utils.TextUtils import org.jsoup.nodes.Document import org.jsoup.nodes.Element import org.jsoup.select.Elements import org.seimicrawler.xpath.JXDocument import org.seimicrawler.xpath.JXNode import java.util.* class AnalyzeByXPath(doc: Any) { private var jxNode: Any = parse(doc) private fun parse(doc: Any): Any { return when (doc) { is JXNode -> if (doc.isElement) doc else strToJXDocument(doc.toString()) is Document -> JXDocument.create(doc) is Element -> JXDocument.create(Elements(doc)) is Elements -> JXDocument.create(doc) else -> strToJXDocument(doc.toString()) } } private fun strToJXDocument(html: String): JXDocument { var html1 = html if (html1.endsWith("")) { html1 = "${html1}" } if (html1.endsWith("") || html1.endsWith("")) { html1 = "${html1}
" } return JXDocument.create(html1) } private fun getResult(xPath: String): List? { val node = jxNode return if (node is JXNode) { node.sel(xPath) } else { (node as JXDocument).selN(xPath) } } internal fun getElements(xPath: String): List? { if (xPath.isEmpty()) return null val jxNodes = ArrayList() val ruleAnalyzes = RuleAnalyzer(xPath) val rules = ruleAnalyzes.splitRule("&&", "||", "%%") if (rules.size == 1) { return getResult(rules[0]) } else { val results = ArrayList>() for (rl in rules) { val temp = getElements(rl) if (temp != null && temp.isNotEmpty()) { results.add(temp) if (temp.isNotEmpty() && ruleAnalyzes.elementsType == "||") { break } } } if (results.size > 0) { if ("%%" == ruleAnalyzes.elementsType) { for (i in results[0].indices) { for (temp in results) { if (i < temp.size) { jxNodes.add(temp[i]) } } } } else { for (temp in results) { jxNodes.addAll(temp) } } } } return jxNodes } internal fun getStringList(xPath: String): List { val result = ArrayList() val ruleAnalyzes = RuleAnalyzer(xPath) val rules = ruleAnalyzes.splitRule("&&", "||", "%%") if (rules.size == 1) { getResult(xPath)?.map { result.add(it.asString()) } return result } else { val results = ArrayList>() for (rl in rules) { val temp = getStringList(rl) if (temp.isNotEmpty()) { results.add(temp) if (temp.isNotEmpty() && ruleAnalyzes.elementsType == "||") { break } } } if (results.size > 0) { if ("%%" == ruleAnalyzes.elementsType) { for (i in results[0].indices) { for (temp in results) { if (i < temp.size) { result.add(temp[i]) } } } } else { for (temp in results) { result.addAll(temp) } } } } return result } fun getString(rule: String): String? { val ruleAnalyzes = RuleAnalyzer(rule) val rules = ruleAnalyzes.splitRule("&&", "||") if (rules.size == 1) { getResult(rule)?.let { return TextUtils.join("\n", it) } return null } else { val textList = arrayListOf() for (rl in rules) { val temp = getString(rl) if (!temp.isNullOrEmpty()) { textList.add(temp) if (ruleAnalyzes.elementsType == "||") { break } } } return textList.joinToString("\n") } } } ================================================ FILE: src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt ================================================ package io.legado.app.model.analyzeRule import com.script.SimpleBindings import io.legado.app.constant.AppConst.SCRIPT_ENGINE import io.legado.app.constant.AppPattern.JS_PATTERN import io.legado.app.data.entities.BaseBook import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BaseSource import io.legado.app.help.CacheManager import io.legado.app.help.JsExtensions import io.legado.app.help.http.CookieStore import io.legado.app.utils.* import kotlinx.coroutines.runBlocking import org.jsoup.nodes.Entities import org.mozilla.javascript.NativeObject import java.net.URL import java.util.* import java.util.regex.Pattern import kotlin.collections.HashMap import mu.KotlinLogging import io.legado.app.model.analyzeRule.RuleDataInterface import io.legado.app.model.webBook.WebBook private val logger = KotlinLogging.logger {} /** * 解析规则获取结果 */ @Suppress("unused", "RegExpRedundantEscape") class AnalyzeRule( var ruleData: RuleDataInterface, private val source: BaseSource? = null ) : JsExtensions { val book get() = ruleData as? BaseBook var chapter: BookChapter? = null var nextChapterUrl: String? = null var content: Any? = null private set var baseUrl: String? = null private set var redirectUrl: URL? = null private set private var isJSON: Boolean = false private var isRegex: Boolean = false private var analyzeByXPath: AnalyzeByXPath? = null private var analyzeByJSoup: AnalyzeByJSoup? = null private var analyzeByJSonPath: AnalyzeByJSonPath? = null private var objectChangedXP = false private var objectChangedJS = false private var objectChangedJP = false @JvmOverloads fun setContent(content: Any?, baseUrl: String? = null): AnalyzeRule { if (content == null) throw AssertionError("内容不可空(Content cannot be null)") this.content = content isJSON = content.toString().isJson() setBaseUrl(baseUrl) objectChangedXP = true objectChangedJS = true objectChangedJP = true return this } fun setBaseUrl(baseUrl: String?): AnalyzeRule { baseUrl?.let { this.baseUrl = baseUrl } return this } fun setRedirectUrl(url: String): URL? { try { redirectUrl = URL(url) } catch (e: Exception) { log("URL($url) error\n${e.localizedMessage}") } return redirectUrl } /** * 获取XPath解析类 */ private fun getAnalyzeByXPath(o: Any): AnalyzeByXPath { return if (o != content) { AnalyzeByXPath(o) } else { if (analyzeByXPath == null || objectChangedXP) { analyzeByXPath = AnalyzeByXPath(content!!) objectChangedXP = false } analyzeByXPath!! } } /** * 获取JSOUP解析类 */ private fun getAnalyzeByJSoup(o: Any): AnalyzeByJSoup { return if (o != content) { AnalyzeByJSoup(o) } else { if (analyzeByJSoup == null || objectChangedJS) { analyzeByJSoup = AnalyzeByJSoup(content!!) objectChangedJS = false } analyzeByJSoup!! } } /** * 获取JSON解析类 */ private fun getAnalyzeByJSonPath(o: Any): AnalyzeByJSonPath { return if (o != content) { AnalyzeByJSonPath(o) } else { if (analyzeByJSonPath == null || objectChangedJP) { analyzeByJSonPath = AnalyzeByJSonPath(content!!) objectChangedJP = false } analyzeByJSonPath!! } } /** * 获取文本列表 */ @JvmOverloads fun getStringList(rule: String?, mContent: Any? = null, isUrl: Boolean = false): List? { if (rule.isNullOrEmpty()) return null val ruleList = splitSourceRule(rule, false) return getStringList(ruleList, mContent, isUrl) } @JvmOverloads fun getStringList( ruleList: List, mContent: Any? = null, isUrl: Boolean = false ): List? { var result: Any? = null val content = mContent ?: this.content if (content != null && ruleList.isNotEmpty()) { result = content if (content is NativeObject) { result = content[ruleList[0].rule]?.toString() } else { for (sourceRule in ruleList) { putRule(sourceRule.putMap) sourceRule.makeUpRule(result) result?.let { if (sourceRule.rule.isNotEmpty()) { result = when (sourceRule.mode) { Mode.Js -> evalJS(sourceRule.rule, result) Mode.Json -> getAnalyzeByJSonPath(it).getStringList(sourceRule.rule) Mode.XPath -> getAnalyzeByXPath(it).getStringList(sourceRule.rule) Mode.Default -> getAnalyzeByJSoup(it).getStringList(sourceRule.rule) else -> sourceRule.rule } } if (sourceRule.replaceRegex.isNotEmpty() && result is List<*>) { val newList = ArrayList() for (item in result as List<*>) { newList.add(replaceRegex(item.toString(), sourceRule)) } result = newList } else if (sourceRule.replaceRegex.isNotEmpty()) { result = replaceRegex(result.toString(), sourceRule) } } } } } if (result == null) return null if (result is String) { result = (result as String).split("\n") } if (isUrl) { val urlList = ArrayList() if (result is List<*>) { for (url in result as List<*>) { val absoluteURL = NetworkUtils.getAbsoluteURL(redirectUrl, url.toString()) if (absoluteURL.isNotEmpty() && !urlList.contains(absoluteURL)) { urlList.add(absoluteURL) } } } return urlList } @Suppress("UNCHECKED_CAST") return result as? List } /** * 获取文本 */ @JvmOverloads fun getString(ruleStr: String?, mContent: Any? = null, isUrl: Boolean = false): String { if (TextUtils.isEmpty(ruleStr)) return "" val ruleList = splitSourceRule(ruleStr) return getString(ruleList, mContent, isUrl) } @JvmOverloads fun getString( ruleList: List, mContent: Any? = null, isUrl: Boolean = false ): String { var result: Any? = null val content = mContent ?: this.content if (content != null && ruleList.isNotEmpty()) { result = content if (result is NativeObject) { result = result[ruleList[0].rule]?.toString() } else { for (sourceRule in ruleList) { putRule(sourceRule.putMap) sourceRule.makeUpRule(result) result?.let { if (sourceRule.rule.isNotBlank() || sourceRule.replaceRegex.isEmpty()) { result = when (sourceRule.mode) { Mode.Js -> evalJS(sourceRule.rule, it) Mode.Json -> getAnalyzeByJSonPath(it).getString(sourceRule.rule) Mode.XPath -> getAnalyzeByXPath(it).getString(sourceRule.rule) Mode.Default -> if (isUrl) { getAnalyzeByJSoup(it).getString0(sourceRule.rule) } else { getAnalyzeByJSoup(it).getString(sourceRule.rule) } else -> sourceRule.rule } } if ((result != null) && sourceRule.replaceRegex.isNotEmpty()) { result = replaceRegex(result.toString(), sourceRule) } } } } } if (result == null) result = "" val str = kotlin.runCatching { Entities.unescape(result.toString()) }.onFailure { log("Entities.unescape() error\n${it.localizedMessage}") }.getOrElse { result.toString() } if (isUrl) { return if (str.isBlank()) { baseUrl ?: "" } else { NetworkUtils.getAbsoluteURL(redirectUrl, str) } } return str } /** * 获取Element */ fun getElement(ruleStr: String): Any? { if (TextUtils.isEmpty(ruleStr)) return null var result: Any? = null val content = this.content val ruleList = splitSourceRule(ruleStr, true) if (content != null && ruleList.isNotEmpty()) { result = content for (sourceRule in ruleList) { putRule(sourceRule.putMap) sourceRule.makeUpRule(result) result?.let { result = when (sourceRule.mode) { Mode.Regex -> AnalyzeByRegex.getElement( result.toString(), sourceRule.rule.splitNotBlank("&&") ) Mode.Js -> evalJS(sourceRule.rule, it) Mode.Json -> getAnalyzeByJSonPath(it).getObject(sourceRule.rule) Mode.XPath -> getAnalyzeByXPath(it).getElements(sourceRule.rule) else -> getAnalyzeByJSoup(it).getElements(sourceRule.rule) } if (sourceRule.replaceRegex.isNotEmpty()) { result = replaceRegex(result.toString(), sourceRule) } } } } return result } /** * 获取列表 */ @Suppress("UNCHECKED_CAST") fun getElements(ruleStr: String): List { var result: Any? = null val content = this.content val ruleList = splitSourceRule(ruleStr, true) if (content != null && ruleList.isNotEmpty()) { result = content for (sourceRule in ruleList) { putRule(sourceRule.putMap) result?.let { result = when (sourceRule.mode) { Mode.Regex -> AnalyzeByRegex.getElements( result.toString(), sourceRule.rule.splitNotBlank("&&") ) Mode.Js -> evalJS(sourceRule.rule, result) Mode.Json -> getAnalyzeByJSonPath(it).getList(sourceRule.rule) Mode.XPath -> getAnalyzeByXPath(it).getElements(sourceRule.rule) else -> getAnalyzeByJSoup(it).getElements(sourceRule.rule) } if (sourceRule.replaceRegex.isNotEmpty()) { result = replaceRegex(result.toString(), sourceRule) } } } } result?.let { return it as List } return ArrayList() } /** * 保存变量 */ private fun putRule(map: Map) { for ((key, value) in map) { put(key, getString(value)) } } /** * 分离put规则 */ private fun splitPutRule(ruleStr: String, putMap: HashMap): String { var vRuleStr = ruleStr val putMatcher = putPattern.matcher(vRuleStr) while (putMatcher.find()) { vRuleStr = vRuleStr.replace(putMatcher.group(), "") GSON.fromJsonObject>(putMatcher.group(1)) .getOrNull() ?.let { putMap.putAll(it) } } return vRuleStr } /** * 正则替换 */ private fun replaceRegex(result: String, rule: SourceRule): String { if (rule.replaceRegex.isEmpty()) return result var vResult = result vResult = if (rule.replaceFirst) { kotlin.runCatching { val pattern = Pattern.compile(rule.replaceRegex) val matcher = pattern.matcher(vResult) if (matcher.find()) { matcher.group(0)!!.replaceFirst(rule.replaceRegex.toRegex(), rule.replacement) } else { "" } }.getOrElse { vResult.replaceFirst(rule.replaceRegex, rule.replacement) } } else { kotlin.runCatching { vResult.replace(rule.replaceRegex.toRegex(), rule.replacement) }.getOrElse { vResult.replace(rule.replaceRegex, rule.replacement) } } return vResult } /** * 分解规则生成规则列表 */ fun splitSourceRule(ruleStr: String?, allInOne: Boolean = false): List { if (ruleStr.isNullOrEmpty()) return emptyList() val ruleList = ArrayList() var mMode: Mode = Mode.Default var start = 0 //仅首字符为:时为AllInOne,其实:与伪类选择器冲突,建议改成?更合理 if (allInOne && ruleStr.startsWith(":")) { mMode = Mode.Regex isRegex = true start = 1 } else if (isRegex) { mMode = Mode.Regex } var tmp: String val jsMatcher = JS_PATTERN.matcher(ruleStr) while (jsMatcher.find()) { if (jsMatcher.start() > start) { tmp = ruleStr.substring(start, jsMatcher.start()).trim { it <= ' ' } if (tmp.isNotEmpty()) { ruleList.add(SourceRule(tmp, mMode)) } } ruleList.add(SourceRule(jsMatcher.group(2) ?: jsMatcher.group(1), Mode.Js)) start = jsMatcher.end() } if (ruleStr.length > start) { tmp = ruleStr.substring(start).trim { it <= ' ' } if (tmp.isNotEmpty()) { ruleList.add(SourceRule(tmp, mMode)) } } return ruleList } /** * 规则类 */ inner class SourceRule internal constructor( ruleStr: String, internal var mode: Mode = Mode.Default ) { internal var rule: String internal var replaceRegex = "" internal var replacement = "" internal var replaceFirst = false internal val putMap = HashMap() private val ruleParam = ArrayList() private val ruleType = ArrayList() private val getRuleType = -2 private val jsRuleType = -1 private val defaultRuleType = 0 init { rule = when { mode == Mode.Js || mode == Mode.Regex -> ruleStr ruleStr.startsWith("@CSS:", true) -> { mode = Mode.Default ruleStr } ruleStr.startsWith("@@") -> { mode = Mode.Default ruleStr.substring(2) } ruleStr.startsWith("@XPath:", true) -> { mode = Mode.XPath ruleStr.substring(7) } ruleStr.startsWith("@Json:", true) -> { mode = Mode.Json ruleStr.substring(6) } isJSON || ruleStr.startsWith("$.") || ruleStr.startsWith("$[") -> { mode = Mode.Json ruleStr } ruleStr.startsWith("/") -> {//XPath特征很明显,无需配置单独的识别标头 mode = Mode.XPath ruleStr } else -> ruleStr } //分离put rule = splitPutRule(rule, putMap) //@get,{{ }}, 拆分 var start = 0 var tmp: String val evalMatcher = evalPattern.matcher(rule) if (evalMatcher.find()) { tmp = rule.substring(start, evalMatcher.start()) if (mode != Mode.Js && mode != Mode.Regex && (evalMatcher.start() == 0 || !tmp.contains("##")) ) { mode = Mode.Regex } do { if (evalMatcher.start() > start) { tmp = rule.substring(start, evalMatcher.start()) splitRegex(tmp) } tmp = evalMatcher.group() when { tmp.startsWith("@get:", true) -> { ruleType.add(getRuleType) ruleParam.add(tmp.substring(6, tmp.lastIndex)) } tmp.startsWith("{{") -> { ruleType.add(jsRuleType) ruleParam.add(tmp.substring(2, tmp.length - 2)) } else -> { splitRegex(tmp) } } start = evalMatcher.end() } while (evalMatcher.find()) } if (rule.length > start) { tmp = rule.substring(start) splitRegex(tmp) } } /** * 拆分\$\d{1,2} */ private fun splitRegex(ruleStr: String) { var start = 0 var tmp: String val ruleStrArray = ruleStr.split("##") val regexMatcher = regexPattern.matcher(ruleStrArray[0]) if (regexMatcher.find()) { if (mode != Mode.Js && mode != Mode.Regex) { mode = Mode.Regex } do { if (regexMatcher.start() > start) { tmp = ruleStr.substring(start, regexMatcher.start()) ruleType.add(defaultRuleType) ruleParam.add(tmp) } tmp = regexMatcher.group() ruleType.add(tmp.substring(1).toInt()) ruleParam.add(tmp) start = regexMatcher.end() } while (regexMatcher.find()) } if (ruleStr.length > start) { tmp = ruleStr.substring(start) ruleType.add(defaultRuleType) ruleParam.add(tmp) } } /** * 替换@get,{{ }} */ fun makeUpRule(result: Any?) { val infoVal = StringBuilder() if (ruleParam.isNotEmpty()) { var index = ruleParam.size while (index-- > 0) { val regType = ruleType[index] when { regType > defaultRuleType -> { @Suppress("UNCHECKED_CAST") (result as? List)?.run { if (this.size > regType) { this[regType]?.let { infoVal.insert(0, it) } } } ?: infoVal.insert(0, ruleParam[index]) } regType == jsRuleType -> { if (isRule(ruleParam[index])) { getString(arrayListOf(SourceRule(ruleParam[index]))).let { infoVal.insert(0, it) } } else { val jsEval: Any? = evalJS(ruleParam[index], result) when { jsEval == null -> Unit jsEval is String -> infoVal.insert(0, jsEval) jsEval is Double && jsEval % 1.0 == 0.0 -> infoVal.insert( 0, String.format("%.0f", jsEval) ) else -> infoVal.insert(0, jsEval.toString()) } } } regType == getRuleType -> { infoVal.insert(0, get(ruleParam[index])) } else -> infoVal.insert(0, ruleParam[index]) } } rule = infoVal.toString() } //分离正则表达式 val ruleStrS = rule.split("##") rule = ruleStrS[0].trim() if (ruleStrS.size > 1) { replaceRegex = ruleStrS[1] } if (ruleStrS.size > 2) { replacement = ruleStrS[2] } if (ruleStrS.size > 3) { replaceFirst = true } } private fun isRule(ruleStr: String): Boolean { return ruleStr.startsWith('@') //js首个字符不可能是@,除非是装饰器,所以@开头规定为规则 || ruleStr.startsWith("$.") || ruleStr.startsWith("$[") || ruleStr.startsWith("//") } } enum class Mode { XPath, Json, Default, Js, Regex } fun put(key: String, value: String): String { chapter?.putVariable(key, value) ?: book?.putVariable(key, value) ?: ruleData.putVariable(key, value) return value } fun get(key: String): String { when (key) { "bookName" -> book?.let { return it.name } "title" -> chapter?.let { return it.title } } return chapter?.getVariable(key) ?: book?.getVariable(key) ?: ruleData?.getVariable(key) ?: "" } /** * 执行JS */ fun evalJS(jsStr: String, result: Any?): Any? { val bindings = SimpleBindings() bindings["java"] = this bindings["cookie"] = CookieStore bindings["cache"] = CacheManager bindings["source"] = source bindings["book"] = book bindings["result"] = result bindings["baseUrl"] = baseUrl bindings["chapter"] = chapter bindings["title"] = chapter?.title bindings["src"] = content bindings["nextChapterUrl"] = nextChapterUrl return SCRIPT_ENGINE.eval(jsStr, bindings) } override fun getSource(): BaseSource? { return source } /** * js实现跨域访问,不能删 */ override fun ajax(urlStr: String): String? { return runBlocking { kotlin.runCatching { val analyzeUrl = AnalyzeUrl(urlStr, source = source, ruleData = book) analyzeUrl.getStrResponseAwait().body }.onFailure { log("ajax(${urlStr}) error\n${it.stackTraceToString()}") // it.printStackTrace() }.getOrElse { it.msg } } } /** * 章节数转数字 */ fun toNumChapter(s: String?): String? { s ?: return null val matcher = titleNumPattern.matcher(s) if (matcher.find()) { return "${matcher.group(1)}${StringUtils.stringToInt(matcher.group(2))}${matcher.group(3)}" } return s } /** * 更新BookUrl,如果搜索结果有tocUrl也会更新,有些书源bookUrl定期更新,可以在js内调用更新 */ fun refreshBookUrl() { runBlocking { val bookSource = source as? BookSource val book = book as? Book if (bookSource == null || book == null) return@runBlocking val books = WebBook(bookSource).searchBook(book.name) books.forEach { if (it.name == book.name && it.author == book.author) { book.bookUrl = it.bookUrl if (it.tocUrl.isNotBlank()) { book.tocUrl = it.tocUrl } return@runBlocking } } } } /** * 更新tocUrl,有些书源目录url定期更新,可以在js调用更新 */ fun refreshTocUrl() { runBlocking { val bookSource = source as? BookSource val book = book as? Book if (bookSource == null || book == null) return@runBlocking WebBook(bookSource).getBookInfo(book) } } companion object { private val putPattern = Pattern.compile("@put:(\\{[^}]+?\\})", Pattern.CASE_INSENSITIVE) private val evalPattern = Pattern.compile("@get:\\{[^}]+?\\}|\\{\\{[\\w\\W]*?\\}\\}", Pattern.CASE_INSENSITIVE) private val regexPattern = Pattern.compile("\\$\\d{1,2}") private val titleNumPattern = Pattern.compile("(第)(.+?)(章)") } } ================================================ FILE: src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt ================================================ package io.legado.app.model.analyzeRule import com.script.SimpleBindings import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst.SCRIPT_ENGINE import io.legado.app.constant.AppConst.UA_NAME import io.legado.app.constant.AppPattern.JS_PATTERN import io.legado.app.constant.AppPattern.dataUriRegex import io.legado.app.data.entities.BaseSource import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.exception.ConcurrentException import io.legado.app.help.CacheManager import io.legado.app.help.JsExtensions import io.legado.app.help.http.* import io.legado.app.utils.* import kotlinx.coroutines.runBlocking import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import java.net.URLEncoder import java.util.regex.Pattern import io.legado.app.model.DebugLog /** * Created by GKF on 2018/1/24. * 搜索URL规则解析 */ class AnalyzeUrl( val mUrl: String, val key: String? = null, val page: Int? = null, val speakText: String? = null, val speakSpeed: Int? = null, var baseUrl: String = "", private val source: BaseSource? = null, private val ruleData: RuleDataInterface? = null, private val chapter: BookChapter? = null, headerMapF: Map? = null, ) : JsExtensions { companion object { val paramPattern: Pattern = Pattern.compile("\\s*,\\s*(?=\\{)") private val pagePattern = Pattern.compile("<(.*?)>") private val concurrentRecordMap = hashMapOf() } var ruleUrl = "" private set var url: String = "" private set var body: String? = null private set var type: String? = null private set val headerMap = HashMap() private var urlNoQuery: String = "" private var queryStr: String? = null private val fieldMap = LinkedHashMap() private var charset: String? = null private var method = RequestMethod.GET private var proxy: String? = null private var retry: Int = 0 private var useWebView: Boolean = false private var webJs: String? = null init { if (!mUrl.isDataUrl()) { val urlMatcher = paramPattern.matcher(baseUrl) if (urlMatcher.find()) baseUrl = baseUrl.substring(0, urlMatcher.start()) (headerMapF ?: source?.getHeaderMap(true))?.let { headerMap.putAll(it) if (it.containsKey("proxy")) { proxy = it["proxy"] headerMap.remove("proxy") } } initUrl() } } /** * 处理url */ fun initUrl() { ruleUrl = mUrl //执行@js, analyzeJs() //替换参数 replaceKeyPageJs() //处理URL analyzeUrl() } /** * 执行@js, */ private fun analyzeJs() { var start = 0 var tmp: String val jsMatcher = JS_PATTERN.matcher(ruleUrl) while (jsMatcher.find()) { if (jsMatcher.start() > start) { tmp = ruleUrl.substring(start, jsMatcher.start()).trim { it <= ' ' } if (tmp.isNotEmpty()) { ruleUrl = tmp.replace("@result", ruleUrl) } } ruleUrl = evalJS(jsMatcher.group(2) ?: jsMatcher.group(1), ruleUrl) as String start = jsMatcher.end() } if (ruleUrl.length > start) { tmp = ruleUrl.substring(start).trim { it <= ' ' } if (tmp.isNotEmpty()) { ruleUrl = tmp.replace("@result", ruleUrl) } } } /** * 替换关键字,页数,JS */ private fun replaceKeyPageJs() { //先替换内嵌规则再替换页数规则,避免内嵌规则中存在大于小于号时,规则被切错 //js if (ruleUrl.contains("{{") && ruleUrl.contains("}}")) { val analyze = RuleAnalyzer(ruleUrl) //创建解析 //替换所有内嵌{{js}} val url = analyze.innerRule("{{", "}}") { val jsEval = evalJS(it) ?: "" when { jsEval is String -> jsEval jsEval is Double && jsEval % 1.0 == 0.0 -> String.format("%.0f", jsEval) else -> jsEval.toString() } } if (url.isNotEmpty()) ruleUrl = url } //page page?.let { val matcher = pagePattern.matcher(ruleUrl) while (matcher.find()) { val pages = matcher.group(1)!!.split(",") ruleUrl = if (page < pages.size) { //pages[pages.size - 1]等同于pages.last() ruleUrl.replace(matcher.group(), pages[page - 1].trim { it <= ' ' }) } else { ruleUrl.replace(matcher.group(), pages.last().trim { it <= ' ' }) } } } } /** * 解析Url */ private fun analyzeUrl() { //replaceKeyPageJs已经替换掉额外内容,此处url是基础形式,可以直接切首个‘,’之前字符串。 val urlMatcher = paramPattern.matcher(ruleUrl) val urlNoOption = if (urlMatcher.find()) ruleUrl.substring(0, urlMatcher.start()) else ruleUrl url = NetworkUtils.getAbsoluteURL(baseUrl, urlNoOption) NetworkUtils.getBaseUrl(url)?.let { baseUrl = it } if (urlNoOption.length != ruleUrl.length) { GSON.fromJsonObject(ruleUrl.substring(urlMatcher.end())).getOrNull() ?.let { option -> option.getMethod()?.let { if (it.equals("POST", true)) method = RequestMethod.POST } option.getHeaderMap()?.forEach { entry -> headerMap[entry.key.toString()] = entry.value.toString() } option.getBody()?.let { body = it } type = option.getType() charset = option.getCharset() retry = option.getRetry() useWebView = option.useWebView() webJs = option.getWebJs() option.getJs()?.let { jsStr -> evalJS(jsStr, url)?.toString()?.let { url = it } } } } headerMap[UA_NAME] ?: let { headerMap[UA_NAME] = AppConst.userAgent } urlNoQuery = url when (method) { RequestMethod.GET -> { val pos = url.indexOf('?') if (pos != -1) { analyzeFields(url.substring(pos + 1)) urlNoQuery = url.substring(0, pos) } } RequestMethod.POST -> body?.let { if (!it.isJson() && !it.isXml() && headerMap["Content-Type"].isNullOrEmpty()) { analyzeFields(it) } } } } /** * 解析QueryMap */ private fun analyzeFields(fieldsTxt: String) { queryStr = fieldsTxt val queryS = fieldsTxt.splitNotBlank("&") for (query in queryS) { val queryM = query.splitNotBlank("=") val value = if (queryM.size > 1) queryM[1] else "" if (charset.isNullOrEmpty()) { if (NetworkUtils.hasUrlEncoded(value)) { fieldMap[queryM[0]] = value } else { fieldMap[queryM[0]] = URLEncoder.encode(value, "UTF-8") } } else if (charset == "escape") { fieldMap[queryM[0]] = EncoderUtils.escape(value) } else { fieldMap[queryM[0]] = URLEncoder.encode(value, charset) } } } /** * 执行JS */ fun evalJS(jsStr: String, result: Any? = null): Any? { val bindings = SimpleBindings() bindings["java"] = this bindings["baseUrl"] = baseUrl bindings["cookie"] = CookieStore bindings["cache"] = CacheManager bindings["page"] = page bindings["key"] = key bindings["speakText"] = speakText bindings["speakSpeed"] = speakSpeed bindings["book"] = ruleData as? Book bindings["source"] = source bindings["result"] = result return SCRIPT_ENGINE.eval(jsStr, bindings) } fun put(key: String, value: String): String { chapter?.putVariable(key, value) ?: ruleData?.putVariable(key, value) return value } fun get(key: String): String { when (key) { "bookName" -> (ruleData as? Book)?.let { return it.name } "title" -> chapter?.let { return it.title } } return chapter?.getVariable(key) ?: ruleData?.getVariable(key) ?: "" } /** * 开始访问,并发判断 */ private fun fetchStart(): ConcurrentRecord? { source ?: return null val concurrentRate = source.concurrentRate if (concurrentRate.isNullOrEmpty()) { return null } val rateIndex = concurrentRate.indexOf("/") var fetchRecord = concurrentRecordMap[source.getKey()] if (fetchRecord == null) { fetchRecord = ConcurrentRecord(rateIndex > 0, System.currentTimeMillis(), 1) concurrentRecordMap[source.getKey()] = fetchRecord return fetchRecord } val waitTime: Int = synchronized(fetchRecord) { try { if (rateIndex == -1) { if (fetchRecord.frequency > 0) { return@synchronized concurrentRate.toInt() } val nextTime = fetchRecord.time + concurrentRate.toInt() if (System.currentTimeMillis() >= nextTime) { fetchRecord.time = System.currentTimeMillis() fetchRecord.frequency = 1 return@synchronized 0 } return@synchronized (nextTime - System.currentTimeMillis()).toInt() } else { val sj = concurrentRate.substring(rateIndex + 1) val nextTime = fetchRecord.time + sj.toInt() if (System.currentTimeMillis() >= nextTime) { fetchRecord.time = System.currentTimeMillis() fetchRecord.frequency = 1 return@synchronized 0 } val cs = concurrentRate.substring(0, rateIndex) if (fetchRecord.frequency > cs.toInt()) { return@synchronized (nextTime - System.currentTimeMillis()).toInt() } else { fetchRecord.frequency = fetchRecord.frequency + 1 return@synchronized 0 } } } catch (e: Exception) { return@synchronized 0 } } if (waitTime > 0) { throw ConcurrentException("根据并发率还需等待${waitTime}毫秒才可以访问", waitTime = waitTime) } return fetchRecord } /** * 访问结束 */ private fun fetchEnd(concurrentRecord: ConcurrentRecord?) { if (concurrentRecord != null && !concurrentRecord.concurrent) { synchronized(concurrentRecord) { concurrentRecord.frequency = concurrentRecord.frequency - 1 } } } /** * 访问网站,返回StrResponse */ suspend fun getStrResponseAwait( jsStr: String? = null, sourceRegex: String? = null, useWebView: Boolean = true, debugLog: DebugLog? = null ): StrResponse { if (type != null) { return StrResponse(url, StringUtils.byteToHexString(getByteArrayAwait())) } val concurrentRecord = fetchStart() setCookie(source?.getKey()) val strResponse: StrResponse if (this.useWebView && useWebView) { throw Exception("不支持webview") } else { strResponse = getProxyClient(proxy, debugLog).newCallStrResponse(retry) { addHeaders(headerMap) when (method) { RequestMethod.POST -> { url(urlNoQuery) val contentType = headerMap["Content-Type"] val body = body if (fieldMap.isNotEmpty() || body.isNullOrBlank()) { postForm(fieldMap, true) } else if (!contentType.isNullOrBlank()) { val requestBody = body.toRequestBody(contentType.toMediaType()) post(requestBody) } else { postJson(body) } } else -> get(urlNoQuery, fieldMap, true) } } } fetchEnd(concurrentRecord) return strResponse } @JvmOverloads fun getStrResponse( jsStr: String? = null, sourceRegex: String? = null, useWebView: Boolean = true, debugLog: DebugLog? = null ): StrResponse { return runBlocking { getStrResponseAwait(jsStr, sourceRegex, useWebView, debugLog) } } /** * 访问网站,返回Response */ suspend fun getResponseAwait(): Response { val concurrentRecord = fetchStart() setCookie(source?.getKey()) @Suppress("BlockingMethodInNonBlockingContext") val response = getProxyClient(proxy).newCallResponse(retry) { addHeaders(headerMap) when (method) { RequestMethod.POST -> { url(urlNoQuery) val contentType = headerMap["Content-Type"] val body = body if (fieldMap.isNotEmpty() || body.isNullOrBlank()) { postForm(fieldMap, true) } else if (!contentType.isNullOrBlank()) { val requestBody = body.toRequestBody(contentType.toMediaType()) post(requestBody) } else { postJson(body) } } else -> get(urlNoQuery, fieldMap, true) } } fetchEnd(concurrentRecord) return response } fun getResponse(): Response { return runBlocking { getResponseAwait() } } /** * 访问网站,返回ByteArray */ suspend fun getByteArrayAwait(): ByteArray { val concurrentRecord = fetchStart() @Suppress("RegExpRedundantEscape") val dataUriFindResult = dataUriRegex.find(urlNoQuery) @Suppress("BlockingMethodInNonBlockingContext") if (dataUriFindResult != null) { val dataUriBase64 = dataUriFindResult.groupValues[1] val byteArray = Base64.decode(dataUriBase64, Base64.DEFAULT) fetchEnd(concurrentRecord) return byteArray } else { setCookie(source?.getKey()) val byteArray = getProxyClient(proxy).newCallResponseBody(retry) { addHeaders(headerMap) when (method) { RequestMethod.POST -> { url(urlNoQuery) val contentType = headerMap["Content-Type"] val body = body if (fieldMap.isNotEmpty() || body.isNullOrBlank()) { postForm(fieldMap, true) } else if (!contentType.isNullOrBlank()) { val requestBody = body.toRequestBody(contentType.toMediaType()) post(requestBody) } else { postJson(body) } } else -> get(urlNoQuery, fieldMap, true) } }.bytes() fetchEnd(concurrentRecord) return byteArray } } fun getByteArray(): ByteArray { return runBlocking { getByteArrayAwait() } } /** * 上传文件 */ suspend fun upload(fileName: String, file: Any, contentType: String): StrResponse { return getProxyClient(proxy).newCallStrResponse(retry) { url(urlNoQuery) val bodyMap = GSON.fromJsonObject>(body).getOrNull()!! bodyMap.forEach { entry -> if (entry.value.toString() == "fileRequest") { bodyMap[entry.key] = mapOf( Pair("fileName", fileName), Pair("file", file), Pair("contentType", contentType) ) } } postMultipart(type, bodyMap) } } /** *设置cookie urlOption的优先级大于书源保存的cookie *@param tag 书源url 缺省为传入的url */ private fun setCookie(tag: String?) { val cookie = CookieStore.getCookie(tag ?: url) if (cookie.isNotEmpty()) { val cookieMap = CookieStore.cookieToMap(cookie) val customCookieMap = CookieStore.cookieToMap(headerMap["Cookie"] ?: "") cookieMap.putAll(customCookieMap) val newCookie = CookieStore.mapToCookie(cookieMap) newCookie?.let { headerMap.put("Cookie", it) } } } fun getUserAgent(): String { return headerMap[UA_NAME] ?: AppConst.userAgent } fun isPost(): Boolean { return method == RequestMethod.POST } override fun getSource(): BaseSource? { return source } data class UrlOption( private var method: String? = null, private var charset: String? = null, private var headers: Any? = null, private var body: Any? = null, private var retry: Int? = null, private var type: String? = null, private var webView: Any? = null, private var webJs: String? = null, private var js: String? = null, ) { fun setMethod(value: String?) { method = if (value.isNullOrBlank()) null else value } fun getMethod(): String? { return method } fun setCharset(value: String?) { charset = if (value.isNullOrBlank()) null else value } fun getCharset(): String? { return charset } fun setRetry(value: String?) { retry = if (value.isNullOrEmpty()) null else value.toIntOrNull() } fun getRetry(): Int { return retry ?: 0 } fun setType(value: String?) { type = if (value.isNullOrBlank()) null else value } fun getType(): String? { return type } fun useWebView(): Boolean { return when (webView) { null, "", false, "false" -> false else -> true } } fun useWebView(boolean: Boolean) { webView = if (boolean) true else null } fun setHeaders(value: String?) { headers = if (value.isNullOrBlank()) { null } else { GSON.fromJsonObject>(value).getOrNull() } } fun getHeaderMap(): Map<*, *>? { return when (val value = headers) { is Map<*, *> -> value is String -> GSON.fromJsonObject>(value).getOrNull() else -> null } } fun setBody(value: String?) { body = when { value.isNullOrBlank() -> null value.isJsonObject() -> GSON.fromJsonObject>(value) value.isJsonArray() -> GSON.fromJsonArray>(value) else -> value } } fun getBody(): String? { return body?.let { if (it is String) it else GSON.toJson(it) } } fun setWebJs(value: String?) { webJs = if (value.isNullOrBlank()) null else value } fun getWebJs(): String? { return webJs } fun setJs(value: String?) { js = if (value.isNullOrBlank()) null else value } fun getJs(): String? { return js } } data class ConcurrentRecord( val concurrent: Boolean, var time: Long, var frequency: Int ) } ================================================ FILE: src/main/java/io/legado/app/model/analyzeRule/QueryTTF.java ================================================ package io.legado.app.model.analyzeRule; import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Triple; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @SuppressWarnings({"FieldCanBeLocal", "StatementWithEmptyBody", "unused"}) public class QueryTTF { private static class Header { public int majorVersion; public int minorVersion; public int numOfTables; public int searchRange; public int entrySelector; public int rangeShift; } private static class Directory { public String tag; // table name public int checkSum; // Check sum public int offset; // Offset from beginning of file public int length; // length of the table in bytes } private static class NameLayout { public int format; public int count; public int stringOffset; public List records = new LinkedList<>(); } private static class NameRecord { public int platformID; // 平台标识符<0:Unicode, 1:Mac, 2:ISO, 3:Windows, 4:Custom> public int encodingID; // 编码标识符 public int languageID; // 语言标识符 public int nameID; // 名称标识符 public int length; // 名称字符串的长度 public int offset; // 名称字符串相对于stringOffset的字节偏移量 } private static class HeadLayout { public int majorVersion; public int minorVersion; public int fontRevision; public int checkSumAdjustment; public int magicNumber; public int flags; public int unitsPerEm; public long created; public long modified; public short xMin; public short yMin; public short xMax; public short yMax; public int macStyle; public int lowestRecPPEM; public short fontDirectionHint; public short indexToLocFormat; // <0:loca是2字节数组, 1:loca是4字节数组> public short glyphDataFormat; } private static class MaxpLayout { public int majorVersion; public int minorVersion; public int numGlyphs; // 字体中的字形数量 public int maxPoints; public int maxContours; public int maxCompositePoints; public int maxCompositeContours; public int maxZones; public int maxTwilightPoints; public int maxStorage; public int maxFunctionDefs; public int maxInstructionDefs; public int maxStackElements; public int maxSizeOfInstructions; public int maxComponentElements; public int maxComponentDepth; } private static class CmapLayout { public int version; public int numTables; public List records = new LinkedList<>(); public Map tables = new HashMap<>(); } private static class CmapRecord { public int platformID; public int encodingID; public int offset; } private static class CmapFormat { public int format; public int length; public int language; public byte[] glyphIdArray; } private static class CmapFormat4 extends CmapFormat { public int segCountX2; public int searchRange; public int entrySelector; public int rangeShift; public int[] endCode; public int reservedPad; public int[] startCode; public short[] idDelta; public int[] idRangeOffset; public int[] glyphIdArray; } private static class CmapFormat6 extends CmapFormat { public int firstCode; public int entryCount; public int[] glyphIdArray; } private static class CmapFormat12 extends CmapFormat { public int reserved; public int length; public int language; public int numGroups; public List> groups; } private static class GlyfLayout { public short numberOfContours; // 非负值为简单字型,负值为符合字型 public short xMin; public short yMin; public short xMax; public short yMax; public int[] endPtsOfContours; // length=numberOfContours public int instructionLength; public byte[] instructions; // length=instructionLength public byte[] flags; public short[] xCoordinates; // length = flags.length public short[] yCoordinates; // length = flags.length } private static class ByteArrayReader { public int index; public byte[] buffer; public ByteArrayReader(byte[] buffer, int index) { this.buffer = buffer; this.index = index; } public long ReadUIntX(long len) { long result = 0; for (long i = 0; i < len; ++i) { result <<= 8; result |= buffer[index++] & 0xFF; } return result; } public long ReadUInt64() { return ReadUIntX(8); } public int ReadUInt32() { return (int) ReadUIntX(4); } public int ReadUInt16() { return (int) ReadUIntX(2); } public short ReadInt16() { return (short) ReadUIntX(2); } public short ReadUInt8() { return (short) ReadUIntX(1); } public String ReadStrings(int len, Charset charset) { byte[] result = len > 0 ? new byte[len] : null; for (int i = 0; i < len; ++i) result[i] = buffer[index++]; return new String(result, charset); } public byte GetByte() { return buffer[index++]; } public byte[] GetBytes(int len) { byte[] result = len > 0 ? new byte[len] : null; for (int i = 0; i < len; ++i) result[i] = buffer[index++]; return result; } public int[] GetUInt16Array(int len) { int[] result = len > 0 ? new int[len] : null; for (int i = 0; i < len; ++i) result[i] = ReadUInt16(); return result; } public short[] GetInt16Array(int len) { short[] result = len > 0 ? new short[len] : null; for (int i = 0; i < len; ++i) result[i] = ReadInt16(); return result; } } private final ByteArrayReader fontReader; private final Header fileHeader = new Header(); private final List directorys = new LinkedList<>(); private final NameLayout name = new NameLayout(); private final HeadLayout head = new HeadLayout(); private final MaxpLayout maxp = new MaxpLayout(); private final List loca = new LinkedList<>(); private final CmapLayout Cmap = new CmapLayout(); private final List glyf = new LinkedList<>(); @SuppressWarnings("unchecked") private final Pair[] pps = new Pair[]{ Pair.of(3, 10), Pair.of(0, 4), Pair.of(3, 1), Pair.of(1, 0), Pair.of(0, 3), Pair.of(0, 1) }; public final Map codeToGlyph = new HashMap<>(); public final Map glyphToCode = new HashMap<>(); private int limitMix = 0; private int limitMax = 0; /** * 构造函数 * * @param buffer 传入TTF字体二进制数组 */ public QueryTTF(byte[] buffer) { fontReader = new ByteArrayReader(buffer, 0); // 获取文件头 fileHeader.majorVersion = fontReader.ReadUInt16(); fileHeader.minorVersion = fontReader.ReadUInt16(); fileHeader.numOfTables = fontReader.ReadUInt16(); fileHeader.searchRange = fontReader.ReadUInt16(); fileHeader.entrySelector = fontReader.ReadUInt16(); fileHeader.rangeShift = fontReader.ReadUInt16(); // 获取目录 for (int i = 0; i < fileHeader.numOfTables; ++i) { Directory d = new Directory(); d.tag = fontReader.ReadStrings(4, StandardCharsets.US_ASCII); d.checkSum = fontReader.ReadUInt32(); d.offset = fontReader.ReadUInt32(); d.length = fontReader.ReadUInt32(); directorys.add(d); } // 解析表 name (字体信息,包含版权、名称、作者等...) for (Directory Temp : directorys) { if (Temp.tag.equals("name")) { fontReader.index = Temp.offset; name.format = fontReader.ReadUInt16(); name.count = fontReader.ReadUInt16(); name.stringOffset = fontReader.ReadUInt16(); for (int i = 0; i < name.count; ++i) { NameRecord record = new NameRecord(); record.platformID = fontReader.ReadUInt16(); record.encodingID = fontReader.ReadUInt16(); record.languageID = fontReader.ReadUInt16(); record.nameID = fontReader.ReadUInt16(); record.length = fontReader.ReadUInt16(); record.offset = fontReader.ReadUInt16(); name.records.add(record); } } } // 解析表 head (获取 head.indexToLocFormat) for (Directory Temp : directorys) { if (Temp.tag.equals("head")) { fontReader.index = Temp.offset; head.majorVersion = fontReader.ReadUInt16(); head.minorVersion = fontReader.ReadUInt16(); head.fontRevision = fontReader.ReadUInt32(); head.checkSumAdjustment = fontReader.ReadUInt32(); head.magicNumber = fontReader.ReadUInt32(); head.flags = fontReader.ReadUInt16(); head.unitsPerEm = fontReader.ReadUInt16(); head.created = fontReader.ReadUInt64(); head.modified = fontReader.ReadUInt64(); head.xMin = fontReader.ReadInt16(); head.yMin = fontReader.ReadInt16(); head.xMax = fontReader.ReadInt16(); head.yMax = fontReader.ReadInt16(); head.macStyle = fontReader.ReadUInt16(); head.lowestRecPPEM = fontReader.ReadUInt16(); head.fontDirectionHint = fontReader.ReadInt16(); head.indexToLocFormat = fontReader.ReadInt16(); head.glyphDataFormat = fontReader.ReadInt16(); } } // 解析表 maxp (获取 maxp.numGlyphs) for (Directory Temp : directorys) { if (Temp.tag.equals("maxp")) { fontReader.index = Temp.offset; maxp.majorVersion = fontReader.ReadUInt16(); maxp.minorVersion = fontReader.ReadUInt16(); maxp.numGlyphs = fontReader.ReadUInt16(); maxp.maxPoints = fontReader.ReadUInt16(); maxp.maxContours = fontReader.ReadUInt16(); maxp.maxCompositePoints = fontReader.ReadUInt16(); maxp.maxCompositeContours = fontReader.ReadUInt16(); maxp.maxZones = fontReader.ReadUInt16(); maxp.maxTwilightPoints = fontReader.ReadUInt16(); maxp.maxStorage = fontReader.ReadUInt16(); maxp.maxFunctionDefs = fontReader.ReadUInt16(); maxp.maxInstructionDefs = fontReader.ReadUInt16(); maxp.maxStackElements = fontReader.ReadUInt16(); maxp.maxSizeOfInstructions = fontReader.ReadUInt16(); maxp.maxComponentElements = fontReader.ReadUInt16(); maxp.maxComponentDepth = fontReader.ReadUInt16(); } } // 解析表 loca (轮廓数据偏移地址表) for (Directory Temp : directorys) { if (Temp.tag.equals("loca")) { fontReader.index = Temp.offset; int offset = head.indexToLocFormat == 0 ? 2 : 4; for (long i = 0; i < Temp.length; i += offset) { loca.add(offset == 2 ? fontReader.ReadUInt16() << 1 : fontReader.ReadUInt32()); } } } // 解析表 cmap (Unicode编码轮廓索引对照表) for (Directory Temp : directorys) { if (Temp.tag.equals("cmap")) { fontReader.index = Temp.offset; Cmap.version = fontReader.ReadUInt16(); Cmap.numTables = fontReader.ReadUInt16(); for (int i = 0; i < Cmap.numTables; ++i) { CmapRecord record = new CmapRecord(); record.platformID = fontReader.ReadUInt16(); record.encodingID = fontReader.ReadUInt16(); record.offset = fontReader.ReadUInt32(); Cmap.records.add(record); } for (int i = 0; i < Cmap.numTables; ++i) { int fmtOffset = Cmap.records.get(i).offset; fontReader.index = Temp.offset + fmtOffset; int EndIndex = fontReader.index; int format = fontReader.ReadUInt16(); if (Cmap.tables.containsKey(fmtOffset)) continue; if (format == 0) { CmapFormat f = new CmapFormat(); f.format = format; f.length = fontReader.ReadUInt16(); f.language = fontReader.ReadUInt16(); f.glyphIdArray = fontReader.GetBytes(f.length - 6); Cmap.tables.put(fmtOffset, f); } else if (format == 4) { CmapFormat4 f = new CmapFormat4(); f.format = format; f.length = fontReader.ReadUInt16(); f.language = fontReader.ReadUInt16(); f.segCountX2 = fontReader.ReadUInt16(); int segCount = f.segCountX2 >> 1; f.searchRange = fontReader.ReadUInt16(); f.entrySelector = fontReader.ReadUInt16(); f.rangeShift = fontReader.ReadUInt16(); f.endCode = fontReader.GetUInt16Array(segCount); f.reservedPad = fontReader.ReadUInt16(); f.startCode = fontReader.GetUInt16Array(segCount); f.idDelta = fontReader.GetInt16Array(segCount); f.idRangeOffset = fontReader.GetUInt16Array(segCount); f.glyphIdArray = fontReader.GetUInt16Array((EndIndex + f.length - fontReader.index) >> 1); Cmap.tables.put(fmtOffset, f); } else if (format == 6) { CmapFormat6 f = new CmapFormat6(); f.format = format; f.length = fontReader.ReadUInt16(); f.language = fontReader.ReadUInt16(); f.firstCode = fontReader.ReadUInt16(); f.entryCount = fontReader.ReadUInt16(); f.glyphIdArray = fontReader.GetUInt16Array(f.entryCount); Cmap.tables.put(fmtOffset, f); } else if (format == 12) { CmapFormat12 f = new CmapFormat12(); f.format = format; f.reserved = fontReader.ReadUInt16(); f.length = fontReader.ReadUInt32(); f.language = fontReader.ReadUInt32(); f.numGroups = fontReader.ReadUInt32(); f.groups = new ArrayList<>(f.numGroups); for (int n = 0; n < f.numGroups; ++n) { f.groups.add(Triple.of(fontReader.ReadUInt32(), fontReader.ReadUInt32(), fontReader.ReadUInt32())); } Cmap.tables.put(fmtOffset, f); } } } } // 解析表 glyf (字体轮廓数据表) for (Directory Temp : directorys) { if (Temp.tag.equals("glyf")) { fontReader.index = Temp.offset; for (int i = 0; i < maxp.numGlyphs; ++i) { fontReader.index = Temp.offset + loca.get(i); short numberOfContours = fontReader.ReadInt16(); if (numberOfContours > 0) { GlyfLayout g = new GlyfLayout(); g.numberOfContours = numberOfContours; g.xMin = fontReader.ReadInt16(); g.yMin = fontReader.ReadInt16(); g.xMax = fontReader.ReadInt16(); g.yMax = fontReader.ReadInt16(); g.endPtsOfContours = fontReader.GetUInt16Array(numberOfContours); g.instructionLength = fontReader.ReadUInt16(); g.instructions = fontReader.GetBytes(g.instructionLength); int flagLength = g.endPtsOfContours[g.endPtsOfContours.length - 1] + 1; // 获取轮廓点描述标志 g.flags = new byte[flagLength]; for (int n = 0; n < flagLength; ++n) { g.flags[n] = fontReader.GetByte(); if ((g.flags[n] & 0x08) != 0x00) { for (int m = fontReader.ReadUInt8(); m > 0; --m) { g.flags[++n] = g.flags[n - 1]; } } } // 获取轮廓点描述x轴相对值 g.xCoordinates = new short[flagLength]; for (int n = 0; n < flagLength; ++n) { short same = (short) ((g.flags[n] & 0x10) != 0 ? 1 : -1); if ((g.flags[n] & 0x02) != 0) { g.xCoordinates[n] = (short) (same * fontReader.ReadUInt8()); } else { g.xCoordinates[n] = same == 1 ? (short) 0 : fontReader.ReadInt16(); } } // 获取轮廓点描述y轴相对值 g.yCoordinates = new short[flagLength]; for (int n = 0; n < flagLength; ++n) { short same = (short) ((g.flags[n] & 0x20) != 0 ? 1 : -1); if ((g.flags[n] & 0x04) != 0) { g.yCoordinates[n] = (short) (same * fontReader.ReadUInt8()); } else { g.yCoordinates[n] = same == 1 ? (short) 0 : fontReader.ReadInt16(); } } // 相对坐标转绝对坐标 // for (int n = 1; n < flagLength; ++n) { // xCoordinates[n] += xCoordinates[n - 1]; // yCoordinates[n] += yCoordinates[n - 1]; // } glyf.add(g); } else { // 复合字体暂未使用 } } } } // 建立Unicode&Glyph双向表 for (int key = 0; key < 130000; ++key) { if (key == 0xFF) key = 0x3400; int gid = getGlyfIndex(key); if (gid == 0) continue; StringBuilder sb = new StringBuilder(); // 字型数据转String,方便存HashMap for (short b : glyf.get(gid).xCoordinates) sb.append(b); for (short b : glyf.get(gid).yCoordinates) sb.append(b); String val = sb.toString(); if (limitMix == 0) limitMix = key; limitMax = key; codeToGlyph.put(key, val); if (glyphToCode.containsKey(val)) continue; glyphToCode.put(val, key); } } /** * 获取字体信息 (1=字体名称) * * @param nameId 传入十进制字体信息索引 * @return 返回查询结果字符串 */ public String getNameById(int nameId) { for (Directory Temp : directorys) { if (!Temp.tag.equals("name")) continue; fontReader.index = Temp.offset; break; } for (NameRecord record : name.records) { if (record.nameID != nameId) continue; fontReader.index += name.stringOffset + record.offset; return fontReader.ReadStrings(record.length, record.platformID == 1 ? StandardCharsets.UTF_8 : StandardCharsets.UTF_16BE); } return "error"; } /** * 使用Unicode值查找轮廓索引 * * @param code 传入Unicode十进制值 * @return 返回十进制轮廓索引 */ private int getGlyfIndex(int code) { if (code == 0) return 0; int fmtKey = 0; for (Pair item : pps) { for (CmapRecord record : Cmap.records) { if ((item.getLeft() == record.platformID) && (item.getRight() == record.encodingID)) { fmtKey = record.offset; break; } } if (fmtKey > 0) break; } if (fmtKey == 0) return 0; int glyfID = 0; CmapFormat table = Cmap.tables.get(fmtKey); assert table != null; int fmt = table.format; if (fmt == 0) { if (code < table.glyphIdArray.length) glyfID = table.glyphIdArray[code] & 0xFF; } else if (fmt == 4) { CmapFormat4 tab = (CmapFormat4) table; if (code > tab.endCode[tab.endCode.length - 1]) return 0; // 二分法查找数值索引 int start = 0, middle, end = tab.endCode.length - 1; while (start + 1 < end) { middle = (start + end) / 2; if (tab.endCode[middle] <= code) start = middle; else end = middle; } if (tab.endCode[start] < code) ++start; if (code < tab.startCode[start]) return 0; if (tab.idRangeOffset[start] != 0) { glyfID = tab.glyphIdArray[code - tab.startCode[start] + (tab.idRangeOffset[start] >> 1) - (tab.idRangeOffset.length - start)]; } else glyfID = code + tab.idDelta[start]; glyfID &= 0xFFFF; } else if (fmt == 6) { CmapFormat6 tab = (CmapFormat6) table; int index = code - tab.firstCode; if (index < 0 || index >= tab.glyphIdArray.length) glyfID = 0; else glyfID = tab.glyphIdArray[index]; } else if (fmt == 12) { CmapFormat12 tab = (CmapFormat12) table; if (code > tab.groups.get(tab.numGroups - 1).getMiddle()) return 0; // 二分法查找数值索引 int start = 0, middle, end = tab.numGroups - 1; while (start + 1 < end) { middle = (start + end) / 2; if (tab.groups.get(middle).getLeft() <= code) start = middle; else end = middle; } if (tab.groups.get(start).getLeft() <= code && code <= tab.groups.get(start).getMiddle()) { glyfID = tab.groups.get(start).getRight() + code - tab.groups.get(start).getLeft(); } } return glyfID; } /** * 判断Unicode值是否在字体范围内 * * @param code 传入Unicode十进制值 * @return 返回bool查询结果 */ public boolean inLimit(char code) { return (limitMix <= code) && (code < limitMax); } /** * 使用Unicode值获取轮廓数据 * * @param key 传入Unicode十进制值 * @return 返回轮廓数组的String值 */ public String getGlyfByCode(int key) { return codeToGlyph.getOrDefault(key, ""); } /** * 使用轮廓数据获取Unicode值 * * @param val 传入轮廓数组的String值 * @return 返回Unicode十进制值 */ public int getCodeByGlyf(String val) { //noinspection ConstantConditions return glyphToCode.getOrDefault(val, 0); } } ================================================ FILE: src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt ================================================ package io.legado.app.model.analyzeRule //通用的规则切分处理 class RuleAnalyzer(data: String, code: Boolean = false) { private var queue: String = data //被处理字符串 private var pos = 0 //当前处理到的位置 private var start = 0 //当前处理字段的开始 private var startX = 0 //当前规则的开始 private var rule = ArrayList() //分割出的规则列表 private var step: Int = 0 //分割字符的长度 var elementsType = "" //当前分割字符串 var innerType = true //是否为内嵌{{}} fun trim() { // 修剪当前规则之前的"@"或者空白符 if (queue[pos] == '@' || queue[pos] < '!') { //在while里重复设置start和startX会拖慢执行速度,所以先来个判断是否存在需要修剪的字段,最后再一次性设置start和startX pos++ while (queue[pos] == '@' || queue[pos] < '!') pos++ start = pos //开始点推移 startX = pos //规则起始点推移 } } //将pos重置为0,方便复用 fun reSetPos() { pos = 0 startX = 0 } /** * 从剩余字串中拉出一个字符串,直到但不包括匹配序列 * @param seq 查找的字符串 **区分大小写** * @return 是否找到相应字段。 */ fun consumeTo(seq: String): Boolean { start = pos //将处理到的位置设置为规则起点 val offset = queue.indexOf(seq, pos) return if (offset != -1) { pos = offset true } else false } /** * 从剩余字串中拉出一个字符串,直到但不包括匹配序列(匹配参数列表中一项即为匹配),或剩余字串用完。 * @param seq 匹配字符串序列 * @return 成功返回true并设置间隔,失败则直接返回fasle */ fun consumeToAny(vararg seq: String): Boolean { var pos = pos //声明新变量记录匹配位置,不更改类本身的位置 while (pos != queue.length) { for (s in seq) { if (queue.regionMatches(pos, s, 0, s.length)) { step = s.length //间隔数 this.pos = pos //匹配成功, 同步处理位置到类 return true //匹配就返回 true } } pos++ //逐个试探 } return false } /** * 从剩余字串中拉出一个字符串,直到但不包括匹配序列(匹配参数列表中一项即为匹配),或剩余字串用完。 * @param seq 匹配字符序列 * @return 返回匹配位置 */ private fun findToAny(vararg seq: Char): Int { var pos = pos //声明新变量记录匹配位置,不更改类本身的位置 while (pos != queue.length) { for (s in seq) if (queue[pos] == s) return pos //匹配则返回位置 pos++ //逐个试探 } return -1 } /** * 拉出一个非内嵌代码平衡组,存在转义文本 */ fun chompCodeBalanced(open: Char, close: Char): Boolean { var pos = pos //声明临时变量记录匹配位置,匹配成功后才同步到类的pos var depth = 0 //嵌套深度 var otherDepth = 0 //其他对称符合嵌套深度 var inSingleQuote = false //单引号 var inDoubleQuote = false //双引号 do { if (pos == queue.length) break val c = queue[pos++] if (c != ESC) { //非转义字符 if (c == '\'' && !inDoubleQuote) inSingleQuote = !inSingleQuote //匹配具有语法功能的单引号 else if (c == '"' && !inSingleQuote) inDoubleQuote = !inDoubleQuote //匹配具有语法功能的双引号 if (inSingleQuote || inDoubleQuote) continue //语法单元未匹配结束,直接进入下个循环 if (c == '[') depth++ //开始嵌套一层 else if (c == ']') depth-- //闭合一层嵌套 else if (depth == 0) { //处于默认嵌套中的非默认字符不需要平衡,仅depth为0时默认嵌套全部闭合,此字符才进行嵌套 if (c == open) otherDepth++ else if (c == close) otherDepth-- } } else pos++ } while (depth > 0 || otherDepth > 0) //拉出一个平衡字串 return if (depth > 0 || otherDepth > 0) false else { this.pos = pos //同步位置 true } } /** * 拉出一个规则平衡组,经过仔细测试xpath和jsoup中,引号内转义字符无效。 */ fun chompRuleBalanced(open: Char, close: Char): Boolean { var pos = pos //声明临时变量记录匹配位置,匹配成功后才同步到类的pos var depth = 0 //嵌套深度 var inSingleQuote = false //单引号 var inDoubleQuote = false //双引号 do { if (pos == queue.length) break val c = queue[pos++] if (c == '\'' && !inDoubleQuote) inSingleQuote = !inSingleQuote //匹配具有语法功能的单引号 else if (c == '"' && !inSingleQuote) inDoubleQuote = !inDoubleQuote //匹配具有语法功能的双引号 if (inSingleQuote || inDoubleQuote) continue //语法单元未匹配结束,直接进入下个循环 else if (c == '\\') { //不在引号中的转义字符才将下个字符转义 pos++ continue } if (c == open) depth++ //开始嵌套一层 else if (c == close) depth-- //闭合一层嵌套 } while (depth > 0) //拉出一个平衡字串 return if (depth > 0) false else { this.pos = pos //同步位置 true } } /** * 不用正则,不到最后不切片也不用中间变量存储,只在序列中标记当前查找字段的开头结尾,到返回时才切片,高效快速准确切割规则 * 解决jsonPath自带的"&&"和"||"与阅读的规则冲突,以及规则正则或字符串中包含"&&"、"||"、"%%"、"@"导致的冲突 */ tailrec fun splitRule(vararg split: String): ArrayList { //首段匹配,elementsType为空 if (split.size == 1) { elementsType = split[0] //设置分割字串 return if (!consumeTo(elementsType)) { rule += queue.substring(startX) rule } else { step = elementsType.length //设置分隔符长度 splitRule() } //递归匹配 } else if (!consumeToAny(* split)) { //未找到分隔符 rule += queue.substring(startX) return rule } val end = pos //记录分隔位置 pos = start //重回开始,启动另一种查找 do { val st = findToAny('[', '(') //查找筛选器位置 if (st == -1) { rule = arrayListOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 elementsType = queue.substring(end, end + step) //设置组合类型 pos = end + step //跳过分隔符 while (consumeTo(elementsType)) { //循环切分规则压入数组 rule += queue.substring(start, pos) pos += step //跳过分隔符 } rule += queue.substring(pos) //将剩余字段压入数组末尾 return rule } if (st > end) { //先匹配到st1pos,表明分隔字串不在选择器中,将选择器前分隔字串分隔的字段依次压入数组 rule = arrayListOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 elementsType = queue.substring(end, end + step) //设置组合类型 pos = end + step //跳过分隔符 while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组 rule += queue.substring(start, pos) pos += step //跳过分隔符 } return if (pos > st) { startX = start splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配 } else { //执行到此,证明后面再无分隔字符 rule += queue.substring(pos) //将剩余字段压入数组末尾 rule } } pos = st //位置推移到筛选器处 val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符 if (!chompBalanced(queue[pos], next)) throw Error( queue.substring(0, start) + "后未平衡" ) //拉出一个筛选器,不平衡则报错 } while (end > pos) start = pos //设置开始查找筛选器位置的起始位置 return splitRule(* split) //递归调用首段匹配 } @JvmName("splitRuleNext") private tailrec fun splitRule(): ArrayList { //二段匹配被调用,elementsType非空(已在首段赋值),直接按elementsType查找,比首段采用的方式更快 val end = pos //记录分隔位置 pos = start //重回开始,启动另一种查找 do { val st = findToAny('[', '(') //查找筛选器位置 if (st == -1) { rule += arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 pos = end + step //跳过分隔符 while (consumeTo(elementsType)) { //循环切分规则压入数组 rule += queue.substring(start, pos) pos += step //跳过分隔符 } rule += queue.substring(pos) //将剩余字段压入数组末尾 return rule } if (st > end) { //先匹配到st1pos,表明分隔字串不在选择器中,将选择器前分隔字串分隔的字段依次压入数组 rule += arrayListOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 pos = end + step //跳过分隔符 while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组 rule += queue.substring(start, pos) pos += step //跳过分隔符 } return if (pos > st) { startX = start splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配 } else { //执行到此,证明后面再无分隔字符 rule += queue.substring(pos) //将剩余字段压入数组末尾 rule } } pos = st //位置推移到筛选器处 val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符 if (!chompBalanced(queue[pos], next)) throw Error( queue.substring(0, start) + "后未平衡" ) //拉出一个筛选器,不平衡则报错 } while (end > pos) start = pos //设置开始查找筛选器位置的起始位置 return if (!consumeTo(elementsType)) { rule += queue.substring(startX) rule } else splitRule() //递归匹配 } /** * 替换内嵌规则 * @param inner 起始标志,如{$. * @param startStep 不属于规则部分的前置字符长度,如{$.中{不属于规则的组成部分,故startStep为1 * @param endStep 不属于规则部分的后置字符长度 * @param fr 查找到内嵌规则时,用于解析的函数 * * */ fun innerRule( inner: String, startStep: Int = 1, endStep: Int = 1, fr: (String) -> String? ): String { val st = StringBuilder() while (consumeTo(inner)) { //拉取成功返回true,ruleAnalyzes里的字符序列索引变量pos后移相应位置,否则返回false,且isEmpty为true val posPre = pos //记录consumeTo匹配位置 if (chompCodeBalanced('{', '}')) { val frv = fr(queue.substring(posPre + startStep, pos - endStep)) if (!frv.isNullOrEmpty()) { st.append(queue.substring(startX, posPre) + frv) //压入内嵌规则前的内容,及内嵌规则解析得到的字符串 startX = pos //记录下次规则起点 continue //获取内容成功,继续选择下个内嵌规则 } } pos += inner.length //拉出字段不平衡,inner只是个普通字串,跳到此inner后继续匹配 } return if (startX == 0) "" else st.apply { append(queue.substring(startX)) }.toString() } /** * 替换内嵌规则 * @param fr 查找到内嵌规则时,用于解析的函数 * * */ fun innerRule( startStr: String, endStr: String, fr: (String) -> String? ): String { val st = StringBuilder() while (consumeTo(startStr)) { //拉取成功返回true,ruleAnalyzes里的字符序列索引变量pos后移相应位置,否则返回false,且isEmpty为true pos += startStr.length //跳过开始字符串 val posPre = pos //记录consumeTo匹配位置 if (consumeTo(endStr)) { val frv = fr(queue.substring(posPre, pos)) st.append( queue.substring( startX, posPre - startStr.length ) + frv ) //压入内嵌规则前的内容,及内嵌规则解析得到的字符串 pos += endStr.length //跳过结束字符串 startX = pos //记录下次规则起点 } } return if (startX == 0) queue else st.apply { append(queue.substring(startX)) }.toString() } val ruleTypeList = ArrayList() //设置平衡组函数,json或JavaScript时设置成chompCodeBalanced,否则为chompRuleBalanced val chompBalanced = if (code) ::chompCodeBalanced else ::chompRuleBalanced companion object { /** * 转义字符 */ private const val ESC = '\\' } } ================================================ FILE: src/main/java/io/legado/app/model/analyzeRule/RuleData.kt ================================================ package io.legado.app.model.analyzeRule import io.legado.app.utils.GSON class RuleData : RuleDataInterface { override val variableMap by lazy { hashMapOf() } override fun putVariable(key: String, value: String?) { if (value == null) { variableMap.remove(key) } else { variableMap[key] = value } } fun getVariable(): String? { if (variableMap.isEmpty()) { return null } return GSON.toJson(variableMap) } } ================================================ FILE: src/main/java/io/legado/app/model/analyzeRule/RuleDataInterface.kt ================================================ package io.legado.app.model.analyzeRule interface RuleDataInterface { val variableMap: HashMap fun putVariable(key: String, value: String?) fun getVariable(key: String): String? { return variableMap[key] } } ================================================ FILE: src/main/java/io/legado/app/model/localBook/CbzFile.kt ================================================ package io.legado.app.model.localBook import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.utils.* import java.io.File import java.io.InputStream import java.util.* import java.nio.file.Paths import java.util.zip.ZipFile import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream import com.htmake.reader.utils.getFileExtetion import com.htmake.reader.utils.xml2map class CbzFile(var book: Book) { var info: MutableMap? = null var cover: InputStream? = null companion object { private var cFile: CbzFile? = null @Synchronized private fun getCbzFile(book: Book): CbzFile { if (cFile == null || cFile?.book?.bookUrl != book.bookUrl) { cFile = CbzFile(book) return cFile!! } cFile?.book = book return cFile!! } @Synchronized fun getChapterList(book: Book): ArrayList { return getCbzFile(book).getChapterList() } @Synchronized fun getContent(book: Book, chapter: BookChapter): String? { return getCbzFile(book).getContent(chapter) } @Synchronized fun upBookInfo(book: Book, onlyCover: Boolean = false) { if (onlyCover) { return getCbzFile(book).updateCover() } return getCbzFile(book).upBookInfo() } } init { } private fun parseBookInfo(): Pair?, InputStream?> { if (cover != null || info != null) { return Pair(info, cover) } val zf = ZipFile(book.getLocalFile()) val entries = zf.entries() val imageExt = listOf("jpg", "jpeg", "gif", "png", "bmp", "webp", "svg") while (entries.hasMoreElements()) { val zipEntry: ZipEntry = entries.nextElement() as ZipEntry if (!zipEntry.isDirectory) { val name = zipEntry.name if (name.equals("ComicInfo.xml")) { // 解析书籍信息 var inputStream = zf.getInputStream(zipEntry) info = xml2map(inputStream) } else if (cover == null) { // 解析第一张图片 val ext = getFileExtetion(name).lowercase() if (imageExt.contains(ext)) { cover = zf.getInputStream(zipEntry) } } } if (cover != null && info != null) { break; } } return Pair(info, cover) } private fun upBookInfo() { val result = parseBookInfo() if (result.first != null) { val bookInfo = result.first as Map val info = bookInfo.get("ComicInfo") as Map? ?: null book.name = (info?.get("Title") ?: book.name) as String book.author = (info?.get("Writer") ?: book.author) as String } updateCover() } private fun updateCover() { val coverFile = "${MD5Utils.md5Encode16(book.bookUrl)}.jpg" val relativeCoverUrl = Paths.get("assets", book.getUserNameSpace(), "covers", coverFile).toString() book.coverUrl = "/" + relativeCoverUrl val coverUrl = Paths.get(book.workRoot(), "storage", relativeCoverUrl).toString() if (!File(coverUrl).exists()) { val result = parseBookInfo() if (result.second != null) { val coverStream = result.second as InputStream FileUtils.writeInputStream(coverUrl, coverStream) } } } private fun getContent(chapter: BookChapter): String? { return "" } private fun getChapterList(): ArrayList { val chapterList = ArrayList() val zf = ZipFile(book.getLocalFile()) val entries = zf.entries() var imageFileList = arrayListOf(); while (entries.hasMoreElements()) { val zipEntry: ZipEntry = entries.nextElement() as ZipEntry if (!zipEntry.isDirectory) { val name = zipEntry.name if (!name.endsWith(".xml")) { // 只获取图片文件 imageFileList.add(name) } } } // 排序 imageFileList.sort() for (i in 0 until imageFileList.size) { val name = imageFileList.get(i) val chapter = BookChapter() chapter.title = name chapter.index = i chapter.bookUrl = book.bookUrl chapter.url = name chapterList.add(chapter) } book.latestChapterTitle = chapterList.lastOrNull()?.title book.totalChapterNum = chapterList.size return chapterList } } ================================================ FILE: src/main/java/io/legado/app/model/localBook/EpubFile.kt ================================================ package io.legado.app.model.localBook import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.help.BookHelp import io.legado.app.utils.* import me.ag2s.epublib.domain.EpubBook import me.ag2s.epublib.domain.Resource import me.ag2s.epublib.epub.EpubReader import org.jsoup.Jsoup import org.jsoup.nodes.Element import org.jsoup.select.Elements import java.io.File import java.io.FileOutputStream import java.io.IOException import java.io.InputStream import java.nio.charset.Charset import java.nio.file.Paths import java.util.* import java.util.zip.ZipFile import mu.KotlinLogging private val logger = KotlinLogging.logger {} class EpubFile(var book: Book) { companion object { private var eFile: EpubFile? = null @Synchronized private fun getEFile(book: Book): EpubFile { if (eFile == null || eFile?.book?.bookUrl != book.bookUrl) { eFile = EpubFile(book) //对于Epub文件默认不启用替换 // book.setUseReplaceRule(false) return eFile!! } eFile?.book = book return eFile!! } @Synchronized fun getChapterList(book: Book): ArrayList { if (book.tocUrl.isEmpty()) { book.tocUrl = "spin+toc" } val epubFile = getEFile(book) return when (book.tocUrl) { "toc" -> { logger.info("epubFile.getChapterList") epubFile.getChapterList() } "spin" -> { logger.info("epubFile.getChapterListBySpine") epubFile.getChapterListBySpine() } "spin { logger.info("epubFile.getChapterListBySpinAndToc true") epubFile.getChapterListBySpinAndToc(true) } "spin+toc" -> { logger.info("epubFile.getChapterListBySpinAndToc") epubFile.getChapterListBySpinAndToc() } "toc+spin" -> { logger.info("epubFile.getChapterListByTocAndSpin") epubFile.getChapterListByTocAndSpin() } "toc { logger.info("epubFile.getChapterListByTocAndSpin true") epubFile.getChapterListByTocAndSpin(true) } else -> { logger.info("epubFile.getChapterListBySpinAndToc") epubFile.getChapterListBySpinAndToc() } } } @Synchronized fun getContent(book: Book, chapter: BookChapter): String? { return getEFile(book).getContent(chapter) } @Synchronized fun getImage( book: Book, href: String ): InputStream? { return getEFile(book).getImage(href) } @Synchronized fun upBookInfo(book: Book, onlyCover: Boolean = false) { if (onlyCover) { return getEFile(book).updateCover() } return getEFile(book).upBookInfo() } } private var mCharset: Charset = Charset.defaultCharset() private var epubBook: EpubBook? = null get() { if (field != null) { return field } field = readEpub() return field } init { try { epubBook?.let { // if (book.coverUrl.isNullOrEmpty()) { // book.coverUrl = FileUtils.getPath( // appCtx.externalFiles, // "covers", // "${MD5Utils.md5Encode16(book.bookUrl)}.jpg" // ) // } // if (!File(book.coverUrl!!).exists()) { // /*部分书籍DRM处理后,封面获取异常,待优化*/ // it.coverImage?.inputStream?.use { input -> // val cover = BitmapFactory.decodeStream(input) // val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!)) // cover.compress(Bitmap.CompressFormat.JPEG, 90, out) // out.flush() // out.close() // } // } } } catch (e: Exception) { e.printStackTrace() } } /*重写epub文件解析代码,直接读出压缩包文件生成Resources给epublib,这样的好处是可以逐一修改某些文件的格式错误*/ private fun readEpub(): EpubBook? { try { val file = book.getLocalFile() //通过懒加载读取epub return EpubReader().readEpubLazy(ZipFile(file), "utf-8") } catch (e: Exception) { e.printStackTrace() } return null } private fun getContent(chapter: BookChapter): String? { /** * * ...titlepage.xhtml */ if (chapter.url.contains("titlepage.xhtml")) { return "" } /*获取当前章节文本*/ epubBook?.let { epubBook -> val nextUrl = chapter.getVariable("nextUrl") val startFragmentId = chapter.startFragmentId val endFragmentId = chapter.endFragmentId val elements = Elements() var isChapter = false /*一些书籍依靠href索引的resource会包含多个章节,需要依靠fragmentId来截取到当前章节的内容*/ /*注:这里较大增加了内容加载的时间,所以首次获取内容后可存储到本地cache,减少重复加载*/ for (res in epubBook.contents) { if (chapter.url.substringBeforeLast("#") == res.href) { elements.add(getBody(res, startFragmentId, endFragmentId)) isChapter = true /** * fix https://github.com/gedoor/legado/issues/1927 加载全部内容的bug * content src text/000001.html(当前章节) - * content src text/000001.html#toc_id_x (下一章节) */ if (res.href == nextUrl?.substringBeforeLast("#")) break } else if (isChapter) { // fix 最后一章存在多个html时 内容缺失 if (res.href == nextUrl?.substringBeforeLast("#")) { break } elements.add(getBody(res, startFragmentId, endFragmentId)) } } var html = elements.outerHtml() val tag = Book.rubyTag if (book.getDelTag(tag)) { html = html.replace("\\s?([\\u4e00-\\u9fa5])\\s?.*?".toRegex(), "$1") } return HtmlFormatter.formatKeepImg(html) } return null } private fun getBody(res: Resource, startFragmentId: String?, endFragmentId: String?): Element { val body = Jsoup.parse(String(res.data, mCharset)).body() if (!startFragmentId.isNullOrBlank()) { body.getElementById(startFragmentId)?.previousElementSiblings()?.remove() } if (!endFragmentId.isNullOrBlank() && endFragmentId != startFragmentId) { body.getElementById(endFragmentId)?.run { nextElementSiblings().remove() remove() } } /*选择去除正文中的H标签,部分书籍标题与阅读标题重复待优化*/ val tag = Book.hTag if (book.getDelTag(tag)) { body.getElementsByTag("h1").remove() body.getElementsByTag("h2").remove() body.getElementsByTag("h3").remove() body.getElementsByTag("h4").remove() body.getElementsByTag("h5").remove() body.getElementsByTag("h6").remove() //body.getElementsMatchingOwnText(chapter.title)?.remove() } val children = body.children() children.select("script").remove() children.select("style").remove() return body } private fun getImage(href: String): InputStream? { val abHref = href.replace("../", "") return epubBook?.resources?.getByHref(abHref)?.inputStream } private fun upBookInfo() { if (epubBook == null) { eFile = null book.intro = "书籍导入异常" } else { val metadata = epubBook!!.metadata book.name = metadata.firstTitle if (book.name.isEmpty()) { book.name = book.originName.replace(".epub", "") } if (metadata.authors.size > 0) { val author = metadata.authors[0].toString().replace("^, |, $".toRegex(), "") book.author = author } if (metadata.descriptions.size > 0) { book.intro = Jsoup.parse(metadata.descriptions[0]).text() } updateCover() } } fun updateCover() { val coverFile = "${MD5Utils.md5Encode16(book.bookUrl)}.jpg" val relativeCoverUrl = Paths.get("assets", book.getUserNameSpace(), "covers", coverFile).toString() book.coverUrl = "/" + relativeCoverUrl val coverUrl = Paths.get(book.workRoot(), "storage", relativeCoverUrl).toString() if (!File(coverUrl).exists()) { FileUtils.writeBytes(coverUrl, epubBook!!.coverImage.data) } // 保存 cover // val cover = epubBook!!.coverImage?.href // if (cover != null) { // val epubRootDir = book.getEpubRootDir() // if (epubRootDir.isEmpty()) { // book.coverUrl = book.bookUrl.replace("storage/data/", "/epub/") + "/index/" + cover // } else { // book.coverUrl = book.bookUrl.replace("storage/data/", "/epub/") + "/index/" + epubRootDir + "/" + cover // } // } } fun getChapterListBySpine(): ArrayList { val chapterList = ArrayList() epubBook?.spine?.spineReferences?.forEachIndexed { index, spinResource -> val resource = spinResource.resource var title = resource.title if (title.isNullOrEmpty()) { try { val doc = Jsoup.parse(String(resource.data, mCharset)) val elements = doc.getElementsByTag("title") if (elements.size > 0) { title = elements[0].text() } } catch (e: IOException) { e.printStackTrace() } } val chapter = BookChapter() chapter.index = index chapter.bookUrl = book.bookUrl chapter.url = resource.href if (index == 0 && title.isEmpty()) { chapter.title = "封面" } else { chapter.title = title } chapterList.add(chapter) } book.latestChapterTitle = chapterList.lastOrNull()?.title book.totalChapterNum = chapterList.size return chapterList } fun getChapterList(): ArrayList { val chapterList = ArrayList() epubBook?.tableOfContents?.allUniqueResources?.forEachIndexed { index, resource -> var title = resource.title if (title.isNullOrEmpty()) { try { val doc = Jsoup.parse(String(resource.data, mCharset)) val elements = doc.getElementsByTag("title") if (elements.size > 0) { title = elements[0].text() } } catch (e: IOException) { e.printStackTrace() } } val chapter = BookChapter() chapter.index = index chapter.bookUrl = book.bookUrl chapter.url = resource.href if (index == 0 && title.isEmpty()) { chapter.title = "封面" } else { chapter.title = title } chapterList.add(chapter) } book.latestChapterTitle = chapterList.lastOrNull()?.title book.totalChapterNum = chapterList.size return chapterList } fun getChapterListBySpinAndToc(useTocTitle: Boolean = false): ArrayList { // 如果读取了 toc,那么 spin 就会使用 toc 的章节名 val tocChapterList = getChapterList() val spinChapterList = getChapterListBySpine() if (spinChapterList.size == 0) { return tocChapterList; } if (tocChapterList.size == 0) { return spinChapterList; } val titleMap: MutableMap = mutableMapOf(); for (i in 0 until tocChapterList.size) { titleMap.put(tocChapterList.get(i).url, tocChapterList.get(i)) } for (i in 0 until spinChapterList.size) { val chapter = spinChapterList.get(i) val tocChapter = titleMap.get(chapter.url) if (tocChapter != null && tocChapter.title.isNotEmpty()) { if (useTocTitle || chapter.title.isEmpty()) { chapter.title = tocChapter.title } } } book.latestChapterTitle = spinChapterList.lastOrNull()?.title book.totalChapterNum = spinChapterList.size return spinChapterList } fun getChapterListByTocAndSpin(useSpinTitle: Boolean = false): ArrayList { // 如果读取了 toc,那么 spin 就会使用 toc 的章节名 val tocChapterList = getChapterList() val spinChapterList = getChapterListBySpine() if (tocChapterList.size == 0) { return spinChapterList; } if (spinChapterList.size == 0) { return tocChapterList; } val titleMap: MutableMap = mutableMapOf(); for (i in 0 until spinChapterList.size) { titleMap.put(spinChapterList.get(i).url, spinChapterList.get(i)) } for (i in 0 until tocChapterList.size) { val chapter = tocChapterList.get(i) val tocChapter = titleMap.get(chapter.url) if (tocChapter != null && tocChapter.title.isNotEmpty()) { if (useSpinTitle || chapter.title.isEmpty()) { chapter.title = tocChapter.title } } } book.latestChapterTitle = tocChapterList.lastOrNull()?.title book.totalChapterNum = tocChapterList.size return tocChapterList } } ================================================ FILE: src/main/java/io/legado/app/model/localBook/LocalBook.kt ================================================ package io.legado.app.model.localBook import io.legado.app.constant.AppConst import io.legado.app.constant.AppPattern import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.help.BookHelp import io.legado.app.utils.* import io.legado.app.exception.TocEmptyException import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException import java.io.InputStream import java.util.regex.Matcher import java.util.regex.Pattern import javax.script.SimpleBindings object LocalBook { private val nameAuthorPatterns = arrayOf( Pattern.compile("(.*?)《([^《》]+)》.*?作者:(.*)"), Pattern.compile("(.*?)《([^《》]+)》(.*)"), Pattern.compile("(^)(.+) 作者:(.+)$"), Pattern.compile("(^)(.+) by (.+)$") ) @Throws(FileNotFoundException::class, SecurityException::class) fun getBookInputStream(book: Book): InputStream { val file = book.getLocalFile() if (file.exists()) { return FileInputStream(file) } throw FileNotFoundException(book.name + " 文件不存在") } @Throws(Exception::class) fun getChapterList(book: Book): ArrayList { val chapters = when { book.isEpub() -> { EpubFile.getChapterList(book) } book.isUmd() -> { UmdFile.getChapterList(book) } book.isCbz() -> { CbzFile.getChapterList(book) } else -> { TextFile.getChapterList(book) } } if (chapters.isEmpty()) { throw TocEmptyException("Chapterlist is empty " + book.getLocalFile()) } return chapters } fun getContent(book: Book, chapter: BookChapter): String? { return when { book.isEpub() -> { EpubFile.getContent(book, chapter) } book.isUmd() -> { UmdFile.getContent(book, chapter) } book.isCbz() -> { CbzFile.getContent(book, chapter) } else -> { TextFile.getContent(book, chapter) } } } fun analyzeNameAuthor(fileName: String): Pair { val tempFileName = fileName.substringBeforeLast(".") var name: String var author: String for (pattern in nameAuthorPatterns) { pattern.matcher(tempFileName).takeIf { it.find() }?.run { name = group(2)!! val group1 = group(1) ?: "" val group3 = group(3) ?: "" author = BookHelp.formatBookAuthor(group1 + group3) return Pair(name, author) } } name = BookHelp.formatBookName(tempFileName) author = BookHelp.formatBookAuthor(tempFileName.replace(name, "")) .takeIf { it.length != tempFileName.length } ?: "" return Pair(name, author) } fun deleteBook(book: Book) { kotlin.runCatching { var bookFile = book.getLocalFile(); if (book.isLocalTxt() || book.isUmd()) { if (bookFile.exists()) { bookFile.delete() } } if (book.isEpub()) { bookFile = bookFile.parentFile if (bookFile != null && bookFile.exists()) { FileUtils.delete(bookFile, true) } } } } } ================================================ FILE: src/main/java/io/legado/app/model/localBook/TextFile.kt ================================================ package io.legado.app.model.localBook import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.TxtTocRule import io.legado.app.help.DefaultData import io.legado.app.utils.EncodingDetect import io.legado.app.utils.MD5Utils import io.legado.app.utils.StringUtils import io.legado.app.utils.Utf8BomUtils import java.io.FileNotFoundException import java.nio.charset.Charset import java.util.regex.Matcher import java.util.regex.Pattern import kotlin.math.min import mu.KotlinLogging private val logger = KotlinLogging.logger {} class TextFile(private val book: Book) { companion object { @Throws(FileNotFoundException::class) fun getChapterList(book: Book): ArrayList { return TextFile(book).getChapterList() } @Throws(FileNotFoundException::class) fun getContent(book: Book, bookChapter: BookChapter): String { val count = (bookChapter.end!! - bookChapter.start!!).toInt() val buffer = ByteArray(count) LocalBook.getBookInputStream(book).use { bis -> bis.skip(bookChapter.start!!) bis.read(buffer) } if (book.charset == null) { book.charset = EncodingDetect.getEncode(book.getLocalFile()) } return String(buffer, book.fileCharset()) .substringAfter(bookChapter.title) .replace("^[\\n\\s]+".toRegex(), "  ") } } private val blank: Byte = 0x0a //默认从文件中获取数据的长度 private val bufferSize = 512000 //没有标题的时候,每个章节的最大长度 private val maxLengthWithNoToc = 10 * 1024 //使用正则划分目录,每个章节的最大允许长度 private val maxLengthWithToc = 102400 private var charset: Charset = book.fileCharset() /** * 获取目录 */ @Throws(FileNotFoundException::class) fun getChapterList(): ArrayList { if (book.charset == null || book.tocUrl.isBlank()) { LocalBook.getBookInputStream(book).use { bis -> val buffer = ByteArray(bufferSize) val length = bis.read(buffer) if (book.charset.isNullOrBlank()) { book.charset = EncodingDetect.getEncode(buffer.copyOf(length)) } charset = book.fileCharset() if (book.tocUrl.isBlank()) { val blockContent = String(buffer, 0, length, charset) book.tocUrl = getTocRule(blockContent)?.pattern() ?: "" } } } val toc = analyze(book.tocUrl.toPattern(Pattern.MULTILINE)) toc.forEachIndexed { index, bookChapter -> bookChapter.index = index bookChapter.bookUrl = book.bookUrl bookChapter.url = MD5Utils.md5Encode16(book.originName + index + bookChapter.title) } book.latestChapterTitle = toc.last().title book.totalChapterNum = toc.size return toc } /** * 按规则解析目录 */ private fun analyze(pattern: Pattern?): ArrayList { if (pattern?.pattern().isNullOrEmpty()) { return analyze() } pattern ?: return analyze() val toc = arrayListOf() LocalBook.getBookInputStream(book).use { bis -> var blockContent: String //加载章节 var curOffset: Long = 0 //读取的长度 var length: Int val buffer = ByteArray(bufferSize) var bufferStart = 3 bis.read(buffer, 0, 3) if (Utf8BomUtils.hasBom(buffer)) { bufferStart = 0 curOffset = 3 } //获取文件中的数据到buffer,直到没有数据为止 while ( bis.read( buffer, bufferStart, bufferSize - bufferStart ).also { length = it } > 0 ) { var end = bufferStart + length if (end == bufferSize) { for (i in bufferStart + length - 1 downTo 0) { if (buffer[i] == blank) { end = i break } } } //将数据转换成String, 不能超过length blockContent = String(buffer, 0, end, charset) buffer.copyInto(buffer, 0, end, bufferStart + length) bufferStart = bufferStart + length - end length = end //当前Block下使过的String的指针 var seekPos = 0 //进行正则匹配 val matcher: Matcher = pattern.matcher(blockContent) //如果存在相应章节 while (matcher.find()) { //获取匹配到的字符在字符串中的起始位置 val chapterStart = matcher.start() //获取章节内容 val chapterContent = blockContent.substring(seekPos, chapterStart) val chapterLength = chapterContent.toByteArray(charset).size val lastStart = toc.lastOrNull()?.start ?: curOffset if (book.getSplitLongChapter() && curOffset + chapterLength - lastStart > maxLengthWithToc ) { toc.lastOrNull()?.let { it.end = it.start } //章节字数太多进行拆分 val lastTitle = toc.lastOrNull()?.title val lastTitleLength = lastTitle?.toByteArray(charset)?.size ?: 0 val chapters = analyze( lastStart + lastTitleLength, curOffset + chapterLength ) lastTitle?.let { chapters.forEachIndexed { index, bookChapter -> bookChapter.title = "$lastTitle(${index + 1})" } } toc.addAll(chapters) //创建当前章节 val curChapter = BookChapter() curChapter.title = matcher.group() curChapter.start = curOffset + chapterLength toc.add(curChapter) } else if (seekPos == 0 && chapterStart != 0) { /* * 如果 seekPos == 0 && chapterStart != 0 表示当前block处前面有一段内容 * 第一种情况一定是序章 第二种情况是上一个章节的内容 */ if (toc.isEmpty()) { //如果当前没有章节,那么就是序章 //加入简介 if (StringUtils.trim(chapterContent).isNotEmpty()) { val qyChapter = BookChapter() qyChapter.title = "前言" qyChapter.start = curOffset qyChapter.end = chapterLength.toLong() toc.add(qyChapter) } //创建当前章节 val curChapter = BookChapter() curChapter.title = matcher.group() curChapter.start = chapterLength.toLong() toc.add(curChapter) } else { //否则就block分割之后,上一个章节的剩余内容 //获取上一章节 val lastChapter = toc.last() lastChapter.isVolume = chapterContent.substringAfter(lastChapter.title).isBlank() //将当前段落添加上一章去 lastChapter.end = lastChapter.end!! + chapterLength.toLong() //创建当前章节 val curChapter = BookChapter() curChapter.title = matcher.group() curChapter.start = lastChapter.end toc.add(curChapter) } } else { if (toc.isNotEmpty()) { //获取章节内容 //获取上一章节 val lastChapter = toc.last() lastChapter.isVolume = chapterContent.substringAfter(lastChapter.title).isBlank() lastChapter.end = lastChapter.start!! + chapterContent.toByteArray(charset).size.toLong() //创建当前章节 val curChapter = BookChapter() curChapter.title = matcher.group() curChapter.start = lastChapter.end toc.add(curChapter) } else { //如果章节不存在则创建章节 val curChapter = BookChapter() curChapter.title = matcher.group() curChapter.start = curOffset curChapter.end = curOffset toc.add(curChapter) } } //设置指针偏移 seekPos += chapterContent.length } //block的偏移点 curOffset += length.toLong() //设置上一章的结尾 toc.lastOrNull()?.end = curOffset } } System.gc() System.runFinalization() return toc } /** * 无规则拆分目录 */ private fun analyze( fileStart: Long = 0L, fileEnd: Long = Long.MAX_VALUE ): ArrayList { val toc = arrayListOf() LocalBook.getBookInputStream(book).use { bis -> //block的个数 var blockPos = 0 //加载章节 var curOffset: Long = 0 var chapterPos = 0 //读取的长度 var length = 0 val buffer = ByteArray(bufferSize) var bufferStart = 3 if (fileStart == 0L) { bis.read(buffer, 0, 3) if (Utf8BomUtils.hasBom(buffer)) { bufferStart = 0 curOffset = 3 } } else { bis.skip(fileStart) curOffset = fileStart bufferStart = 0 } //获取文件中的数据到buffer,直到没有数据为止 while ( fileEnd - curOffset - bufferStart > 0 && bis.read( buffer, bufferStart, min( (bufferSize - bufferStart).toLong(), fileEnd - curOffset - bufferStart ).toInt() ).also { length = it } > 0 ) { blockPos++ //章节在buffer的偏移量 var chapterOffset = 0 //当前剩余可分配的长度 length += bufferStart var strLength = length //分章的位置 chapterPos = 0 while (strLength > 0) { chapterPos++ //是否长度超过一章 if (strLength > maxLengthWithNoToc) { //在buffer中一章的终止点 var end = length //寻找换行符作为终止点 for (i in chapterOffset + maxLengthWithNoToc until length) { if (buffer[i] == blank) { end = i break } } val chapter = BookChapter() chapter.title = "第${blockPos}章($chapterPos)" chapter.start = toc.lastOrNull()?.end ?: curOffset chapter.end = chapter.start!! + end - chapterOffset toc.add(chapter) //减去已经被分配的长度 strLength -= (end - chapterOffset) //设置偏移的位置 chapterOffset = end } else { buffer.copyInto(buffer, 0, length - strLength, length) length -= strLength bufferStart = strLength strLength = 0 } } //block的偏移点 curOffset += length.toLong() } //设置结尾章节 if (bufferStart > 100 || toc.isEmpty()) { val chapter = BookChapter() chapter.title = "第${blockPos}章(${chapterPos})" chapter.start = toc.lastOrNull()?.end ?: curOffset chapter.end = chapter.start!! + bufferStart toc.add(chapter) } else { toc.lastOrNull()?.let { it.end = it.end!! + bufferStart } } } return toc } /** * 获取所有匹配次数大于1的目录规则 */ private fun getTocRule(content: String): Pattern? { val rules = getTocRules().reversed() var maxCs = 1 var tocPattern: Pattern? = null for (tocRule in rules) { val pattern = tocRule.rule.toPattern(Pattern.MULTILINE) val matcher = pattern.matcher(content) var cs = 0 while (matcher.find()) { cs++ } if (cs >= maxCs) { maxCs = cs tocPattern = pattern } } return tocPattern } /** * 获取启用的目录规则 */ private fun getTocRules(): List { return DefaultData.txtTocRules.filter { it.enable } } } ================================================ FILE: src/main/java/io/legado/app/model/localBook/UmdFile.kt ================================================ package io.legado.app.model.localBook import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.utils.* import me.ag2s.umdlib.domain.UmdBook import me.ag2s.umdlib.umd.UmdReader import java.io.File import java.io.InputStream import java.util.* import java.nio.file.Paths class UmdFile(var book: Book) { companion object { private var uFile: UmdFile? = null @Synchronized private fun getUFile(book: Book): UmdFile { if (uFile == null || uFile?.book?.bookUrl != book.bookUrl) { uFile = UmdFile(book) return uFile!! } uFile?.book = book return uFile!! } @Synchronized fun getChapterList(book: Book): ArrayList { return getUFile(book).getChapterList() } @Synchronized fun getContent(book: Book, chapter: BookChapter): String? { return getUFile(book).getContent(chapter) } @Synchronized fun getImage( book: Book, href: String ): InputStream? { return getUFile(book).getImage(href) } @Synchronized fun upBookInfo(book: Book, onlyCover: Boolean = false) { if (onlyCover) { return getUFile(book).updateCover() } return getUFile(book).upBookInfo() } } private var umdBook: UmdBook? = null get() { if (field != null) { return field } field = readUmd() return field } init { try { umdBook?.let { // if (book.coverUrl.isNullOrEmpty()) { // book.coverUrl = FileUtils.getPath( // appCtx.externalFiles, // "covers", // "${MD5Utils.md5Encode16(book.bookUrl)}.jpg" // ) // } // if (!File(book.coverUrl!!).exists()) { // FileUtils.writeBytes(book.coverUrl!!, it.cover.coverData) // } } } catch (e: Exception) { e.printStackTrace() } } private fun readUmd(): UmdBook? { val input = File(book.bookUrl).inputStream() return UmdReader().read(input) } private fun upBookInfo() { if (umdBook == null) { uFile = null book.intro = "书籍导入异常" } else { val hd = umdBook!!.header book.name = hd.title book.author = hd.author book.kind = hd.bookType updateCover() } } private fun updateCover() { if (umdBook == null) { uFile = null return } val coverFile = "${MD5Utils.md5Encode16(book.bookUrl)}.jpg" val relativeCoverUrl = Paths.get("assets", book.getUserNameSpace(), "covers", coverFile).toString() book.coverUrl = "/" + relativeCoverUrl val coverUrl = Paths.get(book.workRoot(), "storage", relativeCoverUrl).toString() if (!File(coverUrl).exists()) { FileUtils.writeBytes(coverUrl, umdBook!!.cover.coverData) } } private fun getContent(chapter: BookChapter): String? { return umdBook?.chapters?.getContentString(chapter.index) } private fun getChapterList(): ArrayList { val chapterList = ArrayList() umdBook?.chapters?.titles?.forEachIndexed { index, _ -> val title = umdBook!!.chapters.getTitle(index) val chapter = BookChapter() chapter.title = title chapter.index = index chapter.bookUrl = book.bookUrl chapter.url = index.toString() System.out.println("UMD" + chapter.url) chapterList.add(chapter) } book.latestChapterTitle = chapterList.lastOrNull()?.title book.totalChapterNum = chapterList.size return chapterList } private fun getImage(@Suppress("UNUSED_PARAMETER") href: String): InputStream? { return null } } ================================================ FILE: src/main/java/io/legado/app/model/rss/Rss.kt ================================================ package io.legado.app.model.rss import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssSource import io.legado.app.help.coroutine.Coroutine import io.legado.app.model.DebugLog import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.RuleData import io.legado.app.utils.NetworkUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlin.coroutines.CoroutineContext object Rss { suspend fun getArticles( sortName: String, sortUrl: String, rssSource: RssSource, page: Int, debugLog: DebugLog? ): Pair, String?> { val ruleData = RuleData() val analyzeUrl = AnalyzeUrl( sortUrl, page = page, source = rssSource, ruleData = ruleData, headerMapF = rssSource.getHeaderMap() ) val body = analyzeUrl.getStrResponseAwait(debugLog = debugLog).body // debugLog?.log(rssSource.sourceUrl, "┌获取链接内容:${sortUrl}") // debugLog?.log(rssSource.sourceUrl, "└\n${body}") return RssParserByRule.parseXML(sortName, sortUrl, body, rssSource, ruleData, debugLog) } suspend fun getContent( rssArticle: RssArticle, ruleContent: String, rssSource: RssSource, debugLog: DebugLog? ): String { val analyzeUrl = AnalyzeUrl( rssArticle.link, baseUrl = rssArticle.origin, source = rssSource, ruleData = rssArticle, headerMapF = rssSource.getHeaderMap() ) val body = analyzeUrl.getStrResponseAwait(debugLog = debugLog).body // debugLog?.log(rssSource.sourceUrl, "┌获取链接内容:${rssArticle.link}") // debugLog?.log(rssSource.sourceUrl, "└\n${body}") val analyzeRule = AnalyzeRule(rssArticle, rssSource) analyzeRule.setContent(body) .setBaseUrl(NetworkUtils.getAbsoluteURL(rssArticle.origin, rssArticle.link)) return analyzeRule.getString(ruleContent) } } ================================================ FILE: src/main/java/io/legado/app/model/rss/RssParserByRule.kt ================================================ package io.legado.app.model.rss import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssSource import io.legado.app.exception.NoStackTraceException import io.legado.app.model.DebugLog import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.RuleData import io.legado.app.utils.NetworkUtils import java.util.* object RssParserByRule { @Throws(Exception::class) fun parseXML( sortName: String, sortUrl: String, body: String?, rssSource: RssSource, ruleData: RuleData, debugLog: DebugLog? ): Pair, String?> { val sourceUrl = rssSource.sourceUrl var nextUrl: String? = null if (body.isNullOrBlank()) { throw NoStackTraceException( "error_get_web_content: " + rssSource.sourceUrl ) } // debugLog?.log(sourceUrl, "≡获取成功:$sourceUrl") // debugLog?.log(sourceUrl, body) var ruleArticles = rssSource.ruleArticles if (ruleArticles.isNullOrBlank()) { debugLog?.log(sourceUrl, "⇒列表规则为空, 使用默认规则解析") return RssParserDefault.parseXML(sortName, body, sourceUrl, debugLog) } else { val articleList = mutableListOf() val analyzeRule = AnalyzeRule(ruleData, rssSource) analyzeRule.setContent(body).setBaseUrl(sortUrl) analyzeRule.setRedirectUrl(sortUrl) var reverse = false if (ruleArticles.startsWith("-")) { reverse = true ruleArticles = ruleArticles.substring(1) } debugLog?.log(sourceUrl, "┌获取列表") val collections = analyzeRule.getElements(ruleArticles) debugLog?.log(sourceUrl, "└列表大小:${collections.size}") if (!rssSource.ruleNextPage.isNullOrEmpty()) { debugLog?.log(sourceUrl, "┌获取下一页链接") if (rssSource.ruleNextPage!!.uppercase(Locale.getDefault()) == "PAGE") { nextUrl = sortUrl } else { nextUrl = analyzeRule.getString(rssSource.ruleNextPage) if (nextUrl.isNotEmpty()) { nextUrl = NetworkUtils.getAbsoluteURL(sortUrl, nextUrl) } } debugLog?.log(sourceUrl, "└$nextUrl") } val ruleTitle = analyzeRule.splitSourceRule(rssSource.ruleTitle) val rulePubDate = analyzeRule.splitSourceRule(rssSource.rulePubDate) val ruleDescription = analyzeRule.splitSourceRule(rssSource.ruleDescription) val ruleImage = analyzeRule.splitSourceRule(rssSource.ruleImage) val ruleLink = analyzeRule.splitSourceRule(rssSource.ruleLink) val variable = ruleData.getVariable() for ((index, item) in collections.withIndex()) { getItem( sourceUrl, item, analyzeRule, variable, index == 0, ruleTitle, rulePubDate, ruleDescription, ruleImage, ruleLink, debugLog )?.let { it.sort = sortName it.origin = sourceUrl articleList.add(it) } } if (reverse) { articleList.reverse() } return Pair(articleList, nextUrl) } } private fun getItem( sourceUrl: String, item: Any, analyzeRule: AnalyzeRule, variable: String?, log: Boolean, ruleTitle: List, rulePubDate: List, ruleDescription: List, ruleImage: List, ruleLink: List, debugLog: DebugLog? ): RssArticle? { val rssArticle = RssArticle(variable = variable) analyzeRule.ruleData = rssArticle analyzeRule.setContent(item) debugLog?.log(sourceUrl, "┌获取标题", log) rssArticle.title = analyzeRule.getString(ruleTitle) debugLog?.log(sourceUrl, "└${rssArticle.title}", log) debugLog?.log(sourceUrl, "┌获取时间", log) rssArticle.pubDate = analyzeRule.getString(rulePubDate) debugLog?.log(sourceUrl, "└${rssArticle.pubDate}", log) debugLog?.log(sourceUrl, "┌获取描述", log) if (ruleDescription.isNullOrEmpty()) { rssArticle.description = null debugLog?.log(sourceUrl, "└描述规则为空,将会解析内容页", log) } else { rssArticle.description = analyzeRule.getString(ruleDescription) debugLog?.log(sourceUrl, "└${rssArticle.description}", log) } debugLog?.log(sourceUrl, "┌获取图片url", log) rssArticle.image = analyzeRule.getString(ruleImage, isUrl = true) debugLog?.log(sourceUrl, "└${rssArticle.image}", log) debugLog?.log(sourceUrl, "┌获取文章链接", log) rssArticle.link = NetworkUtils.getAbsoluteURL(sourceUrl, analyzeRule.getString(ruleLink)) debugLog?.log(sourceUrl, "└${rssArticle.link}", log) if (rssArticle.title.isBlank()) { return null } return rssArticle } } ================================================ FILE: src/main/java/io/legado/app/model/rss/RssParserDefault.kt ================================================ package io.legado.app.model.rss import io.legado.app.data.entities.RssArticle import io.legado.app.model.DebugLog import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserFactory import java.io.IOException import java.io.StringReader @Suppress("unused") object RssParserDefault { @Throws(XmlPullParserException::class, IOException::class) fun parseXML( sortName: String, xml: String, sourceUrl: String, debugLog: DebugLog? ): Pair, String?> { val articleList = mutableListOf() var currentArticle = RssArticle() // val factory = XmlPullParserFactory.newInstance() val factory = XmlPullParserFactory.newInstance(""" org.kxml2.io.KXmlParser org.kxml2.io.KXmlSerializer """, Thread.currentThread().getContextClassLoader().javaClass) factory.isNamespaceAware = false val xmlPullParser = factory.newPullParser() xmlPullParser.setInput(StringReader(xml)) // A flag just to be sure of the correct parsing var insideItem = false var eventType = xmlPullParser.eventType // Start parsing the xml loop@ while (eventType != XmlPullParser.END_DOCUMENT) { // Start parsing the item if (eventType == XmlPullParser.START_TAG) { when { xmlPullParser.name.equals(RSS_ITEM, true) -> insideItem = true xmlPullParser.name.equals(RSS_ITEM_TITLE, true) -> if (insideItem) currentArticle.title = xmlPullParser.nextText().trim() xmlPullParser.name.equals(RSS_ITEM_LINK, true) -> if (insideItem) currentArticle.link = xmlPullParser.nextText().trim() xmlPullParser.name.equals(RSS_ITEM_THUMBNAIL, true) -> if (insideItem) currentArticle.image = xmlPullParser.getAttributeValue(null, RSS_ITEM_URL) xmlPullParser.name.equals(RSS_ITEM_ENCLOSURE, true) -> if (insideItem) { val type = xmlPullParser.getAttributeValue(null, RSS_ITEM_TYPE) if (type != null && type.contains("image/")) { currentArticle.image = xmlPullParser.getAttributeValue(null, RSS_ITEM_URL) } } xmlPullParser.name .equals(RSS_ITEM_DESCRIPTION, true) -> if (insideItem) { val description = xmlPullParser.nextText() currentArticle.description = description.trim() if (currentArticle.image == null) { currentArticle.image = getImageUrl(description) } } xmlPullParser.name.equals(RSS_ITEM_CONTENT, true) -> if (insideItem) { val content = xmlPullParser.nextText().trim() currentArticle.content = content if (currentArticle.image == null) { currentArticle.image = getImageUrl(content) } } xmlPullParser.name .equals(RSS_ITEM_PUB_DATE, true) -> if (insideItem) { val nextTokenType = xmlPullParser.next() if (nextTokenType == XmlPullParser.TEXT) { currentArticle.pubDate = xmlPullParser.text.trim() } // Skip to be able to find date inside 'tag' tag continue@loop } xmlPullParser.name.equals(RSS_ITEM_TIME, true) -> if (insideItem) currentArticle.pubDate = xmlPullParser.nextText() } } else if (eventType == XmlPullParser.END_TAG && xmlPullParser.name.equals("item", true) ) { // The item is correctly parsed insideItem = false currentArticle.origin = sourceUrl currentArticle.sort = sortName articleList.add(currentArticle) currentArticle = RssArticle() } eventType = xmlPullParser.next() } articleList.firstOrNull()?.let { debugLog?.log(sourceUrl, "┌获取标题") debugLog?.log(sourceUrl, "└${it.title}") debugLog?.log(sourceUrl, "┌获取时间") debugLog?.log(sourceUrl, "└${it.pubDate}") debugLog?.log(sourceUrl, "┌获取描述") debugLog?.log(sourceUrl, "└${it.description}") debugLog?.log(sourceUrl, "┌获取图片url") debugLog?.log(sourceUrl, "└${it.image}") debugLog?.log(sourceUrl, "┌获取文章链接") debugLog?.log(sourceUrl, "└${it.link}") } return Pair(articleList, null) } /** * Finds the first img tag and get the src as featured image * * @param input The content in which to search for the tag * @return The url, if there is one */ private fun getImageUrl(input: String): String? { var url: String? = null val patternImg = "(]*>)".toPattern() val matcherImg = patternImg.matcher(input) if (matcherImg.find()) { val imgTag = matcherImg.group(1) val patternLink = "src\\s*=\\s*\"([^\"]+)\"".toPattern() val matcherLink = patternLink.matcher(imgTag!!) if (matcherLink.find()) { url = matcherLink.group(1)!!.trim() } } return url } private const val RSS_ITEM = "item" private const val RSS_ITEM_TITLE = "title" private const val RSS_ITEM_LINK = "link" private const val RSS_ITEM_CATEGORY = "category" private const val RSS_ITEM_THUMBNAIL = "media:thumbnail" private const val RSS_ITEM_ENCLOSURE = "enclosure" private const val RSS_ITEM_DESCRIPTION = "description" private const val RSS_ITEM_CONTENT = "content:encoded" private const val RSS_ITEM_PUB_DATE = "pubDate" private const val RSS_ITEM_TIME = "time" private const val RSS_ITEM_URL = "url" private const val RSS_ITEM_TYPE = "type" } ================================================ FILE: src/main/java/io/legado/app/model/webBook/BookChapterList.kt ================================================ package io.legado.app.model.webBook import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.rule.TocRule import io.legado.app.exception.TocEmptyException import io.legado.app.model.DebugLog import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.utils.isTrue import io.legado.app.utils.TextUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.async import kotlinx.coroutines.ensureActive import kotlinx.coroutines.withContext object BookChapterList { suspend fun analyzeChapterList( book: Book, body: String?, bookSource: BookSource, baseUrl: String, redirectUrl: String, debugLog: DebugLog? = null ): List { body ?: throw Exception( // App.INSTANCE.getString(R.string.error_get_web_content, baseUrl) //todo getString "error_get_web_content" ) val chapterList = arrayListOf() debugLog?.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") // debugLog?.log(bookSource.bookSourceUrl, body) val tocRule = bookSource.getTocRule() val nextUrlList = arrayListOf(redirectUrl) var reverse = false var listRule = tocRule.chapterList ?: "" if (listRule.startsWith("-")) { reverse = true listRule = listRule.substring(1) } if (listRule.startsWith("+")) { listRule = listRule.substring(1) } var chapterData = analyzeChapterList( book, baseUrl, redirectUrl, body, tocRule, listRule, bookSource, true, true, debugLog ) chapterList.addAll(chapterData.first) when (chapterData.second.size) { 0 -> Unit 1 -> { var nextUrl = chapterData.second[0] while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) { nextUrlList.add(nextUrl) AnalyzeUrl( mUrl = nextUrl, source = bookSource, ruleData = book, headerMapF = bookSource.getHeaderMap() ).getStrResponseAwait(debugLog = debugLog).body?.let { nextBody -> chapterData = analyzeChapterList( book, nextUrl, nextUrl, nextBody, tocRule, listRule, bookSource, true, false, debugLog ) nextUrl = chapterData.second.firstOrNull() ?: "" chapterList.addAll(chapterData.first) } } debugLog?.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}") } else -> { debugLog?.log(bookSource.bookSourceUrl, "◇并发解析目录,总页数:${chapterData.second.size}") withContext(IO) { val asyncArray = Array(chapterData.second.size) { async(IO) { val urlStr = chapterData.second[it] val analyzeUrl = AnalyzeUrl( mUrl = urlStr, source = bookSource, ruleData = book, headerMapF = bookSource.getHeaderMap() ) val res = analyzeUrl.getStrResponseAwait(debugLog = debugLog) analyzeChapterList( book, urlStr, res.url, res.body!!, tocRule, listRule, bookSource, false, false, debugLog ).first } } asyncArray.forEach { coroutine -> chapterList.addAll(coroutine.await()) } } } } if (chapterList.isEmpty()) { throw TocEmptyException("目录为空") } //去重 if (!reverse) { chapterList.reverse() } val lh = LinkedHashSet(chapterList) val list = ArrayList(lh) // if (!book.getReverseToc()) { list.reverse() // } debugLog?.log(book.origin, "◇目录总数:${list.size}") list.forEachIndexed { index, bookChapter -> bookChapter.index = index } if (list.size > 0) { book.latestChapterTitle = list.last().title } // book.durChapterTitle = // list.getOrNull(book.durChapterIndex)?.title ?: book.latestChapterTitle if (book.totalChapterNum < list.size) { book.lastCheckCount = list.size - book.totalChapterNum // book.latestChapterTime = System.currentTimeMillis() // book.lastCheckTime = System.currentTimeMillis() } book.totalChapterNum = list.size return list } private fun analyzeChapterList( book: Book, baseUrl: String, redirectUrl: String, body: String, tocRule: TocRule, listRule: String, bookSource: BookSource, getNextUrl: Boolean = true, log: Boolean = false, debugLog: DebugLog? = null ): Pair, List> { val analyzeRule = AnalyzeRule(book, bookSource) analyzeRule.setContent(body).setBaseUrl(baseUrl) analyzeRule.setRedirectUrl(redirectUrl) //获取目录列表 val chapterList = arrayListOf() if(log) debugLog?.log(bookSource.bookSourceUrl, "┌获取目录列表") val elements = analyzeRule.getElements(listRule) if(log) debugLog?.log(bookSource.bookSourceUrl, "└列表大小:${elements.size}") //获取下一页链接 val nextUrlList = arrayListOf() val nextTocRule = tocRule.nextTocUrl if (getNextUrl && !nextTocRule.isNullOrEmpty()) { if(log) debugLog?.log(bookSource.bookSourceUrl, "┌获取目录下一页列表") analyzeRule.getStringList(nextTocRule, isUrl = true)?.let { for (item in it) { if (item != redirectUrl) { nextUrlList.add(item) } } } if(log) debugLog?.log(bookSource.bookSourceUrl, "└" + TextUtils.join(",\n", nextUrlList)) } if (elements.isNotEmpty()) { if(log) debugLog?.log(bookSource.bookSourceUrl, "┌解析目录列表") val nameRule = analyzeRule.splitSourceRule(tocRule.chapterName) val urlRule = analyzeRule.splitSourceRule(tocRule.chapterUrl) val vipRule = analyzeRule.splitSourceRule(tocRule.isVip) val upTimeRule = analyzeRule.splitSourceRule(tocRule.updateTime) val isVolumeRule = analyzeRule.splitSourceRule(tocRule.isVolume) elements.forEachIndexed { index, item -> analyzeRule.setContent(item) val bookChapter = BookChapter(bookUrl = book.bookUrl, baseUrl = redirectUrl) analyzeRule.chapter = bookChapter bookChapter.title = analyzeRule.getString(nameRule) bookChapter.url = analyzeRule.getString(urlRule) bookChapter.tag = analyzeRule.getString(upTimeRule) val isVolume = analyzeRule.getString(isVolumeRule) bookChapter.isVolume = false if (isVolume.isTrue()) { bookChapter.isVolume = true } if (bookChapter.url.isEmpty()) { if (bookChapter.isVolume) { bookChapter.url = bookChapter.title + index if(log) debugLog?.log(bookSource.bookSourceUrl, "⇒一级目录${index}未获取到url,使用标题替代") } else { bookChapter.url = baseUrl if(log) debugLog?.log(bookSource.bookSourceUrl, "⇒目录${index}未获取到url,使用baseUrl替代") } } if (bookChapter.title.isNotEmpty()) { var isVip = analyzeRule.getString(vipRule) if (isVip.isTrue()) { bookChapter.title = "\uD83D\uDD12" + bookChapter.title } chapterList.add(bookChapter) } } if(log) debugLog?.log(bookSource.bookSourceUrl, "└目录列表解析完成") if(log) debugLog?.log(bookSource.bookSourceUrl, "≡首章信息") if(log) debugLog?.log(bookSource.bookSourceUrl, "◇章节名称:${chapterList[0].title}") if(log) debugLog?.log(bookSource.bookSourceUrl, "◇章节链接:${chapterList[0].url}") if(log) debugLog?.log(bookSource.bookSourceUrl, "◇章节信息:${chapterList[0].tag}") if(log) debugLog?.log(bookSource.bookSourceUrl, "◇是否卷名:${chapterList[0].isVolume}") } return Pair(chapterList, nextUrlList) } } ================================================ FILE: src/main/java/io/legado/app/model/webBook/BookContent.kt ================================================ package io.legado.app.model.webBook import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.rule.ContentRule import io.legado.app.model.DebugLog import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.utils.NetworkUtils import io.legado.app.utils.HtmlFormatter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.async import kotlinx.coroutines.ensureActive import kotlinx.coroutines.withContext object BookContent { suspend fun analyzeContent( body: String?, book: Book, bookChapter: BookChapter, bookSource: BookSource, baseUrl: String, redirectUrl: String, nextChapterUrl: String? = null, debugLog: DebugLog? = null ): String { body ?: throw Exception( "error_get_web_content" ) debugLog?.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") val mNextChapterUrl = if (!nextChapterUrl.isNullOrEmpty()) { nextChapterUrl } else { // appDb.bookChapterDao.getChapter(book.bookUrl, bookChapter.index + 1)?.url null } val content = StringBuilder() val nextUrlList = arrayListOf(redirectUrl) val contentRule = bookSource.getContentRule() val analyzeRule = AnalyzeRule(book, bookSource).setContent(body, baseUrl) analyzeRule.setRedirectUrl(redirectUrl) analyzeRule.nextChapterUrl = mNextChapterUrl var contentData = analyzeContent( book, baseUrl, redirectUrl, body, contentRule, bookChapter, bookSource, mNextChapterUrl ) content.append(contentData.first) if (contentData.second.size == 1) { var nextUrl = contentData.second[0] while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) { if (!mNextChapterUrl.isNullOrEmpty() && NetworkUtils.getAbsoluteURL(redirectUrl, nextUrl) == NetworkUtils.getAbsoluteURL(redirectUrl, mNextChapterUrl) ) break nextUrlList.add(nextUrl) val res = AnalyzeUrl( mUrl = nextUrl, source = bookSource, ruleData = book, headerMapF = bookSource.getHeaderMap() ).getStrResponseAwait(debugLog = debugLog) res.body?.let { nextBody -> contentData = analyzeContent( book, nextUrl, res.url, nextBody, contentRule, bookChapter, bookSource, mNextChapterUrl, false ) nextUrl = if (contentData.second.isNotEmpty()) contentData.second[0] else "" content.append("\n").append(contentData.first) } } debugLog?.log(bookSource.bookSourceUrl, "◇本章总页数:${nextUrlList.size}") } else if (contentData.second.size > 1) { debugLog?.log(bookSource.bookSourceUrl, "◇并发解析正文,总页数:${contentData.second.size}") withContext(IO) { val asyncArray = Array(contentData.second.size) { async(IO) { val urlStr = contentData.second[it] val analyzeUrl = AnalyzeUrl( mUrl = urlStr, source = bookSource, ruleData = book, headerMapF = bookSource.getHeaderMap() ) val res = analyzeUrl.getStrResponseAwait(debugLog = debugLog) analyzeContent( book, urlStr, res.url, res.body!!, contentRule, bookChapter, bookSource, mNextChapterUrl, false ).first } } asyncArray.forEach { coroutine -> content.append("\n").append(coroutine.await()) } } } var contentStr = content.toString() val replaceRegex = contentRule.replaceRegex if (!replaceRegex.isNullOrEmpty()) { contentStr = analyzeRule.getString(replaceRegex, contentStr) } debugLog?.log(bookSource.bookSourceUrl, "┌获取章节名称") debugLog?.log(bookSource.bookSourceUrl, "└${bookChapter.title}") debugLog?.log(bookSource.bookSourceUrl, "┌获取正文内容 (长度:${contentStr.length})") if (contentStr.length > 300) { debugLog?.log(bookSource.bookSourceUrl, "└\n${contentStr.substring(0, 50)} ... ${contentStr.substring(contentStr.length - 30, contentStr.length)}") } else { debugLog?.log(bookSource.bookSourceUrl, "└\n${contentStr}") } return contentStr } @Throws(Exception::class) private fun analyzeContent( book: Book, baseUrl: String, redirectUrl: String, body: String, contentRule: ContentRule, chapter: BookChapter, bookSource: BookSource, nextChapterUrl: String?, printLog: Boolean = true, debugLog: DebugLog? = null ): Pair> { val analyzeRule = AnalyzeRule(book, bookSource) analyzeRule.setContent(body, baseUrl) val rUrl = analyzeRule.setRedirectUrl(redirectUrl) analyzeRule.nextChapterUrl = nextChapterUrl val nextUrlList = arrayListOf() analyzeRule.chapter = chapter //获取正文 var content = analyzeRule.getString(contentRule.content) content = HtmlFormatter.formatKeepImg(content, rUrl) //获取下一页链接 val nextUrlRule = contentRule.nextContentUrl if (!nextUrlRule.isNullOrEmpty()) { if(printLog) debugLog?.log(bookSource.bookSourceUrl, "┌获取正文下一页链接") analyzeRule.getStringList(nextUrlRule, isUrl = true)?.let { nextUrlList.addAll(it) } if(printLog) debugLog?.log(bookSource.bookSourceUrl, "└" + nextUrlList.joinToString(",")) } return Pair(content, nextUrlList) } } ================================================ FILE: src/main/java/io/legado/app/model/webBook/BookInfo.kt ================================================ package io.legado.app.model.webBook import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookSource import io.legado.app.help.BookHelp import io.legado.app.model.DebugLog import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.utils.NetworkUtils import io.legado.app.utils.StringUtils.wordCountFormat import io.legado.app.utils.htmlFormat object BookInfo { @Throws(Exception::class) fun analyzeBookInfo( book: Book, body: String?, bookSource: BookSource, baseUrl: String, redirectUrl: String, canReName: Boolean, debugLog: DebugLog? = null ) { body ?: throw Exception( "error_get_web_content: " + baseUrl ) debugLog?.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") val analyzeRule = AnalyzeRule(book, bookSource) analyzeRule.setContent(body).setBaseUrl(baseUrl) analyzeRule.setRedirectUrl(redirectUrl) analyzeBookInfo(book, body, analyzeRule, bookSource, baseUrl, redirectUrl, canReName, debugLog) } @Throws(Exception::class) fun analyzeBookInfo( book: Book, body: String?, analyzeRule: AnalyzeRule, bookSource: BookSource, baseUrl: String, redirectUrl: String, canReName: Boolean, debugLog: DebugLog? = null ) { body ?: throw Exception( "error_get_web_content: " + baseUrl ) val infoRule = bookSource.getBookInfoRule() infoRule.init?.let { if (it.isNotEmpty()) { debugLog?.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则") analyzeRule.setContent(analyzeRule.getElement(it)) } } val mCanReName = canReName && !infoRule.canReName.isNullOrBlank() debugLog?.log(bookSource.bookSourceUrl, "┌获取书名") BookHelp.formatBookName(analyzeRule.getString(infoRule.name)).let { if (it.isNotEmpty() && (mCanReName || book.name.isEmpty())) { book.name = it } debugLog?.log(bookSource.bookSourceUrl, "└${it}") } debugLog?.log(bookSource.bookSourceUrl, "┌获取作者") BookHelp.formatBookAuthor(analyzeRule.getString(infoRule.author)).let { if (it.isNotEmpty() && (mCanReName || book.author.isEmpty())) { book.author = it } debugLog?.log(bookSource.bookSourceUrl, "└${it}") } debugLog?.log(bookSource.bookSourceUrl, "┌获取分类") try { analyzeRule.getStringList(infoRule.kind) ?.joinToString(",") ?.let { if (it.isNotEmpty()) book.kind = it } debugLog?.log(bookSource.bookSourceUrl, "└${book.kind}") } catch (e: Exception) { debugLog?.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") } debugLog?.log(bookSource.bookSourceUrl, "┌获取字数") try { wordCountFormat(analyzeRule.getString(infoRule.wordCount)).let { if (it.isNotEmpty()) book.wordCount = it } debugLog?.log(bookSource.bookSourceUrl, "└${book.wordCount}") } catch (e: Exception) { debugLog?.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") } debugLog?.log(bookSource.bookSourceUrl, "┌获取最新章节") try { analyzeRule.getString(infoRule.lastChapter).let { if (it.isNotEmpty()) book.latestChapterTitle = it } debugLog?.log(bookSource.bookSourceUrl, "└${book.latestChapterTitle}") } catch (e: Exception) { debugLog?.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") } debugLog?.log(bookSource.bookSourceUrl, "┌获取简介") try { analyzeRule.getString(infoRule.intro).let { if (it.isNotEmpty()) book.intro = it.htmlFormat() } debugLog?.log(bookSource.bookSourceUrl, "└${book.intro}") } catch (e: Exception) { debugLog?.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") } debugLog?.log(bookSource.bookSourceUrl, "┌获取封面链接") try { analyzeRule.getString(infoRule.coverUrl).let { if (it.isNotEmpty()) { book.coverUrl = NetworkUtils.getAbsoluteURL(redirectUrl, it) } } debugLog?.log(bookSource.bookSourceUrl, "└${book.coverUrl}") } catch (e: Exception) { debugLog?.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") } debugLog?.log(bookSource.bookSourceUrl, "┌获取目录链接") book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true) if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl if (book.tocUrl == baseUrl) { book.tocHtml = body } debugLog?.log(bookSource.bookSourceUrl, "└${book.tocUrl}") } } ================================================ FILE: src/main/java/io/legado/app/model/webBook/BookList.kt ================================================ package io.legado.app.model.webBook import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.Book import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.rule.BookListRule import io.legado.app.help.BookHelp import io.legado.app.model.DebugLog import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.utils.NetworkUtils import io.legado.app.utils.StringUtils.wordCountFormat import io.legado.app.utils.htmlFormat object BookList { @Throws(Exception::class) fun analyzeBookList( body: String?, bookSource: BookSource, analyzeUrl: AnalyzeUrl, baseUrl: String, variableBook: SearchBook, isSearch: Boolean = true, debugLog: DebugLog? = null ): ArrayList { val bookList = ArrayList() body ?: throw Exception( // App.INSTANCE.getString( // R.string.error_get_web_content, // analyzeUrl.ruleUrl // ) //todo getString "error_get_web_content" ) debugLog?.log(bookSource.bookSourceUrl, "≡获取成功:${analyzeUrl.ruleUrl}") val analyzeRule = AnalyzeRule(variableBook, bookSource) analyzeRule.setContent(body).setBaseUrl(baseUrl) analyzeRule.setRedirectUrl(baseUrl) bookSource.bookUrlPattern?.let { if (baseUrl.matches(it.toRegex())) { debugLog?.log(bookSource.bookSourceUrl, "≡链接为详情页") getInfoItem(body, analyzeRule, bookSource, analyzeUrl, baseUrl, variableBook.variable, debugLog = debugLog)?.let { searchBook -> searchBook.infoHtml = body bookList.add(searchBook) } return bookList } } val collections: List var reverse = false val bookListRule: BookListRule = when { isSearch -> bookSource.getSearchRule() bookSource.getExploreRule().bookList.isNullOrBlank() -> bookSource.getSearchRule() else -> bookSource.getExploreRule() } var ruleList: String = bookListRule.bookList ?: "" if (ruleList.startsWith("-")) { reverse = true ruleList = ruleList.substring(1) } if (ruleList.startsWith("+")) { ruleList = ruleList.substring(1) } debugLog?.log(bookSource.bookSourceUrl, "┌获取书籍列表") collections = analyzeRule.getElements(ruleList) if (collections.isEmpty() && bookSource.bookUrlPattern.isNullOrEmpty()) { debugLog?.log(bookSource.bookSourceUrl, "└列表为空,按详情页解析") getInfoItem(body, analyzeRule, bookSource, analyzeUrl, baseUrl, variableBook.variable, debugLog = debugLog)?.let { searchBook -> searchBook.infoHtml = body bookList.add(searchBook) } } else { val ruleName = analyzeRule.splitSourceRule(bookListRule.name) val ruleBookUrl = analyzeRule.splitSourceRule(bookListRule.bookUrl) val ruleAuthor = analyzeRule.splitSourceRule(bookListRule.author) val ruleCoverUrl = analyzeRule.splitSourceRule(bookListRule.coverUrl) val ruleIntro = analyzeRule.splitSourceRule(bookListRule.intro) val ruleKind = analyzeRule.splitSourceRule(bookListRule.kind) val ruleLastChapter = analyzeRule.splitSourceRule(bookListRule.lastChapter) val ruleWordCount = analyzeRule.splitSourceRule(bookListRule.wordCount) debugLog?.log(bookSource.bookSourceUrl, "└列表大小:${collections.size}") for ((index, item) in collections.withIndex()) { getSearchItem( item, analyzeRule, bookSource, baseUrl, variableBook.variable, index == 0, ruleName = ruleName, ruleBookUrl = ruleBookUrl, ruleAuthor = ruleAuthor, ruleCoverUrl = ruleCoverUrl, ruleIntro = ruleIntro, ruleKind = ruleKind, ruleLastChapter = ruleLastChapter, ruleWordCount = ruleWordCount, debugLog = debugLog )?.let { searchBook -> if (baseUrl == searchBook.bookUrl) { searchBook.infoHtml = body } bookList.add(searchBook) } } if (reverse) { bookList.reverse() } } return bookList } private fun getInfoItem( body: String, analyzeRule: AnalyzeRule, bookSource: BookSource, analyzeUrl: AnalyzeUrl, baseUrl: String, variable: String?, debugLog: DebugLog? = null ): SearchBook? { val book = Book(variable = variable) book.bookUrl = analyzeUrl.ruleUrl book.origin = bookSource.bookSourceUrl book.originName = bookSource.bookSourceName book.originOrder = bookSource.customOrder book.type = bookSource.bookSourceType analyzeRule.ruleData = book BookInfo.analyzeBookInfo( book, body, analyzeRule, bookSource, baseUrl, baseUrl, false, debugLog ) if (book.name.isNotBlank()) { return book.toSearchBook() } return null } private fun getSearchItem( item: Any, analyzeRule: AnalyzeRule, bookSource: BookSource, baseUrl: String, variable: String?, log: Boolean, ruleName: List, ruleBookUrl: List, ruleAuthor: List, ruleKind: List, ruleCoverUrl: List, ruleWordCount: List, ruleIntro: List, ruleLastChapter: List, debugLog: DebugLog? = null ): SearchBook? { val searchBook = SearchBook(variable = variable) searchBook.origin = bookSource.bookSourceUrl searchBook.originName = bookSource.bookSourceName searchBook.type = bookSource.bookSourceType searchBook.originOrder = bookSource.customOrder analyzeRule.ruleData = searchBook analyzeRule.setContent(item) if (log) debugLog?.log(bookSource.bookSourceUrl, "┌获取书名") searchBook.name = BookHelp.formatBookName(analyzeRule.getString(ruleName)) if (log) debugLog?.log(bookSource.bookSourceUrl, "└${searchBook.name}") if (searchBook.name.isNotEmpty()) { if (log) debugLog?.log(bookSource.bookSourceUrl, "┌获取作者") searchBook.author = BookHelp.formatBookAuthor(analyzeRule.getString(ruleAuthor)) if (log) debugLog?.log(bookSource.bookSourceUrl, "└${searchBook.author}") if (log) debugLog?.log(bookSource.bookSourceUrl, "┌获取分类") try { searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(",") if (log) debugLog?.log(bookSource.bookSourceUrl, "└${searchBook.kind}") } catch (e: Exception) { if (log) debugLog?.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") } if (log) debugLog?.log(bookSource.bookSourceUrl, "┌获取字数") try { searchBook.wordCount = wordCountFormat(analyzeRule.getString(ruleWordCount)) if (log) debugLog?.log(bookSource.bookSourceUrl, "└${searchBook.wordCount}") } catch (e: java.lang.Exception) { if (log) debugLog?.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") } if (log) debugLog?.log(bookSource.bookSourceUrl, "┌获取最新章节") try { searchBook.latestChapterTitle = analyzeRule.getString(ruleLastChapter) if (log) debugLog?.log(bookSource.bookSourceUrl, "└${searchBook.latestChapterTitle}") } catch (e: java.lang.Exception) { if (log) debugLog?.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") } if (log) debugLog?.log(bookSource.bookSourceUrl, "┌获取简介") try { searchBook.intro = analyzeRule.getString(ruleIntro).htmlFormat() if (log) debugLog?.log(bookSource.bookSourceUrl, "└${searchBook.intro}") } catch (e: java.lang.Exception) { if (log) debugLog?.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") } if (log) debugLog?.log(bookSource.bookSourceUrl, "┌获取封面链接") try { analyzeRule.getString(ruleCoverUrl).let { if (it.isNotEmpty()) searchBook.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it) } if (log) debugLog?.log(bookSource.bookSourceUrl, "└${searchBook.coverUrl}") } catch (e: java.lang.Exception) { if (log) debugLog?.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") } if (log) debugLog?.log(bookSource.bookSourceUrl, "┌获取详情页链接") searchBook.bookUrl = analyzeRule.getString(ruleBookUrl, isUrl = true) if (searchBook.bookUrl.isEmpty()) { searchBook.bookUrl = baseUrl } if (log) debugLog?.log(bookSource.bookSourceUrl, "└${searchBook.bookUrl}") return searchBook } return null } } ================================================ FILE: src/main/java/io/legado/app/model/webBook/WebBook.kt ================================================ package io.legado.app.model.webBook import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.SearchBook import io.legado.app.help.http.StrResponse import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.webBook.BookChapterList import io.legado.app.model.webBook.BookContent import io.legado.app.model.webBook.BookInfo import io.legado.app.model.webBook.BookList import io.legado.app.model.Debug import io.legado.app.model.DebugLog import mu.KotlinLogging import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext private val logger = KotlinLogging.logger {} class WebBook(val bookSource: BookSource, val debugLog: Boolean = true, var debugLogger: DebugLog? = null) { constructor(bookSourceString: String, debugLog: Boolean = true) : this(BookSource.fromJson(bookSourceString).getOrNull() ?: BookSource(), debugLog) val sourceUrl: String get() = bookSource.bookSourceUrl val debugger: DebugLog? get() { if (debugLogger != null) { return debugLogger } if (debugLog) { return Debug } return null } /** * 搜索 */ suspend fun searchBook( key: String, page: Int? = 1 ): List { val variableBook = SearchBook() return bookSource.searchUrl?.let { searchUrl -> val analyzeUrl = AnalyzeUrl( mUrl = searchUrl, key = key, page = page, baseUrl = bookSource.bookSourceUrl, source = bookSource, ruleData = variableBook, headerMapF = bookSource.getHeaderMap(true), ) var res = analyzeUrl.getStrResponseAwait(debugLog = debugger) //检测书源是否已登录 bookSource.loginCheckJs?.let { checkJs -> if (checkJs.isNotBlank()) { res = analyzeUrl.evalJS(checkJs, res) as StrResponse } } BookList.analyzeBookList( res.body, bookSource, analyzeUrl, res.url, variableBook, true, debugLog = debugger ).map { it.tocHtml = "" it.infoHtml = "" it } } ?: arrayListOf() } /** * 发现 */ suspend fun exploreBook( url: String, page: Int? = 1 ): List { val variableBook = SearchBook() val analyzeUrl = AnalyzeUrl( mUrl = url, page = page, baseUrl = bookSource.bookSourceUrl, source = bookSource, ruleData = variableBook, headerMapF = bookSource.getHeaderMap(true) ) var res = analyzeUrl.getStrResponseAwait(debugLog = debugger) //检测书源是否已登录 bookSource.loginCheckJs?.let { checkJs -> if (checkJs.isNotBlank()) { res = analyzeUrl.evalJS(checkJs, result = res) as StrResponse } } return BookList.analyzeBookList( res.body, bookSource, analyzeUrl, res.url, variableBook, false, debugLog = debugger ) } /** * 书籍信息 */ suspend fun getBookInfo(book: Book, canReName: Boolean = true): Book { book.type = bookSource.bookSourceType if (!book.infoHtml.isNullOrEmpty()) { BookInfo.analyzeBookInfo( book, book.infoHtml, bookSource, book.bookUrl, book.bookUrl, canReName ) return book } else { return getBookInfo(book.bookUrl, canReName) } } /** * 书籍信息 */ suspend fun getBookInfo(bookUrl: String, canReName: Boolean = true): Book { val book = Book() book.bookUrl = bookUrl book.origin = bookSource.bookSourceUrl book.originName = bookSource.bookSourceName book.originOrder = bookSource.customOrder book.type = bookSource.bookSourceType val analyzeUrl = AnalyzeUrl( mUrl = book.bookUrl, baseUrl = bookSource.bookSourceUrl, source = bookSource, ruleData = book, headerMapF = bookSource.getHeaderMap(true) ) var res = analyzeUrl.getStrResponseAwait(debugLog = debugger) //检测书源是否已登录 bookSource.loginCheckJs?.let { checkJs -> if (checkJs.isNotBlank()) { res = analyzeUrl.evalJS(checkJs, result = res) as StrResponse } } BookInfo.analyzeBookInfo(book, res.body, bookSource, book.bookUrl, res.url, canReName, debugLog = debugger) book.tocHtml = null return book } /** * 目录 */ suspend fun getChapterList( book: Book ): List { book.type = bookSource.bookSourceType return if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) { BookChapterList.analyzeChapterList( book, book.tocHtml, bookSource, book.tocUrl, book.tocUrl ) } else { val analyzeUrl = AnalyzeUrl( mUrl = book.tocUrl, baseUrl = book.bookUrl, source = bookSource, ruleData = book, headerMapF = bookSource.getHeaderMap(true) ) var res = analyzeUrl.getStrResponseAwait(debugLog = debugger) //检测书源是否已登录 bookSource.loginCheckJs?.let { checkJs -> if (checkJs.isNotBlank()) { res = analyzeUrl.evalJS(checkJs, result = res) as StrResponse } } return BookChapterList.analyzeChapterList(book, res.body, bookSource, book.tocUrl, res.url, debugLog = debugger) } } /** * 章节内容 */ suspend fun getBookContent( book: Book, bookChapter: BookChapter, // bookChapterUrl:String, nextChapterUrl: String? = null ): String { if (bookSource.getContentRule().content.isNullOrEmpty()) { debugger?.log(bookSource.bookSourceUrl, "⇒正文规则为空,使用章节链接: ${bookChapter.url}") return bookChapter.url } if (bookChapter.isVolume && bookChapter.url.startsWith(bookChapter.title)) { debugger?.log(bookSource.bookSourceUrl, "⇒一级目录正文不解析规则") return bookChapter.tag ?: "" } // val body = if (book != null && bookChapter.url == book.bookUrl && !book.tocHtml.isNullOrEmpty()) { // book.tocHtml // } else { logger.info("bookChapterUrl: {}", bookChapter.url, bookChapter.getAbsoluteURL()) val analyzeUrl = AnalyzeUrl( mUrl = bookChapter.getAbsoluteURL(), baseUrl = book.tocUrl, source = bookSource, ruleData = book, chapter = bookChapter, headerMapF = bookSource.getHeaderMap(true) ) var res = analyzeUrl.getStrResponseAwait( jsStr = bookSource.getContentRule().webJs, sourceRegex = bookSource.getContentRule().sourceRegex, debugLog = debugger ) return BookContent.analyzeContent( res.body, book, bookChapter, bookSource, bookChapter.url, res.url, nextChapterUrl, debugLog = debugger ) } } ================================================ FILE: src/main/java/io/legado/app/utils/ACache.kt ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package io.legado.app.utils import com.htmake.reader.init.appCtx import java.io.* import java.util.* import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong import kotlin.math.min import mu.KotlinLogging private val logger = KotlinLogging.logger {} /** * 本地缓存 */ @Suppress("unused", "MemberVisibilityCanBePrivate") class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) { companion object { const val TIME_HOUR = 60 * 60 const val TIME_DAY = TIME_HOUR * 24 private const val MAX_SIZE = 1000 * 1000 * 50 // 50 mb private const val MAX_COUNT = Integer.MAX_VALUE // 不限制存放数据的数量 private val mInstanceMap = HashMap() @JvmOverloads fun get( cacheName: String = "ACache", maxSize: Long = MAX_SIZE.toLong(), maxCount: Int = MAX_COUNT ): ACache { val f = File(appCtx.cacheDir, cacheName) return get(f, maxSize, maxCount) } @JvmOverloads fun get( cacheDir: File, maxSize: Long = MAX_SIZE.toLong(), maxCount: Int = MAX_COUNT ): ACache { synchronized(this) { var manager = mInstanceMap[cacheDir.absoluteFile.toString()] if (manager == null) { manager = ACache(cacheDir, maxSize, maxCount) mInstanceMap[cacheDir.absolutePath] = manager } return manager } } } private var mCache: ACacheManager? = null init { try { if (!cacheDir.exists() && !cacheDir.mkdirs()) { logger.info("ACache can't make dirs in %s" + cacheDir.absolutePath) } mCache = ACacheManager(cacheDir, max_size, max_count) } catch (e: Exception) { e.printStackTrace() } } // ======================================= // ============ String数据 读写 ============== // ======================================= /** * 保存 String数据 到 缓存中 * * @param key 保存的key * @param value 保存的String数据 */ fun put(key: String, value: String) { mCache?.let { mCache -> try { val file = mCache.newFile(key) file.writeText(value) mCache.put(file) } catch (e: Exception) { e.printStackTrace() } } } /** * 保存 String数据 到 缓存中 * * @param key 保存的key * @param value 保存的String数据 * @param saveTime 保存的时间,单位:秒 */ fun put(key: String, value: String, saveTime: Int) { put(key, Utils.newStringWithDateInfo(saveTime, value)) } /** * 读取 String数据 * * @return String 数据 */ fun getAsString(key: String): String? { mCache?.let { mCache -> val file = mCache[key] if (!file.exists()) return null var removeFile = false try { val text = file.readText() if (!Utils.isDue(text)) { return Utils.clearDateInfo(text) } else { removeFile = true } } catch (e: IOException) { e.printStackTrace() } finally { if (removeFile) remove(key) } } return null } fun getByHashCode(hashCode: String): String? { mCache?.let { mCache -> val file = mCache.newFileFromHashCode(hashCode) if (!file.exists()) return null var removeFile = false try { val text = file.readText() if (!Utils.isDue(text)) { return Utils.clearDateInfo(text) } else { removeFile = true } } catch (e: IOException) { e.printStackTrace() } finally { if (removeFile) file.delete() } } return null } // ======================================= // ========== JSONObject 数据 读写 ========= // ======================================= // /** // * 保存 JSONObject数据 到 缓存中 // * // * @param key 保存的key // * @param value 保存的JSON数据 // */ // fun put(key: String, value: JSONObject) { // put(key, value.toString()) // } // /** // * 保存 JSONObject数据 到 缓存中 // * // * @param key 保存的key // * @param value 保存的JSONObject数据 // * @param saveTime 保存的时间,单位:秒 // */ // fun put(key: String, value: JSONObject, saveTime: Int) { // put(key, value.toString(), saveTime) // } // /** // * 读取JSONObject数据 // * // * @return JSONObject数据 // */ // fun getAsJSONObject(key: String): JSONObject? { // val json = getAsString(key) ?: return null // return try { // JSONObject(json) // } catch (e: Exception) { // null // } // } // // ======================================= // // ============ JSONArray 数据 读写 ============= // // ======================================= // /** // * 保存 JSONArray数据 到 缓存中 // * // * @param key 保存的key // * @param value 保存的JSONArray数据 // */ // fun put(key: String, value: JSONArray) { // put(key, value.toString()) // } // /** // * 保存 JSONArray数据 到 缓存中 // * // * @param key 保存的key // * @param value 保存的JSONArray数据 // * @param saveTime 保存的时间,单位:秒 // */ // fun put(key: String, value: JSONArray, saveTime: Int) { // put(key, value.toString(), saveTime) // } // /** // * 读取JSONArray数据 // * // * @return JSONArray数据 // */ // fun getAsJSONArray(key: String): JSONArray? { // val json = getAsString(key) // return try { // JSONArray(json) // } catch (e: Exception) { // null // } // } // ======================================= // ============== byte 数据 读写 ============= // ======================================= /** * 保存 byte数据 到 缓存中 * * @param key 保存的key * @param value 保存的数据 */ fun put(key: String, value: ByteArray) { mCache?.let { mCache -> val file = mCache.newFile(key) file.writeBytes(value) mCache.put(file) } } /** * 保存 byte数据 到 缓存中 * * @param key 保存的key * @param value 保存的数据 * @param saveTime 保存的时间,单位:秒 */ fun put(key: String, value: ByteArray, saveTime: Int) { put(key, Utils.newByteArrayWithDateInfo(saveTime, value)) } /** * 获取 byte 数据 * * @return byte 数据 */ fun getAsBinary(key: String): ByteArray? { mCache?.let { mCache -> var removeFile = false try { val file = mCache[key] if (!file.exists()) return null val byteArray = file.readBytes() return if (!Utils.isDue(byteArray)) { Utils.clearDateInfo(byteArray) } else { removeFile = true null } } catch (e: Exception) { e.printStackTrace() } finally { if (removeFile) remove(key) } } return null } /** * 保存 Serializable数据到 缓存中 * * @param key 保存的key * @param value 保存的value * @param saveTime 保存的时间,单位:秒 */ @JvmOverloads fun put(key: String, value: Serializable, saveTime: Int = -1) { try { val byteArrayOutputStream = ByteArrayOutputStream() ObjectOutputStream(byteArrayOutputStream).use { oos -> oos.writeObject(value) val data = byteArrayOutputStream.toByteArray() if (saveTime != -1) { put(key, data, saveTime) } else { put(key, data) } } } catch (e: Exception) { e.printStackTrace() } } /** * 读取 Serializable数据 * * @return Serializable 数据 */ fun getAsObject(key: String): Any? { val data = getAsBinary(key) if (data != null) { var bis: ByteArrayInputStream? = null var ois: ObjectInputStream? = null try { bis = ByteArrayInputStream(data) ois = ObjectInputStream(bis) return ois.readObject() } catch (e: Exception) { e.printStackTrace() } finally { try { bis?.close() } catch (e: IOException) { e.printStackTrace() } try { ois?.close() } catch (e: IOException) { e.printStackTrace() } } } return null } // ======================================= // ============== bitmap 数据 读写 ============= // ======================================= // /** // * 保存 bitmap 到 缓存中 // * // * @param key 保存的key // * @param value 保存的bitmap数据 // */ // fun put(key: String, value: Bitmap) { // put(key, Utils.bitmap2Bytes(value)) // } // /** // * 保存 bitmap 到 缓存中 // * // * @param key 保存的key // * @param value 保存的 bitmap 数据 // * @param saveTime 保存的时间,单位:秒 // */ // fun put(key: String, value: Bitmap, saveTime: Int) { // put(key, Utils.bitmap2Bytes(value), saveTime) // } /** * 读取 bitmap 数据 * * @return bitmap 数据 */ // fun getAsBitmap(key: String): Bitmap? { // return if (getAsBinary(key) == null) { // null // } else Utils.bytes2Bitmap(getAsBinary(key)!!) // } // ======================================= // ============= drawable 数据 读写 ============= // ======================================= // /** // * 保存 drawable 到 缓存中 // * // * @param key 保存的key // * @param value 保存的drawable数据 // */ // fun put(key: String, value: Drawable) { // put(key, Utils.drawable2Bitmap(value)) // } // /** // * 保存 drawable 到 缓存中 // * // * @param key 保存的key // * @param value 保存的 drawable 数据 // * @param saveTime 保存的时间,单位:秒 // */ // fun put(key: String, value: Drawable, saveTime: Int) { // put(key, Utils.drawable2Bitmap(value), saveTime) // } /** * 读取 Drawable 数据 * * @return Drawable 数据 */ // fun getAsDrawable(key: String): Drawable? { // return if (getAsBinary(key) == null) { // null // } else Utils.bitmap2Drawable( // Utils.bytes2Bitmap( // getAsBinary(key)!! // ) // ) // } /** * 获取缓存文件 * * @return value 缓存的文件 */ fun file(key: String): File? { mCache?.let { mCache -> try { val f = mCache.newFile(key) if (f.exists()) { return f } } catch (e: Exception) { e.printStackTrace() } } return null } /** * 移除某个key * * @return 是否移除成功 */ fun remove(key: String): Boolean { return mCache?.remove(key) == true } /** * 清除所有数据 */ fun clear() { mCache?.clear() } /** * @author 杨福海(michael) www.yangfuhai.com * @version 1.0 * title 时间计算工具类 */ private object Utils { private const val mSeparator = ' ' /** * 判断缓存的String数据是否到期 * * @return true:到期了 false:还没有到期 */ fun isDue(str: String): Boolean { return isDue(str.toByteArray()) } /** * 判断缓存的byte数据是否到期 * * @return true:到期了 false:还没有到期 */ fun isDue(data: ByteArray): Boolean { try { val text = getDateInfoFromDate(data) if (text != null && text.size == 2) { var saveTimeStr = text[0] while (saveTimeStr.startsWith("0")) { saveTimeStr = saveTimeStr .substring(1) } val saveTime = java.lang.Long.valueOf(saveTimeStr) val deleteAfter = java.lang.Long.valueOf(text[1]) if (System.currentTimeMillis() > saveTime + deleteAfter * 1000) { return true } } } catch (e: Exception) { e.printStackTrace() } return false } fun newStringWithDateInfo(second: Int, strInfo: String): String { return createDateInfo(second) + strInfo } fun newByteArrayWithDateInfo(second: Int, data2: ByteArray): ByteArray { val data1 = createDateInfo(second).toByteArray() val retData = ByteArray(data1.size + data2.size) System.arraycopy(data1, 0, retData, 0, data1.size) System.arraycopy(data2, 0, retData, data1.size, data2.size) return retData } fun clearDateInfo(strInfo: String?): String? { strInfo?.let { if (hasDateInfo(strInfo.toByteArray())) { return strInfo.substring(strInfo.indexOf(mSeparator) + 1) } } return strInfo } fun clearDateInfo(data: ByteArray): ByteArray { return if (hasDateInfo(data)) { copyOfRange( data, indexOf(data, mSeparator) + 1, data.size ) } else data } fun hasDateInfo(data: ByteArray?): Boolean { return (data != null && data.size > 15 && data[13] == '-'.code.toByte() && indexOf(data, mSeparator) > 14) } fun getDateInfoFromDate(data: ByteArray): Array? { if (hasDateInfo(data)) { val saveDate = String(copyOfRange(data, 0, 13)) val deleteAfter = String( copyOfRange( data, 14, indexOf(data, mSeparator) ) ) return arrayOf(saveDate, deleteAfter) } return null } @Suppress("SameParameterValue") private fun indexOf(data: ByteArray, c: Char): Int { for (i in data.indices) { if (data[i] == c.code.toByte()) { return i } } return -1 } private fun copyOfRange(original: ByteArray, from: Int, to: Int): ByteArray { val newLength = to - from require(newLength >= 0) { "$from > $to" } val copy = ByteArray(newLength) System.arraycopy( original, from, copy, 0, min(original.size - from, newLength) ) return copy } private fun createDateInfo(second: Int): String { val currentTime = StringBuilder(System.currentTimeMillis().toString() + "") while (currentTime.length < 13) { currentTime.insert(0, "0") } return "$currentTime-$second$mSeparator" } // /* // * Bitmap → byte[] // */ // fun bitmap2Bytes(bm: Bitmap): ByteArray { // val byteArrayOutputStream = ByteArrayOutputStream() // bm.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream) // return byteArrayOutputStream.toByteArray() // } // /* // * byte[] → Bitmap // */ // fun bytes2Bitmap(b: ByteArray): Bitmap? { // return if (b.isEmpty()) { // null // } else BitmapFactory.decodeByteArray(b, 0, b.size) // } // /* // * Drawable → Bitmap // */ // fun drawable2Bitmap(drawable: Drawable): Bitmap { // // 取 drawable 的长宽 // val w = drawable.intrinsicWidth // val h = drawable.intrinsicHeight // // 取 drawable 的颜色格式 // @Suppress("DEPRECATION") // val config = if (drawable.opacity != PixelFormat.OPAQUE) // Bitmap.Config.ARGB_8888 // else // Bitmap.Config.RGB_565 // // 建立对应 bitmap // val bitmap = Bitmap.createBitmap(w, h, config) // // 建立对应 bitmap 的画布 // val canvas = Canvas(bitmap) // drawable.setBounds(0, 0, w, h) // // 把 drawable 内容画到画布中 // drawable.draw(canvas) // return bitmap // } // /* // * Bitmap → Drawable // */ // fun bitmap2Drawable(bm: Bitmap?): Drawable? { // return if (bm == null) { // null // } else BitmapDrawable(appCtx.resources, bm) // } } /** * @author 杨福海(michael) www.yangfuhai.com * @version 1.0 * title 缓存管理器 */ open inner class ACacheManager( private var cacheDir: File, private val sizeLimit: Long, private val countLimit: Int ) { private val cacheSize: AtomicLong = AtomicLong() private val cacheCount: AtomicInteger = AtomicInteger() private val lastUsageDates = Collections .synchronizedMap(HashMap()) init { calculateCacheSizeAndCacheCount() } /** * 计算 cacheSize和cacheCount */ private fun calculateCacheSizeAndCacheCount() { Thread { try { var size = 0 var count = 0 val cachedFiles = cacheDir.listFiles() if (cachedFiles != null) { for (cachedFile in cachedFiles) { size += calculateSize(cachedFile).toInt() count += 1 lastUsageDates[cachedFile] = cachedFile.lastModified() } cacheSize.set(size.toLong()) cacheCount.set(count) } } catch (e: Exception) { e.printStackTrace() } }.start() } fun put(file: File) { try { var curCacheCount = cacheCount.get() while (curCacheCount + 1 > countLimit) { val freedSize = removeNext() cacheSize.addAndGet(-freedSize) curCacheCount = cacheCount.addAndGet(-1) } cacheCount.addAndGet(1) val valueSize = calculateSize(file) var curCacheSize = cacheSize.get() while (curCacheSize + valueSize > sizeLimit) { val freedSize = removeNext() curCacheSize = cacheSize.addAndGet(-freedSize) } cacheSize.addAndGet(valueSize) val currentTime = System.currentTimeMillis() file.setLastModified(currentTime) lastUsageDates[file] = currentTime } catch (e: Exception) { e.printStackTrace() } } operator fun get(key: String): File { val file = newFile(key) val currentTime = System.currentTimeMillis() file.setLastModified(currentTime) lastUsageDates[file] = currentTime return file } fun newFile(key: String): File { return File(cacheDir, key.hashCode().toString() + "") } fun newFileFromHashCode(hashCode: String): File { return File(cacheDir, hashCode) } fun remove(key: String): Boolean { val image = get(key) return image.delete() } fun clear() { try { lastUsageDates.clear() cacheSize.set(0) val files = cacheDir.listFiles() if (files != null) { for (f in files) { f.delete() } } } catch (e: Exception) { e.printStackTrace() } } /** * 移除旧的文件 */ private fun removeNext(): Long { try { if (lastUsageDates.isEmpty()) { return 0 } var oldestUsage: Long? = null var mostLongUsedFile: File? = null val entries = lastUsageDates.entries synchronized(lastUsageDates) { for ((key, lastValueUsage) in entries) { if (mostLongUsedFile == null) { mostLongUsedFile = key oldestUsage = lastValueUsage } else { if (lastValueUsage < oldestUsage!!) { oldestUsage = lastValueUsage mostLongUsedFile = key } } } } var fileSize: Long = 0 if (mostLongUsedFile != null) { fileSize = calculateSize(mostLongUsedFile!!) if (mostLongUsedFile!!.delete()) { lastUsageDates.remove(mostLongUsedFile) } } return fileSize } catch (e: Exception) { e.printStackTrace() return 0 } } private fun calculateSize(file: File): Long { return file.length() } } } ================================================ FILE: src/main/java/io/legado/app/utils/AnkoHelps.kt ================================================ package io.legado.app.utils inline fun attempt(f: () -> T): AttemptResult { var value: T? = null var error: Throwable? = null try { value = f() } catch(t: Throwable) { error = t } return AttemptResult(value, error) } data class AttemptResult @PublishedApi internal constructor(val value: T?, val error: Throwable?) { inline fun then(f: (T) -> R): AttemptResult { if (isError) { @Suppress("UNCHECKED_CAST") return this as AttemptResult } return attempt { f(value as T) } } inline val isError: Boolean get() = error != null inline val hasValue: Boolean get() = error == null } ================================================ FILE: src/main/java/io/legado/app/utils/Base64.java ================================================ package io.legado.app.utils; import java.io.UnsupportedEncodingException; public class Base64 { /** * Default values for encoder/decoder flags. */ public static final int DEFAULT = 0; /** * Encoder flag bit to omit the padding '=' characters at the end * of the output (if any). */ public static final int NO_PADDING = 1; /** * Encoder flag bit to omit all line terminators (i.e., the output * will be on one long line). */ public static final int NO_WRAP = 2; /** * Encoder flag bit to indicate lines should be terminated with a * CRLF pair instead of just an LF. Has no effect if {@code * NO_WRAP} is specified as well. */ public static final int CRLF = 4; /** * Encoder/decoder flag bit to indicate using the "URL and * filename safe" variant of Base64 (see RFC 3548 section 4) where * {@code -} and {@code _} are used in place of {@code +} and * {@code /}. */ public static final int URL_SAFE = 8; /** * Flag to pass to {@link Base64OutputStream} to indicate that it * should not close the output stream it is wrapping when it * itself is closed. */ public static final int NO_CLOSE = 16; // -------------------------------------------------------- // shared code // -------------------------------------------------------- /* package */ static abstract class Coder { public byte[] output; public int op; /** * Encode/decode another block of input data. this.output is * provided by the caller, and must be big enough to hold all * the coded data. On exit, this.opwill be set to the length * of the coded data. * * @param finish true if this is the final call to process for * this object. Will finalize the coder state and * include any final bytes in the output. * @return true if the input so far is good; false if some * error has been detected in the input stream.. */ public abstract boolean process(byte[] input, int offset, int len, boolean finish); /** * @return the maximum number of bytes a call to process() * could produce for the given number of input bytes. This may * be an overestimate. */ public abstract int maxOutputSize(int len); } // -------------------------------------------------------- // decoding // -------------------------------------------------------- /** * Decode the Base64-encoded data in input and return the data in * a new byte array. * *

The padding '=' characters at the end are considered optional, but * if any are present, there must be the correct number of them. * * @param str the input String to decode, which is converted to * bytes using the default charset * @param flags controls certain features of the decoded output. * Pass {@code DEFAULT} to decode standard Base64. * @throws IllegalArgumentException if the input contains * incorrect padding */ public static byte[] decode(String str, int flags) { return decode(str.getBytes(), flags); } /** * Decode the Base64-encoded data in input and return the data in * a new byte array. * *

The padding '=' characters at the end are considered optional, but * if any are present, there must be the correct number of them. * * @param input the input array to decode * @param flags controls certain features of the decoded output. * Pass {@code DEFAULT} to decode standard Base64. * @throws IllegalArgumentException if the input contains * incorrect padding */ public static byte[] decode(byte[] input, int flags) { return decode(input, 0, input.length, flags); } /** * Decode the Base64-encoded data in input and return the data in * a new byte array. * *

The padding '=' characters at the end are considered optional, but * if any are present, there must be the correct number of them. * * @param input the data to decode * @param offset the position within the input array at which to start * @param len the number of bytes of input to decode * @param flags controls certain features of the decoded output. * Pass {@code DEFAULT} to decode standard Base64. * @throws IllegalArgumentException if the input contains * incorrect padding */ public static byte[] decode(byte[] input, int offset, int len, int flags) { // Allocate space for the most data the input could represent. // (It could contain less if it contains whitespace, etc.) Decoder decoder = new Decoder(flags, new byte[len * 3 / 4]); if (!decoder.process(input, offset, len, true)) { throw new IllegalArgumentException("bad base-64"); } // Maybe we got lucky and allocated exactly enough output space. if (decoder.op == decoder.output.length) { return decoder.output; } // Need to shorten the array, so allocate a new one of the // right size and copy. byte[] temp = new byte[decoder.op]; System.arraycopy(decoder.output, 0, temp, 0, decoder.op); return temp; } /* package */ static class Decoder extends Coder { /** * Lookup table for turning bytes into their position in the * Base64 alphabet. */ private static final int DECODE[] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, }; /** * Decode lookup table for the "web safe" variant (RFC 3548 * sec. 4) where - and _ replace + and /. */ private static final int DECODE_WEBSAFE[] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, }; /** * Non-data values in the DECODE arrays. */ private static final int SKIP = -1; private static final int EQUALS = -2; /** * States 0-3 are reading through the next input tuple. * State 4 is having read one '=' and expecting exactly * one more. * State 5 is expecting no more data or padding characters * in the input. * State 6 is the error state; an error has been detected * in the input and no future input can "fix" it. */ private int state; // state number (0 to 6) private int value; final private int[] alphabet; public Decoder(int flags, byte[] output) { this.output = output; alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE; state = 0; value = 0; } /** * @return an overestimate for the number of bytes {@code * len} bytes could decode to. */ public int maxOutputSize(int len) { return len * 3 / 4 + 10; } /** * Decode another block of input data. * * @return true if the state machine is still healthy. false if * bad base-64 data has been detected in the input stream. */ public boolean process(byte[] input, int offset, int len, boolean finish) { if (this.state == 6) return false; int p = offset; len += offset; // Using local variables makes the decoder about 12% // faster than if we manipulate the member variables in // the loop. (Even alphabet makes a measurable // difference, which is somewhat surprising to me since // the member variable is final.) int state = this.state; int value = this.value; int op = 0; final byte[] output = this.output; final int[] alphabet = this.alphabet; while (p < len) { // Try the fast path: we're starting a new tuple and the // next four bytes of the input stream are all data // bytes. This corresponds to going through states // 0-1-2-3-0. We expect to use this method for most of // the data. // // If any of the next four bytes of input are non-data // (whitespace, etc.), value will end up negative. (All // the non-data values in decode are small negative // numbers, so shifting any of them up and or'ing them // together will result in a value with its top bit set.) // // You can remove this whole block and the output should // be the same, just slower. if (state == 0) { while (p + 4 <= len && (value = ((alphabet[input[p] & 0xff] << 18) | (alphabet[input[p + 1] & 0xff] << 12) | (alphabet[input[p + 2] & 0xff] << 6) | (alphabet[input[p + 3] & 0xff]))) >= 0) { output[op + 2] = (byte) value; output[op + 1] = (byte) (value >> 8); output[op] = (byte) (value >> 16); op += 3; p += 4; } if (p >= len) break; } // The fast path isn't available -- either we've read a // partial tuple, or the next four input bytes aren't all // data, or whatever. Fall back to the slower state // machine implementation. int d = alphabet[input[p++] & 0xff]; switch (state) { case 0: if (d >= 0) { value = d; ++state; } else if (d != SKIP) { this.state = 6; return false; } break; case 1: if (d >= 0) { value = (value << 6) | d; ++state; } else if (d != SKIP) { this.state = 6; return false; } break; case 2: if (d >= 0) { value = (value << 6) | d; ++state; } else if (d == EQUALS) { // Emit the last (partial) output tuple; // expect exactly one more padding character. output[op++] = (byte) (value >> 4); state = 4; } else if (d != SKIP) { this.state = 6; return false; } break; case 3: if (d >= 0) { // Emit the output triple and return to state 0. value = (value << 6) | d; output[op + 2] = (byte) value; output[op + 1] = (byte) (value >> 8); output[op] = (byte) (value >> 16); op += 3; state = 0; } else if (d == EQUALS) { // Emit the last (partial) output tuple; // expect no further data or padding characters. output[op + 1] = (byte) (value >> 2); output[op] = (byte) (value >> 10); op += 2; state = 5; } else if (d != SKIP) { this.state = 6; return false; } break; case 4: if (d == EQUALS) { ++state; } else if (d != SKIP) { this.state = 6; return false; } break; case 5: if (d != SKIP) { this.state = 6; return false; } break; } } if (!finish) { // We're out of input, but a future call could provide // more. this.state = state; this.value = value; this.op = op; return true; } // Done reading input. Now figure out where we are left in // the state machine and finish up. switch (state) { case 0: // Output length is a multiple of three. Fine. break; case 1: // Read one extra input byte, which isn't enough to // make another output byte. Illegal. this.state = 6; return false; case 2: // Read two extra input bytes, enough to emit 1 more // output byte. Fine. output[op++] = (byte) (value >> 4); break; case 3: // Read three extra input bytes, enough to emit 2 more // output bytes. Fine. output[op++] = (byte) (value >> 10); output[op++] = (byte) (value >> 2); break; case 4: // Read one padding '=' when we expected 2. Illegal. this.state = 6; return false; case 5: // Read all the padding '='s we expected and no more. // Fine. break; } this.state = state; this.op = op; return true; } } // -------------------------------------------------------- // encoding // -------------------------------------------------------- /** * Base64-encode the given data and return a newly allocated * String with the result. * * @param input the data to encode * @param flags controls certain features of the encoded output. * Passing {@code DEFAULT} results in output that * adheres to RFC 2045. */ public static String encodeToString(byte[] input, int flags) { try { return new String(encode(input, flags), "US-ASCII"); } catch (UnsupportedEncodingException e) { // US-ASCII is guaranteed to be available. throw new AssertionError(e); } } /** * Base64-encode the given data and return a newly allocated * String with the result. * * @param input the data to encode * @param offset the position within the input array at which to * start * @param len the number of bytes of input to encode * @param flags controls certain features of the encoded output. * Passing {@code DEFAULT} results in output that * adheres to RFC 2045. */ public static String encodeToString(byte[] input, int offset, int len, int flags) { try { return new String(encode(input, offset, len, flags), "US-ASCII"); } catch (UnsupportedEncodingException e) { // US-ASCII is guaranteed to be available. throw new AssertionError(e); } } /** * Base64-encode the given data and return a newly allocated * byte[] with the result. * * @param input the data to encode * @param flags controls certain features of the encoded output. * Passing {@code DEFAULT} results in output that * adheres to RFC 2045. */ public static byte[] encode(byte[] input, int flags) { return encode(input, 0, input.length, flags); } /** * Base64-encode the given data and return a newly allocated * byte[] with the result. * * @param input the data to encode * @param offset the position within the input array at which to * start * @param len the number of bytes of input to encode * @param flags controls certain features of the encoded output. * Passing {@code DEFAULT} results in output that * adheres to RFC 2045. */ public static byte[] encode(byte[] input, int offset, int len, int flags) { Encoder encoder = new Encoder(flags, null); // Compute the exact length of the array we will produce. int output_len = len / 3 * 4; // Account for the tail of the data and the padding bytes, if any. if (encoder.do_padding) { if (len % 3 > 0) { output_len += 4; } } else { switch (len % 3) { case 0: break; case 1: output_len += 2; break; case 2: output_len += 3; break; } } // Account for the newlines, if any. if (encoder.do_newline && len > 0) { output_len += (((len - 1) / (3 * Encoder.LINE_GROUPS)) + 1) * (encoder.do_cr ? 2 : 1); } encoder.output = new byte[output_len]; encoder.process(input, offset, len, true); assert encoder.op == output_len; return encoder.output; } /* package */ static class Encoder extends Coder { /** * Emit a new line every this many output tuples. Corresponds to * a 76-character line length (the maximum allowable according to * RFC 2045). */ public static final int LINE_GROUPS = 19; /** * Lookup table for turning Base64 alphabet positions (6 bits) * into output bytes. */ private static final byte ENCODE[] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', }; /** * Lookup table for turning Base64 alphabet positions (6 bits) * into output bytes. */ private static final byte ENCODE_WEBSAFE[] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_', }; final private byte[] tail; /* package */ int tailLen; private int count; final public boolean do_padding; final public boolean do_newline; final public boolean do_cr; final private byte[] alphabet; public Encoder(int flags, byte[] output) { this.output = output; do_padding = (flags & NO_PADDING) == 0; do_newline = (flags & NO_WRAP) == 0; do_cr = (flags & CRLF) != 0; alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE; tail = new byte[2]; tailLen = 0; count = do_newline ? LINE_GROUPS : -1; } /** * @return an overestimate for the number of bytes {@code * len} bytes could encode to. */ public int maxOutputSize(int len) { return len * 8 / 5 + 10; } public boolean process(byte[] input, int offset, int len, boolean finish) { // Using local variables makes the encoder about 9% faster. final byte[] alphabet = this.alphabet; final byte[] output = this.output; int op = 0; int count = this.count; int p = offset; len += offset; int v = -1; // First we need to concatenate the tail of the previous call // with any input bytes available now and see if we can empty // the tail. switch (tailLen) { case 0: // There was no tail. break; case 1: if (p + 2 <= len) { // A 1-byte tail with at least 2 bytes of // input available now. v = ((tail[0] & 0xff) << 16) | ((input[p++] & 0xff) << 8) | (input[p++] & 0xff); tailLen = 0; } ; break; case 2: if (p + 1 <= len) { // A 2-byte tail with at least 1 byte of input. v = ((tail[0] & 0xff) << 16) | ((tail[1] & 0xff) << 8) | (input[p++] & 0xff); tailLen = 0; } break; } if (v != -1) { output[op++] = alphabet[(v >> 18) & 0x3f]; output[op++] = alphabet[(v >> 12) & 0x3f]; output[op++] = alphabet[(v >> 6) & 0x3f]; output[op++] = alphabet[v & 0x3f]; if (--count == 0) { if (do_cr) output[op++] = '\r'; output[op++] = '\n'; count = LINE_GROUPS; } } // At this point either there is no tail, or there are fewer // than 3 bytes of input available. // The main loop, turning 3 input bytes into 4 output bytes on // each iteration. while (p + 3 <= len) { v = ((input[p] & 0xff) << 16) | ((input[p + 1] & 0xff) << 8) | (input[p + 2] & 0xff); output[op] = alphabet[(v >> 18) & 0x3f]; output[op + 1] = alphabet[(v >> 12) & 0x3f]; output[op + 2] = alphabet[(v >> 6) & 0x3f]; output[op + 3] = alphabet[v & 0x3f]; p += 3; op += 4; if (--count == 0) { if (do_cr) output[op++] = '\r'; output[op++] = '\n'; count = LINE_GROUPS; } } if (finish) { // Finish up the tail of the input. Note that we need to // consume any bytes in tail before any bytes // remaining in input; there should be at most two bytes // total. if (p - tailLen == len - 1) { int t = 0; v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4; tailLen -= t; output[op++] = alphabet[(v >> 6) & 0x3f]; output[op++] = alphabet[v & 0x3f]; if (do_padding) { output[op++] = '='; output[op++] = '='; } if (do_newline) { if (do_cr) output[op++] = '\r'; output[op++] = '\n'; } } else if (p - tailLen == len - 2) { int t = 0; v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) | (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2); tailLen -= t; output[op++] = alphabet[(v >> 12) & 0x3f]; output[op++] = alphabet[(v >> 6) & 0x3f]; output[op++] = alphabet[v & 0x3f]; if (do_padding) { output[op++] = '='; } if (do_newline) { if (do_cr) output[op++] = '\r'; output[op++] = '\n'; } } else if (do_newline && op > 0 && count != LINE_GROUPS) { if (do_cr) output[op++] = '\r'; output[op++] = '\n'; } assert tailLen == 0; assert p == len; } else { // Save the leftovers in tail to be consumed on the next // call to encodeInternal. if (p == len - 1) { tail[tailLen++] = input[p]; } else if (p == len - 2) { tail[tailLen++] = input[p]; tail[tailLen++] = input[p + 1]; } } this.op = op; this.count = count; return true; } } private Base64() { } // don't instantiate } ================================================ FILE: src/main/java/io/legado/app/utils/EncoderUtils.kt ================================================ package io.legado.app.utils import io.legado.app.utils.Base64 import java.security.spec.AlgorithmParameterSpec import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec @Suppress("unused") object EncoderUtils { fun escape(src: String): String { val tmp = StringBuilder() for (char in src) { val charCode = char.code if (charCode in 48..57 || charCode in 65..90 || charCode in 97..122) { tmp.append(char) continue } val prefix = when { charCode < 16 -> "%0" charCode < 256 -> "%" else -> "%u" } tmp.append(prefix).append(charCode.toString(16)) } return tmp.toString() } @JvmOverloads fun base64Decode(str: String, flags: Int = Base64.DEFAULT): String { val bytes = Base64.decode(str, flags) return String(bytes) } @JvmOverloads fun base64Encode(str: String, flags: Int = Base64.NO_WRAP): String? { return Base64.encodeToString(str.toByteArray(), flags) } //////////AES Start /** * Return the Base64-encode bytes of AES encryption. * * @param data The data. * @param key The key. * @param transformation The name of the transformation, * 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*. * @param iv The buffer with the IV. The contents of the * buffer are copied to protect against subsequent modification. * @return the Base64-encode bytes of AES encryption */ @Throws(Exception::class) fun encryptAES2Base64( data: ByteArray?, key: ByteArray?, transformation: String? = "DES/ECB/PKCS5Padding", iv: ByteArray? = null ): ByteArray? { return Base64.encode(encryptAES(data, key, transformation, iv), Base64.NO_WRAP) } /** * Return the bytes of AES encryption. * * @param data The data. * @param key The key. * @param transformation The name of the transformation, * 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*. * @param iv The buffer with the IV. The contents of the * buffer are copied to protect against subsequent modification. * @return the bytes of AES encryption */ @Throws(Exception::class) fun encryptAES( data: ByteArray?, key: ByteArray?, transformation: String? = "DES/ECB/PKCS5Padding", iv: ByteArray? = null ): ByteArray? { return symmetricTemplate(data, key, "AES", transformation!!, iv, true) } /** * Return the bytes of AES decryption for Base64-encode bytes. * * @param data The data. * @param key The key. * @param transformation The name of the transformation, * 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*. * @param iv The buffer with the IV. The contents of the * buffer are copied to protect against subsequent modification. * @return the bytes of AES decryption for Base64-encode bytes */ @Throws(Exception::class) fun decryptBase64AES( data: ByteArray?, key: ByteArray?, transformation: String = "DES/ECB/PKCS5Padding", iv: ByteArray? = null ): ByteArray? { return decryptAES(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv) } /** * Return the bytes of AES decryption. * * @param data The data. * @param key The key. * @param transformation The name of the transformation, * 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*. * @param iv The buffer with the IV. The contents of the * buffer are copied to protect against subsequent modification. * @return the bytes of AES decryption */ @Throws(Exception::class) fun decryptAES( data: ByteArray?, key: ByteArray?, transformation: String = "DES/ECB/PKCS5Padding", iv: ByteArray? = null ): ByteArray? { return symmetricTemplate(data, key, "AES", transformation, iv, false) } /** * Return the bytes of symmetric encryption or decryption. * * @param data The data. * @param key The key. * @param algorithm The name of algorithm. * @param transformation The name of the transformation, * 加密算法/加密模式/填充类型, DES/CBC/PKCS5Padding. * @param iv The buffer with the IV. The contents of the * buffer are copied to protect against subsequent modification. * @param isEncrypt True to encrypt, false otherwise. * @return the bytes of symmetric encryption or decryption */ @Suppress("SameParameterValue") @Throws(Exception::class) private fun symmetricTemplate( data: ByteArray?, key: ByteArray?, algorithm: String, transformation: String, iv: ByteArray?, isEncrypt: Boolean ): ByteArray? { return if (data == null || data.isEmpty() || key == null || key.isEmpty()) null else { val keySpec = SecretKeySpec(key, algorithm) val cipher = Cipher.getInstance(transformation) val mode = if (isEncrypt) Cipher.ENCRYPT_MODE else Cipher.DECRYPT_MODE if (iv == null || iv.isEmpty()) { cipher.init(mode, keySpec) } else { val params: AlgorithmParameterSpec = IvParameterSpec(iv) cipher.init(mode, keySpec, params) } cipher.doFinal(data) } } //////////DES Start /** * Return the Base64-encode bytes of DES encryption. * * @param data The data. * @param key The key. * @param transformation The name of the transformation, * 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*. * @param iv The buffer with the IV. The contents of the * buffer are copied to protect against subsequent modification. * @return the Base64-encode bytes of AES encryption */ @Throws(Exception::class) fun encryptDES2Base64( data: ByteArray?, key: ByteArray?, transformation: String? = "DES/ECB/PKCS5Padding", iv: ByteArray? = null ): ByteArray? { return Base64.encode(encryptDES(data, key, transformation, iv), Base64.NO_WRAP) } /** * Return the bytes of DES encryption. * * @param data The data. * @param key The key. * @param transformation The name of the transformation, * 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*. * @param iv The buffer with the IV. The contents of the * buffer are copied to protect against subsequent modification. * @return the bytes of AES encryption */ @Throws(Exception::class) fun encryptDES( data: ByteArray?, key: ByteArray?, transformation: String? = "DES/ECB/PKCS5Padding", iv: ByteArray? = null ): ByteArray? { return symmetricTemplate(data, key, "DES", transformation!!, iv, true) } /** * Return the bytes of DES decryption for Base64-encode bytes. * * @param data The data. * @param key The key. * @param transformation The name of the transformation, * 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*. * @param iv The buffer with the IV. The contents of the * buffer are copied to protect against subsequent modification. * @return the bytes of AES decryption for Base64-encode bytes */ @Throws(Exception::class) fun decryptBase64DES( data: ByteArray?, key: ByteArray?, transformation: String = "DES/ECB/PKCS5Padding", iv: ByteArray? = null ): ByteArray? { return decryptDES(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv) } /** * Return the bytes of DES decryption. * * @param data The data. * @param key The key. * @param transformation The name of the transformation, * 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*. * @param iv The buffer with the IV. The contents of the * buffer are copied to protect against subsequent modification. * @return the bytes of AES decryption */ @Throws(Exception::class) fun decryptDES( data: ByteArray?, key: ByteArray?, transformation: String = "DES/ECB/PKCS5Padding", iv: ByteArray? = null ): ByteArray? { return symmetricTemplate(data, key, "DES", transformation, iv, false) } //////////DESede Start /** * Return the Base64-encode bytes of DESede encryption. * * @param data The data. * @param key The key. * @param transformation The name of the transformation, * 加密算法/加密模式/填充类型, *DESede/CBC/PKCS5Padding*. * @param iv The buffer with the IV. The contents of the * buffer are copied to protect against subsequent modification. * @return the Base64-encode bytes of AES encryption */ @Throws(Exception::class) fun encryptDESede2Base64( data: ByteArray?, key: ByteArray?, transformation: String? = "DESede/ECB/PKCS5Padding", iv: ByteArray? = null ): ByteArray? { return Base64.encode(encryptDESede(data, key, transformation, iv), Base64.NO_WRAP) } /** * Return the bytes of DESede encryption. * * @param data The data. * @param key The key. * @param transformation The name of the transformation, * 加密算法/加密模式/填充类型, *DESede/CBC/PKCS5Padding*. * @param iv The buffer with the IV. The contents of the * buffer are copied to protect against subsequent modification. * @return the bytes of AES encryption */ @Throws(Exception::class) fun encryptDESede( data: ByteArray?, key: ByteArray?, transformation: String? = "DESede/ECB/PKCS5Padding", iv: ByteArray? = null ): ByteArray? { return symmetricTemplate(data, key, "DESede", transformation!!, iv, true) } /** * Return the bytes of DESede decryption for Base64-encode bytes. * * @param data The data. * @param key The key. * @param transformation The name of the transformation, * 加密算法/加密模式/填充类型, *DESede/CBC/PKCS5Padding*. * @param iv The buffer with the IV. The contents of the * buffer are copied to protect against subsequent modification. * @return the bytes of AES decryption for Base64-encode bytes */ @Throws(Exception::class) fun decryptBase64DESede( data: ByteArray?, key: ByteArray?, transformation: String = "DESede/ECB/PKCS5Padding", iv: ByteArray? = null ): ByteArray? { return decryptDESede(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv) } /** * Return the bytes of DESede decryption. * * @param data The data. * @param key The key. * @param transformation The name of the transformation, * 加密算法/加密模式/填充类型, *DESede/CBC/PKCS5Padding*. * @param iv The buffer with the IV. The contents of the * buffer are copied to protect against subsequent modification. * @return the bytes of AES decryption */ @Throws(Exception::class) fun decryptDESede( data: ByteArray?, key: ByteArray?, transformation: String = "DESede/ECB/PKCS5Padding", iv: ByteArray? = null ): ByteArray? { return symmetricTemplate(data, key, "DESede", transformation, iv, false) } } ================================================ FILE: src/main/java/io/legado/app/utils/EncodingDetect.kt ================================================ package io.legado.app.utils import io.legado.app.lib.icu4j.CharsetDetector import org.jsoup.Jsoup import java.io.File import java.io.FileInputStream import java.nio.charset.StandardCharsets import java.util.* /** * 自动获取文件的编码 * */ @Suppress("MemberVisibilityCanBePrivate", "unused") object EncodingDetect { fun getHtmlEncode(bytes: ByteArray): String? { try { val doc = Jsoup.parse(String(bytes, StandardCharsets.UTF_8)) val metaTags = doc.getElementsByTag("meta") var charsetStr: String for (metaTag in metaTags) { charsetStr = metaTag.attr("charset") if (!charsetStr.isEmpty()) { return charsetStr } val content = metaTag.attr("content") val httpEquiv = metaTag.attr("http-equiv") if (httpEquiv.lowercase(Locale.getDefault()) == "content-type") { charsetStr = if (content.lowercase(Locale.getDefault()).contains("charset")) { content.substring( content.lowercase(Locale.getDefault()) .indexOf("charset") + "charset=".length ) } else { content.substring(content.lowercase(Locale.getDefault()).indexOf(";") + 1) } if (!charsetStr.isEmpty()) { return charsetStr } } } } catch (ignored: Exception) { } return getEncode(bytes) } fun getEncode(bytes: ByteArray): String { val match = CharsetDetector().setText(bytes).detect() return match?.name ?: "UTF-8" } /** * 得到文件的编码 */ fun getEncode(filePath: String): String { return getEncode(File(filePath)) } /** * 得到文件的编码 */ fun getEncode(file: File): String { val tempByte = getFileBytes(file) return getEncode(tempByte) } private fun getFileBytes(file: File?): ByteArray { val byteArray = ByteArray(8000) try { FileInputStream(file).use { it.read(byteArray) } } catch (e: Exception) { System.err.println("Error: $e") } return byteArray } } ================================================ FILE: src/main/java/io/legado/app/utils/FileExtensions.kt ================================================ package io.legado.app.utils import java.io.File fun File.getFile(vararg subDirFiles: String): File { val path = FileUtils.getPath(this, *subDirFiles) return File(path) } fun File.exists(vararg subDirFiles: String): Boolean { return getFile(*subDirFiles).exists() } ================================================ FILE: src/main/java/io/legado/app/utils/FilesUtil.kt ================================================ package io.legado.app.utils import java.io.* import java.nio.charset.Charset import java.text.SimpleDateFormat import java.util.* import java.util.regex.Pattern import java.text.DecimalFormat object FileUtils { const val GB: Long = 1073741824 const val MB: Long = 1048576 const val KB: Long = 1024 fun exists(root: File, vararg subDirFiles: String): Boolean { return getFile(root, *subDirFiles).exists() } fun createFileIfNotExist(root: File, vararg subDirFiles: String): File { val filePath = getPath(root, *subDirFiles) return createFileIfNotExist(filePath) } fun createFolderIfNotExist(root: File, vararg subDirs: String): File { val filePath = getPath(root, *subDirs) return createFolderIfNotExist(filePath) } fun createFolderIfNotExist(filePath: String): File { val file = File(filePath) //如果文件夹不存在,就创建它 if (!file.exists()) { file.mkdirs() } return file } @Synchronized fun createFileIfNotExist(filePath: String): File { val file = File(filePath) try { if (!file.exists()) { //创建父类文件夹 file.parent?.let { createFolderIfNotExist(it) } //创建文件 file.createNewFile() } } catch (e: IOException) { e.printStackTrace() } return file } fun createFileWithReplace(filePath: String): File { val file = File(filePath) if (!file.exists()) { //创建父类文件夹 file.parent?.let { createFolderIfNotExist(it) } //创建文件 file.createNewFile() } else { file.delete() file.createNewFile() } return file } fun getFile(root: File, vararg subDirFiles: String): File { val filePath = getPath(root, *subDirFiles) return File(filePath) } fun getPath(root: File, vararg subDirFiles: String): String { val path = StringBuilder(root.absolutePath) subDirFiles.forEach { if (it.isNotEmpty()) { path.append(File.separator).append(it) } } return path.toString() } //递归删除文件夹下的数据 @Synchronized fun deleteFile(filePath: String) { val file = File(filePath) if (!file.exists()) return if (file.isDirectory) { val files = file.listFiles() files?.forEach { subFile -> val path = subFile.path deleteFile(path) } } //删除文件 file.delete() } fun getCachePath(): String { // TODO throw Exception("Not implemented") return "" } const val BY_NAME_ASC = 0 const val BY_NAME_DESC = 1 const val BY_TIME_ASC = 2 const val BY_TIME_DESC = 3 const val BY_SIZE_ASC = 4 const val BY_SIZE_DESC = 5 const val BY_EXTENSION_ASC = 6 const val BY_EXTENSION_DESC = 7 @kotlin.annotation.Retention(AnnotationRetention.SOURCE) annotation class SortType /** * 将目录分隔符统一为平台默认的分隔符,并为目录结尾添加分隔符 */ fun separator(path: String): String { var path1 = path val separator = File.separator path1 = path1.replace("\\", separator) if (!path1.endsWith(separator)) { path1 += separator } return path1 } fun closeSilently(c: Closeable?) { if (c == null) { return } try { c.close() } catch (ignored: IOException) { } } /** * 列出指定目录下的所有子目录 */ @JvmOverloads fun listDirs( startDirPath: String, excludeDirs: Array? = null, @SortType sortType: Int = BY_NAME_ASC ): Array { var excludeDirs1 = excludeDirs val dirList = ArrayList() val startDir = File(startDirPath) if (!startDir.isDirectory) { return arrayOf() } val dirs = startDir.listFiles(FileFilter { f -> if (f == null) { return@FileFilter false } f.isDirectory }) ?: return arrayOf() if (excludeDirs1 == null) { excludeDirs1 = arrayOf() } for (dir in dirs) { val file = dir.absoluteFile if (!excludeDirs1.contentDeepToString().contains(file.name)) { dirList.add(file) } } when (sortType) { BY_NAME_ASC -> Collections.sort(dirList, SortByName()) BY_NAME_DESC -> { Collections.sort(dirList, SortByName()) dirList.reverse() } BY_TIME_ASC -> Collections.sort(dirList, SortByTime()) BY_TIME_DESC -> { Collections.sort(dirList, SortByTime()) dirList.reverse() } BY_SIZE_ASC -> Collections.sort(dirList, SortBySize()) BY_SIZE_DESC -> { Collections.sort(dirList, SortBySize()) dirList.reverse() } BY_EXTENSION_ASC -> Collections.sort(dirList, SortByExtension()) BY_EXTENSION_DESC -> { Collections.sort(dirList, SortByExtension()) dirList.reverse() } } return dirList.toTypedArray() } /** * 列出指定目录下的所有子目录及所有文件 */ @JvmOverloads fun listDirsAndFiles( startDirPath: String, allowExtensions: Array? = null ): Array? { val dirs: Array? val files: Array? = if (allowExtensions == null) { listFiles(startDirPath) } else { listFiles(startDirPath, allowExtensions) } dirs = listDirs(startDirPath) if (files == null) { return null } return dirs + files } /** * 列出指定目录下的所有文件 */ @JvmOverloads fun listFiles( startDirPath: String, filterPattern: Pattern? = null, @SortType sortType: Int = BY_NAME_ASC ): Array { val fileList = ArrayList() val f = File(startDirPath) if (!f.isDirectory) { return arrayOf() } val files = f.listFiles(FileFilter { file -> if (file == null) { return@FileFilter false } if (file.isDirectory) { return@FileFilter false } filterPattern?.matcher(file.name)?.find() ?: true }) ?: return arrayOf() for (file in files) { fileList.add(file.absoluteFile) } when (sortType) { BY_NAME_ASC -> Collections.sort(fileList, SortByName()) BY_NAME_DESC -> { Collections.sort(fileList, SortByName()) fileList.reverse() } BY_TIME_ASC -> Collections.sort(fileList, SortByTime()) BY_TIME_DESC -> { Collections.sort(fileList, SortByTime()) fileList.reverse() } BY_SIZE_ASC -> Collections.sort(fileList, SortBySize()) BY_SIZE_DESC -> { Collections.sort(fileList, SortBySize()) fileList.reverse() } BY_EXTENSION_ASC -> Collections.sort(fileList, SortByExtension()) BY_EXTENSION_DESC -> { Collections.sort(fileList, SortByExtension()) fileList.reverse() } } return fileList.toTypedArray() } /** * 列出指定目录下的所有文件 */ fun listFiles(startDirPath: String, allowExtensions: Array?): Array? { val file = File(startDirPath) return file.listFiles { _, name -> //返回当前目录所有以某些扩展名结尾的文件 val extension = getExtension(name) allowExtensions?.contentDeepToString()?.contains(extension) == true || allowExtensions == null } } /** * 列出指定目录下的所有文件 */ fun listFiles(startDirPath: String, allowExtension: String?): Array? { return if (allowExtension == null) listFiles(startDirPath, allowExtension = null) else listFiles(startDirPath, arrayOf(allowExtension)) } /** * 判断文件或目录是否存在 */ fun exist(path: String): Boolean { val file = File(path) return file.exists() } /** * 删除文件或目录 */ @JvmOverloads fun delete(file: File, deleteRootDir: Boolean = false): Boolean { var result = false if (file.isFile) { //是文件 result = deleteResolveEBUSY(file) } else { //是目录 val files = file.listFiles() ?: return false if (files.isEmpty()) { result = deleteRootDir && deleteResolveEBUSY(file) } else { for (f in files) { delete(f, deleteRootDir) result = deleteResolveEBUSY(f) } } if (deleteRootDir) { result = deleteResolveEBUSY(file) } } return result } /** * bug: open failed: EBUSY (Device or resource busy) * fix: http://stackoverflow.com/questions/11539657/open-failed-ebusy-device-or-resource-busy */ private fun deleteResolveEBUSY(file: File): Boolean { // Before you delete a Directory or File: rename it! val to = File(file.absolutePath + System.currentTimeMillis()) file.renameTo(to) return to.delete() } /** * 删除文件或目录 */ @JvmOverloads fun delete(path: String, deleteRootDir: Boolean = false): Boolean { val file = File(path) return if (file.exists()) { delete(file, deleteRootDir) } else false } /** * 复制文件为另一个文件,或复制某目录下的所有文件及目录到另一个目录下 */ fun copy(src: String, tar: String): Boolean { val srcFile = File(src) return srcFile.exists() && copy(srcFile, File(tar)) } /** * 复制文件或目录 */ fun copy(src: File, tar: File): Boolean { try { if (src.isFile) { val `is` = FileInputStream(src) val op = FileOutputStream(tar) val bis = BufferedInputStream(`is`) val bos = BufferedOutputStream(op) val bt = ByteArray(1024 * 8) while (true) { val len = bis.read(bt) if (len == -1) { break } else { bos.write(bt, 0, len) } } bis.close() bos.close() } else if (src.isDirectory) { tar.mkdirs() src.listFiles()?.forEach { file -> copy(file.absoluteFile, File(tar.absoluteFile, file.name)) } } return true } catch (e: Exception) { return false } } /** * 移动文件或目录 */ fun move(src: String, tar: String): Boolean { return move(File(src), File(tar)) } /** * 移动文件或目录 */ fun move(src: File, tar: File): Boolean { return rename(src, tar) } /** * 文件重命名 */ fun rename(oldPath: String, newPath: String): Boolean { return rename(File(oldPath), File(newPath)) } /** * 文件重命名 */ fun rename(src: File, tar: File): Boolean { return src.renameTo(tar) } /** * 读取文本文件, 失败将返回空串 */ @JvmOverloads fun readText(filepath: String, charset: String = "utf-8"): String { try { val data = readBytes(filepath) if (data != null) { return String(data, Charset.forName(charset)).trim { it <= ' ' } } } catch (ignored: UnsupportedEncodingException) { } return "" } /** * 读取文件内容, 失败将返回空串 */ fun readBytes(filepath: String): ByteArray? { var fis: FileInputStream? = null try { fis = FileInputStream(filepath) val baos = ByteArrayOutputStream() val buffer = ByteArray(1024) while (true) { val len = fis.read(buffer, 0, buffer.size) if (len == -1) { break } else { baos.write(buffer, 0, len) } } val data = baos.toByteArray() baos.close() return data } catch (e: IOException) { return null } finally { closeSilently(fis) } } /** * 保存文本内容 */ @JvmOverloads fun writeText(filepath: String, content: String, charset: String = "utf-8"): Boolean { return try { writeBytes(filepath, content.toByteArray(charset(charset))) } catch (e: UnsupportedEncodingException) { false } } /** * 保存文件内容 */ fun writeBytes(filepath: String, data: ByteArray): Boolean { val file = File(filepath) var fos: FileOutputStream? = null return try { if (!file.exists()) { file.parentFile?.mkdirs() file.createNewFile() } fos = FileOutputStream(filepath) fos.write(data) true } catch (e: IOException) { false } finally { closeSilently(fos) } } /** * 保存文件内容 */ fun writeInputStream(filepath: String, data: InputStream): Boolean { val file = File(filepath) return writeInputStream(file, data) } /** * 保存文件内容 */ fun writeInputStream(file: File, data: InputStream): Boolean { var fos: FileOutputStream? = null return try { if (!file.exists()) { file.parentFile?.mkdirs() file.createNewFile() } val buffer = ByteArray(1024 * 4) fos = FileOutputStream(file) while (true) { val len = data.read(buffer, 0, buffer.size) if (len == -1) { break } else { fos.write(buffer, 0, len) } } data.close() fos.flush() true } catch (e: IOException) { false } finally { closeSilently(fos) } } /** * 追加文本内容 */ fun appendText(path: String, content: String): Boolean { val file = File(path) var writer: FileWriter? = null return try { if (!file.exists()) { file.createNewFile() } writer = FileWriter(file, true) writer.write(content) true } catch (e: IOException) { false } finally { closeSilently(writer) } } /** * 获取文件大小 */ fun getLength(path: String): Long { val file = File(path) return if (!file.isFile || !file.exists()) { 0 } else file.length() } /** * 获取文件或网址的名称(包括后缀) */ fun getName(pathOrUrl: String?): String { if (pathOrUrl == null) { return "" } val pos = pathOrUrl.lastIndexOf('/') return if (0 <= pos) { pathOrUrl.substring(pos + 1) } else { System.currentTimeMillis().toString() + "." + getExtension(pathOrUrl) } } /** * 获取文件名(不包括扩展名) */ fun getNameExcludeExtension(path: String): String { return try { var fileName = File(path).name val lastIndexOf = fileName.lastIndexOf(".") if (lastIndexOf != -1) { fileName = fileName.substring(0, lastIndexOf) } fileName } catch (e: Exception) { "" } } /** * 获取格式化后的文件大小 */ fun getSize(path: String): String { val fileSize = getLength(path) return toFileSizeString(fileSize) } fun toFileSizeString(fileSize: Long): String { val df = DecimalFormat("0.00") val fileSizeString: String fileSizeString = when { fileSize < KB -> fileSize.toString() + "B" fileSize < MB -> df.format(fileSize.toDouble() / KB) + "K" fileSize < GB -> df.format(fileSize.toDouble() / MB) + "M" else -> df.format(fileSize.toDouble() / GB) + "G" } return fileSizeString } /** * 获取文件后缀,不包括“.” */ fun getExtension(pathOrUrl: String): String { val dotPos = pathOrUrl.lastIndexOf('.') return if (0 <= dotPos) { pathOrUrl.substring(dotPos + 1) } else { "ext" } } /** * 获取文件的MIME类型 */ fun getMimeType(pathOrUrl: String): String { // val ext = getExtension(pathOrUrl) // val map = MimeTypeMap.getSingleton() // return map.getMimeTypeFromExtension(ext) ?: "*/*" // throw Exception("Not implemented") } /** * 获取格式化后的文件/目录创建或最后修改时间 */ @JvmOverloads fun getDateTime(path: String, format: String = "yyyy年MM月dd日HH:mm"): String { val file = File(path) return getDateTime(file, format) } /** * 获取格式化后的文件/目录创建或最后修改时间 */ fun getDateTime(file: File, format: String): String { val cal = Calendar.getInstance() cal.timeInMillis = file.lastModified() return SimpleDateFormat(format, Locale.PRC).format(cal.time) } /** * 比较两个文件的最后修改时间 */ fun compareLastModified(path1: String, path2: String): Int { val stamp1 = File(path1).lastModified() val stamp2 = File(path2).lastModified() return when { stamp1 > stamp2 -> { 1 } stamp1 < stamp2 -> { -1 } else -> { 0 } } } /** * 创建多级别的目录 */ fun makeDirs(path: String): Boolean { return makeDirs(File(path)) } /** * 创建多级别的目录 */ fun makeDirs(file: File): Boolean { return file.mkdirs() } class SortByExtension : Comparator { override fun compare(f1: File?, f2: File?): Int { return if (f1 == null || f2 == null) { if (f1 == null) { -1 } else { 1 } } else { if (f1.isDirectory && f2.isFile) { -1 } else if (f1.isFile && f2.isDirectory) { 1 } else { f1.name.compareTo(f2.name, ignoreCase = true) } } } } class SortByName : Comparator { private var caseSensitive: Boolean = false constructor(caseSensitive: Boolean) { this.caseSensitive = caseSensitive } constructor() { this.caseSensitive = false } override fun compare(f1: File?, f2: File?): Int { if (f1 == null || f2 == null) { return if (f1 == null) { -1 } else { 1 } } else { return if (f1.isDirectory && f2.isFile) { -1 } else if (f1.isFile && f2.isDirectory) { 1 } else { val s1 = f1.name val s2 = f2.name if (caseSensitive) { s1.compareTo(s2, ignoreCase = false) } else { s1.compareTo(s2, ignoreCase = true) } } } } } class SortBySize : Comparator { override fun compare(f1: File?, f2: File?): Int { return if (f1 == null || f2 == null) { if (f1 == null) { -1 } else { 1 } } else { if (f1.isDirectory && f2.isFile) { -1 } else if (f1.isFile && f2.isDirectory) { 1 } else { if (f1.length() < f2.length()) { -1 } else { 1 } } } } } class SortByTime : Comparator { override fun compare(f1: File?, f2: File?): Int { return if (f1 == null || f2 == null) { if (f1 == null) { -1 } else { 1 } } else { if (f1.isDirectory && f2.isFile) { -1 } else if (f1.isFile && f2.isDirectory) { 1 } else { if (f1.lastModified() > f2.lastModified()) { -1 } else { 1 } } } } } } ================================================ FILE: src/main/java/io/legado/app/utils/GsonExtensions.kt ================================================ package io.legado.app.utils import com.google.gson.* import com.google.gson.internal.LinkedTreeMap import com.google.gson.reflect.TypeToken import com.google.gson.stream.JsonWriter import java.io.InputStream import java.io.InputStreamReader import java.io.OutputStream import java.io.OutputStreamWriter import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import kotlin.math.ceil val GSON: Gson by lazy { GsonBuilder() .registerTypeAdapter( object : TypeToken?>() {}.type, MapDeserializerDoubleAsIntFix() ) .registerTypeAdapter(Int::class.java, IntJsonDeserializer()) .disableHtmlEscaping() .setPrettyPrinting() .create() } inline fun genericType(): Type = object : TypeToken() {}.type inline fun Gson.fromJsonObject(json: String?): Result { return kotlin.runCatching { fromJson(json, genericType()) as? T } } inline fun Gson.fromJsonArray(json: String?): Result?> { return kotlin.runCatching { fromJson(json, ParameterizedTypeImpl(T::class.java)) as? List } } inline fun Gson.fromJsonObject(inputStream: InputStream?): Result { return kotlin.runCatching { val reader = InputStreamReader(inputStream) fromJson(reader, genericType()) as? T } } inline fun Gson.fromJsonArray(inputStream: InputStream?): Result?> { return kotlin.runCatching { val reader = InputStreamReader(inputStream) fromJson(reader, ParameterizedTypeImpl(T::class.java)) as? List } } fun Gson.writeToOutputStream(out: OutputStream, any: Any) { val writer = JsonWriter(OutputStreamWriter(out, "UTF-8")) writer.setIndent(" ") if (any is List<*>) { writer.beginArray() any.forEach { it?.let { toJson(it, it::class.java, writer) } } writer.endArray() } else { toJson(any, any::class.java, writer) } writer.close() } class ParameterizedTypeImpl(private val clazz: Class<*>) : ParameterizedType { override fun getRawType(): Type = List::class.java override fun getOwnerType(): Type? = null override fun getActualTypeArguments(): Array = arrayOf(clazz) } /** * int类型转化失败时跳过 */ class IntJsonDeserializer : JsonDeserializer { override fun deserialize( json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext? ): Int? { return when { json.isJsonPrimitive -> { val prim = json.asJsonPrimitive if (prim.isNumber) { prim.asNumber.toInt() } else { null } } else -> null } } } /** * 修复Int变为Double的问题 */ class MapDeserializerDoubleAsIntFix : JsonDeserializer?> { @Throws(JsonParseException::class) override fun deserialize( jsonElement: JsonElement, type: Type, jsonDeserializationContext: JsonDeserializationContext ): Map? { @Suppress("unchecked_cast") return read(jsonElement) as? Map } fun read(json: JsonElement): Any? { when { json.isJsonArray -> { val list: MutableList = ArrayList() val arr = json.asJsonArray for (anArr in arr) { list.add(read(anArr)) } return list } json.isJsonObject -> { val map: MutableMap = LinkedTreeMap() val obj = json.asJsonObject val entitySet = obj.entrySet() for ((key, value) in entitySet) { map[key] = read(value) } return map } json.isJsonPrimitive -> { val prim = json.asJsonPrimitive when { prim.isBoolean -> { return prim.asBoolean } prim.isString -> { return prim.asString } prim.isNumber -> { val num: Number = prim.asNumber // here you can handle double int/long values // and return any type you want // this solution will transform 3.0 float to long values return if (ceil(num.toDouble()) == num.toLong().toDouble()) { num.toLong() } else { num.toDouble() } } } } } return null } } ================================================ FILE: src/main/java/io/legado/app/utils/HtmlFormatter.kt ================================================ package io.legado.app.utils import io.legado.app.model.analyzeRule.AnalyzeUrl import java.net.URL import java.util.regex.Pattern object HtmlFormatter { private val wrapHtmlRegex = "]*>".toRegex() private val commentRegex = "".toRegex() //注释 private val notImgHtmlRegex = "])[^<>]*>".toRegex() private val otherHtmlRegex = "])[^<>]*>".toRegex() private val formatImagePattern = Pattern.compile( "]*src *= *\"([^\"{]*\\{(?:[^{}]|\\{[^}]+\\})+\\})\"[^>]*>|]*data-[^=]*= *\"([^\"]*)\"[^>]*>|]*src *= *\"([^\"]*)\"[^>]*>", Pattern.CASE_INSENSITIVE ) fun format(html: String?, otherRegex: Regex = otherHtmlRegex): String { html ?: return "" return html.replace(wrapHtmlRegex, "\n") .replace(commentRegex, "") .replace(otherRegex, "") .replace("\\s*\\n+\\s*".toRegex(), "\n  ") .replace("^[\\n\\s]+".toRegex(), "  ") .replace("[\\n\\s]+$".toRegex(), "") } fun formatKeepImg(html: String?, redirectUrl: URL? = null): String { html ?: return "" val keepImgHtml = format(html, notImgHtmlRegex) //正则的“|”处于顶端而不处于()中时,具有类似||的熔断效果,故以此机制简化原来的代码 val matcher = formatImagePattern.matcher(keepImgHtml) var appendPos = 0 val sb = StringBuffer() while (matcher.find()) { var param = "" sb.append( keepImgHtml.substring(appendPos, matcher.start()), "" ) appendPos = matcher.end() } if (appendPos < keepImgHtml.length) sb.append( keepImgHtml.substring( appendPos, keepImgHtml.length ) ) return sb.toString() } } ================================================ FILE: src/main/java/io/legado/app/utils/JsonExtensions.kt ================================================ package io.legado.app.utils import com.jayway.jsonpath.* val jsonPath: ParseContext by lazy { JsonPath.using( Configuration.builder() .options(Option.SUPPRESS_EXCEPTIONS) .build() ) } fun ReadContext.readString(path: String): String? = this.read(path, String::class.java) fun ReadContext.readBool(path: String): Boolean? = this.read(path, Boolean::class.java) fun ReadContext.readInt(path: String): Int? = this.read(path, Int::class.java) fun ReadContext.readLong(path: String): Long? = this.read(path, Long::class.java) ================================================ FILE: src/main/java/io/legado/app/utils/JsoupExtensions.kt ================================================ package io.legado.app.utils import org.jsoup.internal.StringUtil import org.jsoup.nodes.CDataNode import org.jsoup.nodes.Element import org.jsoup.nodes.Node import org.jsoup.nodes.TextNode import org.jsoup.select.NodeTraversor import org.jsoup.select.NodeVisitor fun Element.textArray(): Array { val sb = StringUtil.borrowBuilder() NodeTraversor.traverse(object : NodeVisitor { override fun head(node: Node, depth: Int) { if (node is TextNode) { appendNormalisedText(sb, node) } else if (node is Element) { if (sb.isNotEmpty() && (node.isBlock || node.tag().name == "br") && !lastCharIsWhitespace(sb) ) sb.append("\n") } } override fun tail(node: Node, depth: Int) { if (node is Element) { if (node.isBlock && node.nextSibling() is TextNode && !lastCharIsWhitespace(sb) ) { sb.append("\n") } } } }, this) val text = StringUtil.releaseBuilder(sb).trim { it <= ' ' } return text.splitNotBlank("\n") } private fun appendNormalisedText(sb: StringBuilder, textNode: TextNode) { val text = textNode.wholeText if (preserveWhitespace(textNode.parentNode()) || textNode is CDataNode) sb.append(text) else StringUtil.appendNormalisedWhitespace(sb, text, lastCharIsWhitespace(sb)) } private fun preserveWhitespace(node: Node?): Boolean { if (node is Element) { var el = node as Element? var i = 0 do { if (el!!.tag().preserveWhitespace()) return true el = el.parent() i++ } while (i < 6 && el != null) } return false } private fun lastCharIsWhitespace(sb: java.lang.StringBuilder): Boolean { return sb.isNotEmpty() && sb[sb.length - 1] == ' ' } ================================================ FILE: src/main/java/io/legado/app/utils/LogUtils.kt ================================================ @file:Suppress("unused") package io.legado.app.utils fun Throwable.printOnDebug() { printStackTrace() } ================================================ FILE: src/main/java/io/legado/app/utils/MD5Utils.kt ================================================ package io.legado.app.utils import java.security.MessageDigest import java.security.NoSuchAlgorithmException /** * 将字符串转化为MD5 */ object MD5Utils { fun md5Encode(str: String?): String { if (str == null) return "" var reStr = "" try { val md5:MessageDigest = MessageDigest.getInstance("MD5") val bytes:ByteArray = md5.digest(str.toByteArray()) val stringBuffer:StringBuilder = StringBuilder() for (b in bytes) { val bt:Int = b.toInt() and 0xff if (bt < 16) { stringBuffer.append(0) } stringBuffer.append(Integer.toHexString(bt)) } reStr = stringBuffer.toString() } catch (e: NoSuchAlgorithmException) { e.printStackTrace() } return reStr } fun md5Encode16(str: String): String { var reStr = md5Encode(str) reStr = reStr.substring(8, 24) return reStr } } ================================================ FILE: src/main/java/io/legado/app/utils/NetworkUtils.kt ================================================ package io.legado.app.utils import retrofit2.Response import java.net.InetAddress import java.net.NetworkInterface import java.net.SocketException import java.net.URL import java.util.* import java.util.regex.Pattern object NetworkUtils { fun getUrl(response: Response<*>): String { val networkResponse = response.raw().networkResponse return networkResponse?.request?.url?.toString() ?: response.raw().request.url.toString() } private val notNeedEncoding: BitSet by lazy { val bitSet = BitSet(256) for (i in 'a'.code..'z'.code) { bitSet.set(i) } for (i in 'A'.code..'Z'.code) { bitSet.set(i) } for (i in '0'.code..'9'.code) { bitSet.set(i) } for (char in "+-_.$:()!*@&#,[]") { bitSet.set(char.code) } return@lazy bitSet } /** * 支持JAVA的URLEncoder.encode出来的string做判断。 即: 将' '转成'+' * 0-9a-zA-Z保留

* ! * ' ( ) ; : @ & = + $ , / ? # [ ] 保留 * 其他字符转成%XX的格式,X是16进制的大写字符,范围是[0-9A-F] */ fun hasUrlEncoded(str: String): Boolean { var needEncode = false var i = 0 while (i < str.length) { val c = str[i] if (notNeedEncoding.get(c.code)) { i++ continue } if (c == '%' && i + 2 < str.length) { // 判断是否符合urlEncode规范 val c1 = str[++i] val c2 = str[++i] if (isDigit16Char(c1) && isDigit16Char(c2)) { i++ continue } } // 其他字符,肯定需要urlEncode needEncode = true break } return !needEncode } /** * 判断c是否是16进制的字符 */ private fun isDigit16Char(c: Char): Boolean { return c in '0'..'9' || c in 'A'..'F' || c in 'a'..'f' } /** * 获取绝对地址 */ fun getAbsoluteURL(baseURL: String?, relativePath: String): String { if (baseURL.isNullOrEmpty()) return relativePath if (relativePath.isNullOrEmpty()) return baseURL var relativeUrl = relativePath try { val absoluteUrl = URL(baseURL.substringBefore(",")) val parseUrl = URL(absoluteUrl, relativePath) relativeUrl = parseUrl.toString() return relativeUrl } catch (e: Exception) { e.printStackTrace() } return relativeUrl } /** * 获取绝对地址 */ fun getAbsoluteURL(baseURL: URL?, relativePath: String): String { if (baseURL == null) return relativePath var relativeUrl = relativePath try { val parseUrl = URL(baseURL, relativePath) relativeUrl = parseUrl.toString() return relativeUrl } catch (e: Exception) { e.printStackTrace() } return relativeUrl } fun getBaseUrl(url: String?): String? { if (url == null || !url.startsWith("http")) return null val index = url.indexOf("/", 9) return if (index == -1) { url } else url.substring(0, index) } fun getSubDomain(url: String?): String { val baseUrl = getBaseUrl(url) ?: return "" return if (baseUrl.indexOf(".") == baseUrl.lastIndexOf(".")) { baseUrl.substring(baseUrl.lastIndexOf("/") + 1) } else baseUrl.substring(baseUrl.indexOf(".") + 1) } /** * Get local Ip address. */ fun getLocalIPAddress(): InetAddress? { var enumeration: Enumeration? = null try { enumeration = NetworkInterface.getNetworkInterfaces() } catch (e: SocketException) { e.printStackTrace() } if (enumeration != null) { while (enumeration.hasMoreElements()) { val nif = enumeration.nextElement() val addresses = nif.inetAddresses if (addresses != null) { while (addresses.hasMoreElements()) { val address = addresses.nextElement() if (!address.isLoopbackAddress && isIPv4Address(address.hostAddress)) { return address } } } } } return null } /** * Check if valid IPV4 address. * * @param input the address string to check for validity. * @return True if the input parameter is a valid IPv4 address. */ fun isIPv4Address(input: String): Boolean { return IPV4_PATTERN.matcher(input).matches() } /** * Ipv4 address check. */ private val IPV4_PATTERN = Pattern.compile( "^(" + "([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}" + "([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$" ) } ================================================ FILE: src/main/java/io/legado/app/utils/SourceAnalyzer.kt ================================================ package io.legado.app.help import com.jayway.jsonpath.JsonPath import io.legado.app.constant.AppConst import io.legado.app.constant.BookType import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.rule.* import io.legado.app.exception.NoStackTraceException import io.legado.app.utils.* import io.legado.app.model.Debug import java.io.InputStream import java.util.regex.Pattern @Suppress("RegExpRedundantEscape") object SourceAnalyzer { private val headerPattern = Pattern.compile("@Header:\\{.+?\\}", Pattern.CASE_INSENSITIVE) private val jsPattern = Pattern.compile("\\{\\{.+?\\}\\}", Pattern.CASE_INSENSITIVE) fun jsonToBookSources(json: String): Result> { return kotlin.runCatching { val bookSources = mutableListOf() when { json.isJsonArray() -> { val items: List> = jsonPath.parse(json).read("$") for (item in items) { val jsonItem = jsonPath.parse(item) jsonToBookSource(jsonItem.jsonString()).getOrThrow().let { bookSources.add(it) } } } json.isJsonObject() -> { jsonToBookSource(json).getOrThrow().let { bookSources.add(it) } } else -> { throw NoStackTraceException("格式不对") } } bookSources } } fun jsonToBookSources(inputStream: InputStream): Result> { return kotlin.runCatching { val bookSources = mutableListOf() kotlin.runCatching { val items: List> = jsonPath.parse(inputStream).read("$") for (item in items) { val jsonItem = jsonPath.parse(item) jsonToBookSource(jsonItem.jsonString()).getOrThrow().let { bookSources.add(it) } } }.onFailure { val item: Map = jsonPath.parse(inputStream).read("$") val jsonItem = jsonPath.parse(item) jsonToBookSource(jsonItem.jsonString()).getOrThrow().let { bookSources.add(it) } } bookSources } } fun jsonToBookSource(json: String): Result { val source = BookSource() val sourceAny = GSON.fromJsonObject(json.trim()) .onFailure { Debug.log("转化书源出错", it.localizedMessage) }.getOrNull() return kotlin.runCatching { if (sourceAny?.ruleToc == null) { source.apply { val jsonItem = jsonPath.parse(json.trim()) bookSourceUrl = jsonItem.readString("bookSourceUrl") ?: throw NoStackTraceException("格式不对") bookSourceName = jsonItem.readString("bookSourceName") ?: "" bookSourceGroup = jsonItem.readString("bookSourceGroup") // loginUrl = jsonItem.readString("loginUrl") // loginUi = jsonItem.readString("loginUi") // loginCheckJs = jsonItem.readString("loginCheckJs") bookSourceComment = jsonItem.readString("bookSourceComment") ?: "" bookUrlPattern = jsonItem.readString("ruleBookUrlPattern") customOrder = jsonItem.readInt("serialNumber") ?: 0 header = uaToHeader(jsonItem.readString("httpUserAgent")) searchUrl = toNewUrl(jsonItem.readString("ruleSearchUrl")) exploreUrl = toNewUrls(jsonItem.readString("ruleFindUrl")) val sourceType = jsonItem.readString("bookSourceType") bookSourceType = when(sourceType) { "AUDIO" -> BookType.audio "audio" -> BookType.audio "1" -> BookType.audio "IMAGE" -> BookType.image "image" -> BookType.image "2" -> BookType.image "FILE" -> BookType.file "file" -> BookType.file "3" -> BookType.file else -> BookType.default } enabled = jsonItem.readBool("enable") ?: true if (exploreUrl.isNullOrBlank()) { enabledExplore = false } ruleSearch = SearchRule( bookList = toNewRule(jsonItem.readString("ruleSearchList")), name = toNewRule(jsonItem.readString("ruleSearchName")), author = toNewRule(jsonItem.readString("ruleSearchAuthor")), intro = toNewRule(jsonItem.readString("ruleSearchIntroduce")), kind = toNewRule(jsonItem.readString("ruleSearchKind")), bookUrl = toNewRule(jsonItem.readString("ruleSearchNoteUrl")), coverUrl = toNewRule(jsonItem.readString("ruleSearchCoverUrl")), lastChapter = toNewRule(jsonItem.readString("ruleSearchLastChapter")) ) ruleExplore = ExploreRule( bookList = toNewRule(jsonItem.readString("ruleFindList")), name = toNewRule(jsonItem.readString("ruleFindName")), author = toNewRule(jsonItem.readString("ruleFindAuthor")), intro = toNewRule(jsonItem.readString("ruleFindIntroduce")), kind = toNewRule(jsonItem.readString("ruleFindKind")), bookUrl = toNewRule(jsonItem.readString("ruleFindNoteUrl")), coverUrl = toNewRule(jsonItem.readString("ruleFindCoverUrl")), lastChapter = toNewRule(jsonItem.readString("ruleFindLastChapter")) ) ruleBookInfo = BookInfoRule( init = toNewRule(jsonItem.readString("ruleBookInfoInit")), name = toNewRule(jsonItem.readString("ruleBookName")), author = toNewRule(jsonItem.readString("ruleBookAuthor")), intro = toNewRule(jsonItem.readString("ruleIntroduce")), kind = toNewRule(jsonItem.readString("ruleBookKind")), coverUrl = toNewRule(jsonItem.readString("ruleCoverUrl")), lastChapter = toNewRule(jsonItem.readString("ruleBookLastChapter")), tocUrl = toNewRule(jsonItem.readString("ruleChapterUrl")) ) ruleToc = TocRule( chapterList = toNewRule(jsonItem.readString("ruleChapterList")), chapterName = toNewRule(jsonItem.readString("ruleChapterName")), chapterUrl = toNewRule(jsonItem.readString("ruleContentUrl")), nextTocUrl = toNewRule(jsonItem.readString("ruleChapterUrlNext")) ) var content = toNewRule(jsonItem.readString("ruleBookContent")) ?: "" if (content.startsWith("$") && !content.startsWith("$.")) { content = content.substring(1) } ruleContent = ContentRule( content = content, replaceRegex = toNewRule(jsonItem.readString("ruleBookContentReplace")), nextContentUrl = toNewRule(jsonItem.readString("ruleContentUrlNext")) ) } } else { source.bookSourceUrl = sourceAny.bookSourceUrl source.bookSourceName = sourceAny.bookSourceName source.bookSourceGroup = sourceAny.bookSourceGroup source.bookSourceType = sourceAny.bookSourceType source.bookUrlPattern = sourceAny.bookUrlPattern source.customOrder = sourceAny.customOrder source.enabled = sourceAny.enabled source.enabledExplore = sourceAny.enabledExplore source.concurrentRate = sourceAny.concurrentRate source.header = sourceAny.header source.loginUrl = when (sourceAny.loginUrl) { null -> null is String -> sourceAny.loginUrl.toString() else -> JsonPath.parse(sourceAny.loginUrl).readString("url") } // source.loginUi = if (sourceAny.loginUi is List<*>) { // GSON.toJson(sourceAny.loginUi) // } else { // sourceAny.loginUi?.toString() // } source.loginCheckJs = sourceAny.loginCheckJs source.bookSourceComment = sourceAny.bookSourceComment source.lastUpdateTime = sourceAny.lastUpdateTime source.respondTime = sourceAny.respondTime source.weight = sourceAny.weight source.exploreUrl = sourceAny.exploreUrl source.ruleExplore = if (sourceAny.ruleExplore is String) { GSON.fromJsonObject(sourceAny.ruleExplore.toString()) .getOrNull() } else { GSON.fromJsonObject(GSON.toJson(sourceAny.ruleExplore)) .getOrNull() } source.searchUrl = sourceAny.searchUrl source.ruleSearch = if (sourceAny.ruleSearch is String) { GSON.fromJsonObject(sourceAny.ruleSearch.toString()) .getOrNull() } else { GSON.fromJsonObject(GSON.toJson(sourceAny.ruleSearch)) .getOrNull() } source.ruleBookInfo = if (sourceAny.ruleBookInfo is String) { GSON.fromJsonObject(sourceAny.ruleBookInfo.toString()) .getOrNull() } else { GSON.fromJsonObject(GSON.toJson(sourceAny.ruleBookInfo)) .getOrNull() } source.ruleToc = if (sourceAny.ruleToc is String) { GSON.fromJsonObject(sourceAny.ruleToc.toString()) .getOrNull() } else { GSON.fromJsonObject(GSON.toJson(sourceAny.ruleToc)) .getOrNull() } source.ruleContent = if (sourceAny.ruleContent is String) { GSON.fromJsonObject(sourceAny.ruleContent.toString()) .getOrNull() } else { GSON.fromJsonObject(GSON.toJson(sourceAny.ruleContent)) .getOrNull() } } source } } data class BookSourceAny( var bookSourceName: String = "", // 名称 var bookSourceGroup: String? = null, // 分组 var bookSourceUrl: String = "", // 地址,包括 http/https var bookSourceType: Int = BookType.default, // 类型,0 文本,1 音频 var bookUrlPattern: String? = null, // 详情页url正则 var customOrder: Int = 0, // 手动排序编号 var enabled: Boolean = true, // 是否启用 var enabledExplore: Boolean = true, // 启用发现 var concurrentRate: String? = null, // 并发率 var header: String? = null, // 请求头 var loginUrl: Any? = null, // 登录规则 var loginUi: Any? = null, // 登录UI var loginCheckJs: String? = null, //登录检测js var bookSourceComment: String? = "", //书源注释 var lastUpdateTime: Long = 0, // 最后更新时间,用于排序 var respondTime: Long = 180000L, // 响应时间,用于排序 var weight: Int = 0, // 智能排序的权重 var exploreUrl: String? = null, // 发现url var ruleExplore: Any? = null, // 发现规则 var searchUrl: String? = null, // 搜索url var ruleSearch: Any? = null, // 搜索规则 var ruleBookInfo: Any? = null, // 书籍信息页规则 var ruleToc: Any? = null, // 目录页规则 var ruleContent: Any? = null // 正文页规则 ) // default规则适配 // #正则#替换内容 替换成 ##正则##替换内容 // | 替换成 || // & 替换成 && private fun toNewRule(oldRule: String?): String? { if (oldRule.isNullOrBlank()) return null var newRule = oldRule var reverse = false var allinone = false if (oldRule.startsWith("-")) { reverse = true newRule = oldRule.substring(1) } if (newRule.startsWith("+")) { allinone = true newRule = newRule.substring(1) } if (!newRule.startsWith("@CSS:", true) && !newRule.startsWith("@XPath:", true) && !newRule.startsWith("//") && !newRule.startsWith("##") && !newRule.startsWith(":") && !newRule.contains("@js:", true) && !newRule.contains("", true) ) { if (newRule.contains("#") && !newRule.contains("##")) { newRule = oldRule.replace("#", "##") } if (newRule.contains("|") && !newRule.contains("||")) { if (newRule.contains("##")) { val list = newRule.split("##") if (list[0].contains("|")) { newRule = list[0].replace("|", "||") for (i in 1 until list.size) { newRule += "##" + list[i] } } } else { newRule = newRule.replace("|", "||") } } if (newRule.contains("&") && !newRule.contains("&&") && !newRule.contains("http") && !newRule.startsWith("/") ) { newRule = newRule.replace("&", "&&") } } if (allinone) { newRule = "+$newRule" } if (reverse) { newRule = "-$newRule" } return newRule } private fun toNewUrls(oldUrls: String?): String? { if (oldUrls.isNullOrBlank()) return null if (oldUrls.startsWith("@js:") || oldUrls.startsWith("")) { return oldUrls } if (!oldUrls.contains("\n") && !oldUrls.contains("&&")) { return toNewUrl(oldUrls) } val urls = oldUrls.split("(&&|\r?\n)+".toRegex()) return urls.map { toNewUrl(it)?.replace("\n\\s*".toRegex(), "") }.joinToString("\n") } private fun toNewUrl(oldUrl: String?): String? { if (oldUrl.isNullOrBlank()) return null var url: String = oldUrl if (oldUrl.startsWith("", true)) { url = url.replace("=searchKey", "={{key}}") .replace("=searchPage", "={{page}}") return url } val map = HashMap() var mather = headerPattern.matcher(url) if (mather.find()) { val header = mather.group() url = url.replace(header, "") map["headers"] = header.substring(8) } var urlList = url.split("|") url = urlList[0] if (urlList.size > 1) { map["charset"] = urlList[1].split("=")[1] } mather = jsPattern.matcher(url) val jsList = arrayListOf() while (mather.find()) { jsList.add(mather.group()) url = url.replace(jsList.last(), "$${jsList.size - 1}") } url = url.replace("{", "<").replace("}", ">") url = url.replace("searchKey", "{{key}}") url = url.replace("".toRegex(), "{{page$1}}") .replace("searchPage([-+]1)".toRegex(), "{{page$1}}") .replace("searchPage", "{{page}}") for ((index, item) in jsList.withIndex()) { url = url.replace( "$$index", item.replace("searchKey", "key").replace("searchPage", "page") ) } urlList = url.split("@") url = urlList[0] if (urlList.size > 1) { map["method"] = "POST" map["body"] = urlList[1] } if (map.size > 0) { url += "," + GSON.toJson(map) } return url } private fun uaToHeader(ua: String?): String? { if (ua.isNullOrEmpty()) return null val map = mapOf(Pair(AppConst.UA_NAME, ua)) return GSON.toJson(map) } } ================================================ FILE: src/main/java/io/legado/app/utils/StringExtensions.kt ================================================ package io.legado.app.utils // import org.apache.commons.text.StringEscapeUtils import io.legado.app.constant.AppPattern.dataUriRegex fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim() fun String?.isAbsUrl() = if (this.isNullOrBlank()) false else this.startsWith("http://", true) || this.startsWith("https://", true) fun String?.isDataUrl() = this?.let { dataUriRegex.matches(it) } ?: false fun String?.isJson(): Boolean = this?.run { val str = this.trim() when { str.startsWith("{") && str.endsWith("}") -> true str.startsWith("[") && str.endsWith("]") -> true else -> false } } ?: false fun String?.isJsonObject(): Boolean = this?.run { val str = this.trim() str.startsWith("{") && str.endsWith("}") } ?: false fun String?.isJsonArray(): Boolean = this?.run { val str = this.trim() str.startsWith("[") && str.endsWith("]") } ?: false fun String?.isXml(): Boolean = this?.run { val str = this.trim() str.startsWith("<") && str.endsWith(">") } ?: false fun String?.isTrue(nullIsTrue: Boolean = false): Boolean { if (this.isNullOrBlank() || this == "null") { return nullIsTrue } return !this.matches("\\s*(?i)(false|no|not|0)\\s*".toRegex()) } fun String?.htmlFormat(): String = if (this.isNullOrBlank()) "" else this.replace("(?i)<(br[\\s/]*|/*p\\b.*?|/*div\\b.*?)>".toRegex(), "\n")// 替换特定标签为换行符 .replace("<[script>]*.*?>| ".toRegex(), "")// 删除script标签对和空格转义符 .replace("\\s*\\n+\\s*".toRegex(), "\n  ")// 移除空行,并增加段前缩进2个汉字 .replace("^[\\n\\s]+".toRegex(), "  ")//移除开头空行,并增加段前缩进2个汉字 .replace("[\\n\\s]+$".toRegex(), "") //移除尾部空行 fun String.splitNotBlank(vararg delimiter: String): Array = run { this.split(*delimiter).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray() } fun String.splitNotBlank(regex: Regex, limit: Int = 0): Array = run { this.split(regex, limit).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray() } fun String.startWithIgnoreCase(start: String): Boolean { return if (this.isBlank()) false else startsWith(start, true) } fun String.cnCompare(other: String): Int { // return java.text.Collator.getInstance(Locale.CHINA).compare(this, other) return this.compareTo(other) } /** * 将字符串拆分为单个字符,包含emoji */ fun String.toStringArray(): Array { var codePointIndex = 0 return try { Array(codePointCount(0, length)) { val start = codePointIndex codePointIndex = offsetByCodePoints(start, 1) substring(start, codePointIndex) } } catch (e: Exception) { split("").toTypedArray() } } ================================================ FILE: src/main/java/io/legado/app/utils/StringUtils.kt ================================================ package io.legado.app.utils import io.legado.app.utils.TextUtils import java.text.DecimalFormat import java.text.ParseException import java.text.SimpleDateFormat import java.util.* import java.util.regex.Matcher import java.util.regex.Pattern import kotlin.math.abs import kotlin.math.log10 import kotlin.math.pow object StringUtils { private val TAG = "StringUtils" private const val HOUR_OF_DAY = 24 private const val DAY_OF_YESTERDAY = 2 private const val TIME_UNIT = 60 private val ChnMap = chnMap private val chnMap: HashMap get() { val map = HashMap() var cnStr = "零一二三四五六七八九十" var c = cnStr.toCharArray() for (i in 0..10) { map[c[i]] = i } cnStr = "〇壹贰叁肆伍陆柒捌玖拾" c = cnStr.toCharArray() for (i in 0..10) { map[c[i]] = i } map['两'] = 2 map['百'] = 100 map['佰'] = 100 map['千'] = 1000 map['仟'] = 1000 map['万'] = 10000 map['亿'] = 100000000 return map } //将时间转换成日期 fun dateConvert(time: Long, pattern: String): String { val date = Date(time) val format = SimpleDateFormat(pattern) return format.format(date) } //将日期转换成昨天、今天、明天 fun dateConvert(source: String, pattern: String): String { val format = SimpleDateFormat(pattern) val calendar = Calendar.getInstance() try { val date = format.parse(source) val curTime = calendar.timeInMillis calendar.time = date //将MISC 转换成 sec val difSec = Math.abs((curTime - date.time) / 1000) val difMin = difSec / 60 val difHour = difMin / 60 val difDate = difHour / 60 val oldHour = calendar.get(Calendar.HOUR) //如果没有时间 if (oldHour == 0) { //比日期:昨天今天和明天 if (difDate == 0L) { return "今天" } else if (difDate < DAY_OF_YESTERDAY) { return "昨天" } else { val convertFormat = SimpleDateFormat("yyyy-MM-dd") return convertFormat.format(date) } } return when { difSec < TIME_UNIT -> difSec.toString() + "秒前" difMin < TIME_UNIT -> difMin.toString() + "分钟前" difHour < HOUR_OF_DAY -> difHour.toString() + "小时前" difDate < DAY_OF_YESTERDAY -> "昨天" else -> { val convertFormat = SimpleDateFormat("yyyy-MM-dd") convertFormat.format(date) } } } catch (e: ParseException) { e.printStackTrace() } return "" } /** * 单位转换 */ fun toSize(length: Long): String { if (length <= 0) return "0" val units = arrayOf("b", "kb", "M", "G", "T") //计算单位的,原理是利用lg,公式是 lg(1024^n) = nlg(1024),最后 nlg(1024)/lg(1024) = n。 val digitGroups = (log10(length.toDouble()) / log10(1024.0)).toInt() //计算原理是,size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2 return DecimalFormat("#,##0.##") .format(length / 1024.0.pow(digitGroups.toDouble())) + " " + units[digitGroups] } fun toFirstCapital(str: String): String { return str.substring(0, 1).uppercase(Locale.getDefault()) + str.substring(1) } /** * 将文本中的半角字符,转换成全角字符 */ fun halfToFull(input: String): String { val c = input.toCharArray() for (i in c.indices) { if (c[i].code == 32) //半角空格 { c[i] = 12288.toChar() continue } //根据实际情况,过滤不需要转换的符号 //if (c[i] == 46) //半角点号,不转换 // continue; if (c[i].code in 33..126) //其他符号都转换为全角 c[i] = (c[i].code + 65248).toChar() } return String(c) } /** * 字符串全角转换为半角 */ fun fullToHalf(input: String): String { val c = input.toCharArray() for (i in c.indices) { if (c[i].code == 12288) //全角空格 { c[i] = 32.toChar() continue } if (c[i].code in 65281..65374) c[i] = (c[i].code - 65248).toChar() } return String(c) } /** * 中文大写数字转数字 */ fun chineseNumToInt(chNum: String): Int { var result = 0 var tmp = 0 var billion = 0 val cn = chNum.toCharArray() // "一零二五" 形式 if (cn.size > 1 && chNum.matches("^[〇零一二三四五六七八九壹贰叁肆伍陆柒捌玖]$".toRegex())) { for (i in cn.indices) { cn[i] = (48 + ChnMap[cn[i]]!!).toChar() } return Integer.parseInt(String(cn)) } // "一千零二十五", "一千二" 形式 return kotlin.runCatching { for (i in cn.indices) { val tmpNum = ChnMap[cn[i]]!! when { tmpNum == 100000000 -> { result += tmp result *= tmpNum billion = billion * 100000000 + result result = 0 tmp = 0 } tmpNum == 10000 -> { result += tmp result *= tmpNum tmp = 0 } tmpNum >= 10 -> { if (tmp == 0) tmp = 1 result += tmpNum * tmp tmp = 0 } else -> { tmp = if (i >= 2 && i == cn.size - 1 && ChnMap[cn[i - 1]]!! > 10) tmpNum * ChnMap[cn[i - 1]]!! / 10 else tmp * 10 + tmpNum } } } result += tmp + billion result }.getOrDefault(-1) } /** * 字符串转数字 */ fun stringToInt(str: String?): Int { if (str != null) { val num = fullToHalf(str).replace("\\s+".toRegex(), "") return kotlin.runCatching { Integer.parseInt(num) }.getOrElse { chineseNumToInt(num) } } return -1 } /** * 是否包含数字 */ fun isContainNumber(company: String): Boolean { val p = Pattern.compile("[0-9]+") val m = p.matcher(company) return m.find() } /** * 是否数字 */ fun isNumeric(str: String): Boolean { val pattern = Pattern.compile("-?[0-9]+") val isNum = pattern.matcher(str) return isNum.matches() } fun wordCountFormat(wc: String?): String { if (wc == null) return "" var wordsS = "" if (isNumeric(wc)) { val words: Int = wc.toInt() if (words > 0) { wordsS = words.toString() + "字" if (words > 10000) { val df = DecimalFormat("#.#") wordsS = df.format(words * 1.0f / 10000f.toDouble()) + "万字" } } } else { wordsS = wc } return wordsS } /** * 移除字符串首尾空字符的高效方法(利用ASCII值判断,包括全角空格) */ fun trim(s: String): String { if (s.isEmpty()) return "" var start = 0 val len = s.length var end = len - 1 while (start < end && (s[start].code <= 0x20 || s[start] == ' ')) { ++start } while (start < end && (s[end].code <= 0x20 || s[end] == ' ')) { --end } if (end < len) ++end return if (start > 0 || end < len) s.substring(start, end) else s } /** * 重复字符串 */ fun repeat(str: String, n: Int): String { val stringBuilder = StringBuilder() for (i in 0 until n) { stringBuilder.append(str) } return stringBuilder.toString() } /** * 移除UTF头 */ fun removeUTFCharacters(data: String?): String? { if (data == null) return null val p = Pattern.compile("\\\\u(\\p{XDigit}{4})") val m = p.matcher(data) val buf = StringBuffer(data.length) while (m.find()) { val ch = Integer.parseInt(m.group(1)!!, 16).toChar().toString() m.appendReplacement(buf, Matcher.quoteReplacement(ch)) } m.appendTail(buf) return buf.toString() } fun formatHtml(html: String): String { return if (TextUtils.isEmpty(html)) "" else html.replace("(?i)<(br[\\s/]*|/*p.*?|/*div.*?)>".toRegex(), "\n")// 替换特定标签为换行符 .replace("<[script>]*.*?>| ".toRegex(), "")// 删除script标签对和空格转义符 .replace("\\s*\\n+\\s*".toRegex(), "\n  ")// 移除空行,并增加段前缩进2个汉字 .replace("^[\\n\\s]+".toRegex(), "  ")//移除开头空行,并增加段前缩进2个汉字 .replace("[\\n\\s]+$".toRegex(), "") //移除尾部空行 } fun byteToHexString(bytes: ByteArray?): String { if (bytes == null) return "" val sb = StringBuilder(bytes.size * 2) for (b in bytes) { val hex = 0xff and b.toInt() if (hex < 16) { sb.append('0') } sb.append(Integer.toHexString(hex)) } return sb.toString() } fun hexStringToByte(hexString: String): ByteArray { val hexStr = hexString.replace(" ", "") val len = hexStr.length val bytes = ByteArray(len / 2) var i = 0 while (i < len) { // 两位一组,表示一个字节,把这样表示的16进制字符串,还原成一个字节 bytes[i / 2] = ((Character.digit(hexString[i], 16) shl 4) + Character.digit(hexString[i + 1], 16)).toByte() i += 2 } return bytes } } ================================================ FILE: src/main/java/io/legado/app/utils/TextUtils.java ================================================ package io.legado.app.utils; import java.util.Iterator; public class TextUtils { public static boolean isEmpty(CharSequence str) { return str == null || str.length() == 0; } /** * Returns a string containing the tokens joined by delimiters. * * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string * "null" will be used as the delimiter. * @param tokens an array objects to be joined. Strings will be formed from the objects by * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If * tokens is an empty array, an empty string will be returned. */ public static String join(CharSequence delimiter, Object[] tokens) { final int length = tokens.length; if (length == 0) { return ""; } final StringBuilder sb = new StringBuilder(); sb.append(tokens[0]); for (int i = 1; i < length; i++) { sb.append(delimiter); sb.append(tokens[i]); } return sb.toString(); } /** * Returns a string containing the tokens joined by delimiters. * * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string * "null" will be used as the delimiter. * @param tokens an array objects to be joined. Strings will be formed from the objects by * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If * tokens is empty, an empty string will be returned. */ public static String join(CharSequence delimiter, Iterable tokens) { final Iterator it = tokens.iterator(); if (!it.hasNext()) { return ""; } final StringBuilder sb = new StringBuilder(); sb.append(it.next()); while (it.hasNext()) { sb.append(delimiter); sb.append(it.next()); } return sb.toString(); } } ================================================ FILE: src/main/java/io/legado/app/utils/ThrowableExtensions.kt ================================================ package io.legado.app.utils val Throwable.msg: String get() { val stackTrace = stackTraceToString() val lMsg = this.localizedMessage ?: "noErrorMsg" return when { stackTrace.isNotEmpty() -> stackTrace else -> lMsg } } ================================================ FILE: src/main/java/io/legado/app/utils/UTF8BOMFighter.kt ================================================ package io.legado.app.utils object UTF8BOMFighter { private val UTF8_BOM_BYTES = byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte()) fun removeUTF8BOM(xmlText: String): String { val bytes = xmlText.toByteArray() val containsBOM = (bytes.size > 3 && bytes[0] == UTF8_BOM_BYTES[0] && bytes[1] == UTF8_BOM_BYTES[1] && bytes[2] == UTF8_BOM_BYTES[2]) if (containsBOM) { return String(bytes, 3, bytes.size - 3) } return xmlText } fun removeUTF8BOM(bytes: ByteArray): ByteArray { val containsBOM = (bytes.size > 3 && bytes[0] == UTF8_BOM_BYTES[0] && bytes[1] == UTF8_BOM_BYTES[1] && bytes[2] == UTF8_BOM_BYTES[2]) if (containsBOM) { val copy = ByteArray(bytes.size - 3) System.arraycopy(bytes, 3, copy, 0, bytes.size - 3) return copy } return bytes } } ================================================ FILE: src/main/java/io/legado/app/utils/Utf8BomUtils.kt ================================================ package io.legado.app.utils @Suppress("unused") object Utf8BomUtils { private val UTF8_BOM_BYTES = byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte()) fun removeUTF8BOM(xmlText: String): String { val bytes = xmlText.toByteArray() val containsBOM = (bytes.size > 3 && bytes[0] == UTF8_BOM_BYTES[0] && bytes[1] == UTF8_BOM_BYTES[1] && bytes[2] == UTF8_BOM_BYTES[2]) if (containsBOM) { return String(bytes, 3, bytes.size - 3) } return xmlText } fun removeUTF8BOM(bytes: ByteArray): ByteArray { val containsBOM = (bytes.size > 3 && bytes[0] == UTF8_BOM_BYTES[0] && bytes[1] == UTF8_BOM_BYTES[1] && bytes[2] == UTF8_BOM_BYTES[2]) if (containsBOM) { val copy = ByteArray(bytes.size - 3) System.arraycopy(bytes, 3, copy, 0, bytes.size - 3) return copy } return bytes } fun hasBom(bytes: ByteArray): Boolean { return (bytes.size > 3 && bytes[0] == UTF8_BOM_BYTES[0] && bytes[1] == UTF8_BOM_BYTES[1] && bytes[2] == UTF8_BOM_BYTES[2]) } } ================================================ FILE: src/main/java/io/legado/app/utils/ZipUtils.kt ================================================ package io.legado.app.utils import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.withContext import java.io.* import java.util.* import java.util.zip.ZipEntry import java.util.zip.ZipFile import java.util.zip.ZipOutputStream import mu.KotlinLogging private val logger = KotlinLogging.logger {} @Suppress("unused", "BlockingMethodInNonBlockingContext", "MemberVisibilityCanBePrivate") object ZipUtils { /** * Zip the files. * * @param srcFiles The source of files. * @param zipFilePath The path of ZIP file. * @return `true`: success

`false`: fail * @throws IOException if an I/O error has occurred */ suspend fun zipFiles( srcFiles: Collection, zipFilePath: String ): Boolean { return zipFiles(srcFiles, zipFilePath, null) } /** * Zip the files. * * @param srcFilePaths The paths of source files. * @param zipFilePath The path of ZIP file. * @param comment The comment. * @return `true`: success

`false`: fail * @throws IOException if an I/O error has occurred */ suspend fun zipFiles( srcFilePaths: Collection?, zipFilePath: String?, comment: String? ): Boolean = withContext(IO) { if (srcFilePaths == null || zipFilePath == null) return@withContext false ZipOutputStream(FileOutputStream(zipFilePath)).use { for (srcFile in srcFilePaths) { if (!zipFile(getFileByPath(srcFile)!!, "", it, comment)) return@withContext false } return@withContext true } } /** * Zip the files. * * @param srcFiles The source of files. * @param zipFile The ZIP file. * @param comment The comment. * @return `true`: success

`false`: fail * @throws IOException if an I/O error has occurred */ @Throws(IOException::class) @JvmOverloads fun zipFiles( srcFiles: Collection?, zipFile: File?, comment: String? = null ): Boolean { if (srcFiles == null || zipFile == null) return false ZipOutputStream(FileOutputStream(zipFile)).use { for (srcFile in srcFiles) { if (!zipFile(srcFile, "", it, comment)) return false } return true } } /** * Zip the file. * * @param srcFilePath The path of source file. * @param zipFilePath The path of ZIP file. * @return `true`: success

`false`: fail * @throws IOException if an I/O error has occurred */ @Throws(IOException::class) fun zipFile( srcFilePath: String, zipFilePath: String ): Boolean { return zipFile(getFileByPath(srcFilePath), getFileByPath(zipFilePath), null) } /** * Zip the file. * * @param srcFilePath The path of source file. * @param zipFilePath The path of ZIP file. * @param comment The comment. * @return `true`: success

`false`: fail * @throws IOException if an I/O error has occurred */ @Throws(IOException::class) fun zipFile( srcFilePath: String, zipFilePath: String, comment: String ): Boolean { return zipFile(getFileByPath(srcFilePath), getFileByPath(zipFilePath), comment) } /** * Zip the file. * * @param srcFile The source of file. * @param zipFile The ZIP file. * @param comment The comment. * @return `true`: success

`false`: fail * @throws IOException if an I/O error has occurred */ @Throws(IOException::class) @JvmOverloads fun zipFile( srcFile: File?, zipFile: File?, comment: String? = null ): Boolean { if (srcFile == null || zipFile == null) return false ZipOutputStream(FileOutputStream(zipFile)).use { zos -> return zipFile(srcFile, "", zos, comment) } } @Throws(IOException::class) private fun zipFile( srcFile: File, rootPath: String, zos: ZipOutputStream, comment: String? ): Boolean { var rootPath1 = rootPath if (!srcFile.exists()) return true rootPath1 = rootPath1 + (if (isSpace(rootPath1)) "" else File.separator) + srcFile.name if (srcFile.isDirectory) { val fileList = srcFile.listFiles() if (fileList == null || fileList.isEmpty()) { val entry = ZipEntry("$rootPath1/") entry.comment = comment zos.putNextEntry(entry) zos.closeEntry() } else { for (file in fileList) { if (!zipFile(file, rootPath1, zos, comment)) return false } } } else { BufferedInputStream(FileInputStream(srcFile)).use { `is` -> val entry = ZipEntry(rootPath1) entry.comment = comment zos.putNextEntry(entry) zos.write(`is`.readBytes()) zos.closeEntry() } } return true } /** * Unzip the file. * * @param zipFilePath The path of ZIP file. * @param destDirPath The path of destination directory. * @return the unzipped files * @throws IOException if unzip unsuccessfully */ @Throws(IOException::class) fun unzipFile(zipFilePath: String, destDirPath: String): List? { return unzipFileByKeyword(zipFilePath, destDirPath, null) } /** * Unzip the file. * * @param zipFile The ZIP file. * @param destDir The destination directory. * @return the unzipped files * @throws IOException if unzip unsuccessfully */ @Throws(IOException::class) fun unzipFile( zipFile: File, destDir: File ): List? { return unzipFileByKeyword(zipFile, destDir, null) } /** * Unzip the file by keyword. * * @param zipFilePath The path of ZIP file. * @param destDirPath The path of destination directory. * @param keyword The keyboard. * @return the unzipped files * @throws IOException if unzip unsuccessfully */ @Throws(IOException::class) fun unzipFileByKeyword( zipFilePath: String, destDirPath: String, keyword: String? ): List? { return unzipFileByKeyword( getFileByPath(zipFilePath), getFileByPath(destDirPath), keyword ) } /** * Unzip the file by keyword. * * @param zipFile The ZIP file. * @param destDir The destination directory. * @param keyword The keyboard. * @return the unzipped files * @throws IOException if unzip unsuccessfully */ @Throws(IOException::class) fun unzipFileByKeyword( zipFile: File?, destDir: File?, keyword: String? ): List? { if (zipFile == null || destDir == null) return null val files = ArrayList() val zip = ZipFile(zipFile) val entries = zip.entries() zip.use { if (isSpace(keyword)) { while (entries.hasMoreElements()) { val entry = entries.nextElement() as ZipEntry val entryName = entry.name if (entryName.contains("../")) { logger.error("ZipUtils " + "entryName: $entryName is dangerous!") continue } if (!unzipChildFile(destDir, files, zip, entry, entryName)) return files } } else { while (entries.hasMoreElements()) { val entry = entries.nextElement() as ZipEntry val entryName = entry.name if (entryName.contains("../")) { logger.error("ZipUtils " + "entryName: $entryName is dangerous!") continue } if (entryName.contains(keyword!!)) { if (!unzipChildFile(destDir, files, zip, entry, entryName)) return files } } } } return files } @Throws(IOException::class) private fun unzipChildFile( destDir: File, files: MutableList, zip: ZipFile, entry: ZipEntry, name: String ): Boolean { val file = File(destDir, name) files.add(file) if (entry.isDirectory) { return createOrExistsDir(file) } else { if (!createOrExistsFile(file)) return false BufferedInputStream(zip.getInputStream(entry)).use { `in` -> BufferedOutputStream(FileOutputStream(file)).use { out -> out.write(`in`.readBytes()) } } } return true } /** * Return the files' path in ZIP file. * * @param zipFilePath The path of ZIP file. * @return the files' path in ZIP file * @throws IOException if an I/O error has occurred */ @Throws(IOException::class) fun getFilesPath(zipFilePath: String): List? { return getFilesPath(getFileByPath(zipFilePath)) } /** * Return the files' path in ZIP file. * * @param zipFile The ZIP file. * @return the files' path in ZIP file * @throws IOException if an I/O error has occurred */ @Throws(IOException::class) fun getFilesPath(zipFile: File?): List? { if (zipFile == null) return null val paths = ArrayList() val zip = ZipFile(zipFile) val entries = zip.entries() while (entries.hasMoreElements()) { val entryName = (entries.nextElement() as ZipEntry).name if (entryName.contains("../")) { logger.error("ZipUtils " + "entryName: $entryName is dangerous!") paths.add(entryName) } else { paths.add(entryName) } } zip.close() return paths } /** * Return the files' comment in ZIP file. * * @param zipFilePath The path of ZIP file. * @return the files' comment in ZIP file * @throws IOException if an I/O error has occurred */ @Throws(IOException::class) fun getComments(zipFilePath: String): List? { return getComments(getFileByPath(zipFilePath)) } /** * Return the files' comment in ZIP file. * * @param zipFile The ZIP file. * @return the files' comment in ZIP file * @throws IOException if an I/O error has occurred */ @Throws(IOException::class) fun getComments(zipFile: File?): List? { if (zipFile == null) return null val comments = ArrayList() val zip = ZipFile(zipFile) val entries = zip.entries() while (entries.hasMoreElements()) { val entry = entries.nextElement() as ZipEntry comments.add(entry.comment) } zip.close() return comments } private fun createOrExistsDir(file: File?): Boolean { return file != null && if (file.exists()) file.isDirectory else file.mkdirs() } private fun createOrExistsFile(file: File?): Boolean { if (file == null) return false if (file.exists()) return file.isFile if (!createOrExistsDir(file.parentFile)) return false return try { file.createNewFile() } catch (e: IOException) { e.printStackTrace() false } } private fun getFileByPath(filePath: String): File? { return if (isSpace(filePath)) null else File(filePath) } private fun isSpace(s: String?): Boolean { if (s == null) return true var i = 0 val len = s.length while (i < len) { if (!Character.isWhitespace(s[i])) { return false } ++i } return true } } ================================================ FILE: src/main/java/me/ag2s/epublib/Constants.java ================================================ package me.ag2s.epublib; public interface Constants { String CHARACTER_ENCODING = "UTF-8"; String DOCTYPE_XHTML = ""; String NAMESPACE_XHTML = "http://www.w3.org/1999/xhtml"; String EPUB_GENERATOR_NAME = "Ag2S EpubLib"; String EPUB_DUOKAN_NAME = "DK-SONGTI"; char FRAGMENT_SEPARATOR_CHAR = '#'; String DEFAULT_TOC_ID = "toc"; } ================================================ FILE: src/main/java/me/ag2s/epublib/browsersupport/NavigationEvent.java ================================================ package me.ag2s.epublib.browsersupport; import java.util.EventObject; import me.ag2s.epublib.domain.EpubBook; import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.util.StringUtil; /** * Used to tell NavigationEventListener just what kind of navigation action * the user just did. * * @author paul * */ @SuppressWarnings("unused") public class NavigationEvent extends EventObject { private static final long serialVersionUID = -6346750144308952762L; private Resource oldResource; private int oldSpinePos; private Navigator navigator; private EpubBook oldBook; private int oldSectionPos; private String oldFragmentId; public NavigationEvent(Object source) { super(source); } public NavigationEvent(Object source, Navigator navigator) { super(source); this.navigator = navigator; this.oldBook = navigator.getBook(); this.oldFragmentId = navigator.getCurrentFragmentId(); this.oldSectionPos = navigator.getCurrentSectionPos(); this.oldResource = navigator.getCurrentResource(); this.oldSpinePos = navigator.getCurrentSpinePos(); } /** * The previous position within the section. * * @return The previous position within the section. */ public int getOldSectionPos() { return oldSectionPos; } public Navigator getNavigator() { return navigator; } public String getOldFragmentId() { return oldFragmentId; } // package void setOldFragmentId(String oldFragmentId) { this.oldFragmentId = oldFragmentId; } public EpubBook getOldBook() { return oldBook; } // package void setOldPagePos(int oldPagePos) { this.oldSectionPos = oldPagePos; } public int getCurrentSectionPos() { return navigator.getCurrentSectionPos(); } public int getOldSpinePos() { return oldSpinePos; } public int getCurrentSpinePos() { return navigator.getCurrentSpinePos(); } public String getCurrentFragmentId() { return navigator.getCurrentFragmentId(); } public boolean isBookChanged() { if (oldBook == null) { return true; } return oldBook != navigator.getBook(); } public boolean isSpinePosChanged() { return getOldSpinePos() != getCurrentSpinePos(); } public boolean isFragmentChanged() { return StringUtil.equals(getOldFragmentId(), getCurrentFragmentId()); } public Resource getOldResource() { return oldResource; } public Resource getCurrentResource() { return navigator.getCurrentResource(); } public void setOldResource(Resource oldResource) { this.oldResource = oldResource; } public void setOldSpinePos(int oldSpinePos) { this.oldSpinePos = oldSpinePos; } public void setNavigator(Navigator navigator) { this.navigator = navigator; } public void setOldBook(EpubBook oldBook) { this.oldBook = oldBook; } public EpubBook getCurrentBook() { return getNavigator().getBook(); } public boolean isResourceChanged() { return oldResource != getCurrentResource(); } @SuppressWarnings("NullableProblems") public String toString() { return StringUtil.toString( "oldSectionPos", oldSectionPos, "oldResource", oldResource, "oldBook", oldBook, "oldFragmentId", oldFragmentId, "oldSpinePos", oldSpinePos, "currentPagePos", getCurrentSectionPos(), "currentResource", getCurrentResource(), "currentBook", getCurrentBook(), "currentFragmentId", getCurrentFragmentId(), "currentSpinePos", getCurrentSpinePos() ); } public boolean isSectionPosChanged() { return oldSectionPos != getCurrentSectionPos(); } } ================================================ FILE: src/main/java/me/ag2s/epublib/browsersupport/NavigationEventListener.java ================================================ package me.ag2s.epublib.browsersupport; /** * Implemented by classes that want to be notified if the user moves to * another location in the book. * * @author paul * */ public interface NavigationEventListener { /** * Called whenever the user navigates to another position in the book. * * @param navigationEvent f */ void navigationPerformed(NavigationEvent navigationEvent); } ================================================ FILE: src/main/java/me/ag2s/epublib/browsersupport/NavigationHistory.java ================================================ package me.ag2s.epublib.browsersupport; import java.util.ArrayList; import java.util.List; import me.ag2s.epublib.domain.EpubBook; import me.ag2s.epublib.domain.Resource; /** * A history of the user's locations with the epub. * * @author paul.siegmann */ public class NavigationHistory implements NavigationEventListener { public static final int DEFAULT_MAX_HISTORY_SIZE = 1000; private static final long DEFAULT_HISTORY_WAIT_TIME = 1000; private static class Location { private String href; public Location(String href) { super(); this.href = href; } @SuppressWarnings("unused") public void setHref(String href) { this.href = href; } public String getHref() { return href; } } private long lastUpdateTime = 0; private List locations = new ArrayList<>(); private final Navigator navigator; private int currentPos = -1; private int currentSize = 0; private int maxHistorySize = DEFAULT_MAX_HISTORY_SIZE; private long historyWaitTime = DEFAULT_HISTORY_WAIT_TIME; public NavigationHistory(Navigator navigator) { this.navigator = navigator; navigator.addNavigationEventListener(this); initBook(navigator.getBook()); } public int getCurrentPos() { return currentPos; } public int getCurrentSize() { return currentSize; } public void initBook(EpubBook book) { if (book == null) { return; } locations = new ArrayList<>(); currentPos = -1; currentSize = 0; if (navigator.getCurrentResource() != null) { addLocation(navigator.getCurrentResource().getHref()); } } /** * If the time between a navigation event is less than the historyWaitTime * then the new location is not added to the history. * * When a user is rapidly viewing many pages using the slider we do not * want all of them to be added to the history. * * @return the time we wait before adding the page to the history */ public long getHistoryWaitTime() { return historyWaitTime; } public void setHistoryWaitTime(long historyWaitTime) { this.historyWaitTime = historyWaitTime; } public void addLocation(Resource resource) { if (resource == null) { return; } addLocation(resource.getHref()); } /** * Adds the location after the current position. * If the currentposition is not the end of the list then the elements * between the current element and the end of the list will be discarded. * * Does nothing if the new location matches the current location. *
* If this nr of locations becomes larger then the historySize then the * first item(s) will be removed. *v * @param location d */ public void addLocation(Location location) { // do nothing if the new location matches the current location if (!(locations.isEmpty()) && location.getHref().equals(locations.get(currentPos).getHref())) { return; } currentPos++; if (currentPos != currentSize) { locations.set(currentPos, location); } else { locations.add(location); checkHistorySize(); } currentSize = currentPos + 1; } /** * Removes all elements that are too much for the maxHistorySize * out of the history. */ private void checkHistorySize() { while (locations.size() > maxHistorySize) { locations.remove(0); currentSize--; currentPos--; } } public void addLocation(String href) { addLocation(new Location(href)); } private String getLocationHref(int pos) { if (pos < 0 || pos >= locations.size()) { return null; } return locations.get(currentPos).getHref(); } /** * Moves the current positions delta positions. * * move(-1) to go one position back in history.
* move(1) to go one position forward.
发 * * @param delta f * * @return Whether we actually moved. If the requested value is illegal * it will return false, true otherwise. */ public boolean move(int delta) { if (((currentPos + delta) < 0) || ((currentPos + delta) >= currentSize)) { return false; } currentPos += delta; navigator.gotoResource(getLocationHref(currentPos), this); return true; } /** * If this is not the source of the navigationEvent then the addLocation * will be called with the href of the currentResource in the navigationEvent. */ @Override public void navigationPerformed(NavigationEvent navigationEvent) { if (this == navigationEvent.getSource()) { return; } if (navigationEvent.getCurrentResource() == null) { return; } if ((System.currentTimeMillis() - this.lastUpdateTime) > historyWaitTime) { // if the user scrolled rapidly through the pages then the last page // will not be added to the history. We fix that here: addLocation(navigationEvent.getOldResource()); addLocation(navigationEvent.getCurrentResource().getHref()); } lastUpdateTime = System.currentTimeMillis(); } public String getCurrentHref() { if (currentPos < 0 || currentPos >= locations.size()) { return null; } return locations.get(currentPos).getHref(); } public void setMaxHistorySize(int maxHistorySize) { this.maxHistorySize = maxHistorySize; } public int getMaxHistorySize() { return maxHistorySize; } } ================================================ FILE: src/main/java/me/ag2s/epublib/browsersupport/Navigator.java ================================================ package me.ag2s.epublib.browsersupport; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import me.ag2s.epublib.domain.EpubBook; import me.ag2s.epublib.domain.Resource; /** * A helper class for epub browser applications. *

* It helps moving from one resource to the other, from one resource * to the other and keeping other elements of the application up-to-date * by calling the NavigationEventListeners. * * @author paul */ public class Navigator implements Serializable { private static final long serialVersionUID = 1076126986424925474L; private EpubBook book; private int currentSpinePos; private Resource currentResource; private int currentPagePos; private String currentFragmentId; private final List eventListeners = new ArrayList<>(); public Navigator() { this(null); } public Navigator(EpubBook book) { this.book = book; this.currentSpinePos = 0; if (book != null) { this.currentResource = book.getCoverPage(); } this.currentPagePos = 0; } private synchronized void handleEventListeners( NavigationEvent navigationEvent) { for (int i = 0; i < eventListeners.size(); i++) { NavigationEventListener navigationEventListener = eventListeners.get(i); navigationEventListener.navigationPerformed(navigationEvent); } } public boolean addNavigationEventListener( NavigationEventListener navigationEventListener) { return this.eventListeners.add(navigationEventListener); } public boolean removeNavigationEventListener( NavigationEventListener navigationEventListener) { return this.eventListeners.remove(navigationEventListener); } public int gotoFirstSpineSection(Object source) { return gotoSpineSection(0, source); } public int gotoPreviousSpineSection(Object source) { return gotoPreviousSpineSection(0, source); } public int gotoPreviousSpineSection(int pagePos, Object source) { if (currentSpinePos < 0) { return gotoSpineSection(0, pagePos, source); } else { return gotoSpineSection(currentSpinePos - 1, pagePos, source); } } public boolean hasNextSpineSection() { return (currentSpinePos < (book.getSpine().size() - 1)); } public boolean hasPreviousSpineSection() { return (currentSpinePos > 0); } public int gotoNextSpineSection(Object source) { if (currentSpinePos < 0) { return gotoSpineSection(0, source); } else { return gotoSpineSection(currentSpinePos + 1, source); } } public int gotoResource(String resourceHref, Object source) { Resource resource = book.getResources().getByHref(resourceHref); return gotoResource(resource, source); } public int gotoResource(Resource resource, Object source) { return gotoResource(resource, 0, null, source); } public int gotoResource(Resource resource, String fragmentId, Object source) { return gotoResource(resource, 0, fragmentId, source); } public int gotoResource(Resource resource, int pagePos, Object source) { return gotoResource(resource, pagePos, null, source); } public int gotoResource(Resource resource, int pagePos, String fragmentId, Object source) { if (resource == null) { return -1; } NavigationEvent navigationEvent = new NavigationEvent(source, this); this.currentResource = resource; this.currentSpinePos = book.getSpine().getResourceIndex(currentResource); this.currentPagePos = pagePos; this.currentFragmentId = fragmentId; handleEventListeners(navigationEvent); return currentSpinePos; } public int gotoResourceId(String resourceId, Object source) { return gotoSpineSection(book.getSpine().findFirstResourceById(resourceId), source); } public int gotoSpineSection(int newSpinePos, Object source) { return gotoSpineSection(newSpinePos, 0, source); } /** * Go to a specific section. * Illegal spine positions are silently ignored. * * @param newSpinePos f * @param source f * @return The current position within the spine */ public int gotoSpineSection(int newSpinePos, int newPagePos, Object source) { if (newSpinePos == currentSpinePos) { return currentSpinePos; } if (newSpinePos < 0 || newSpinePos >= book.getSpine().size()) { return currentSpinePos; } NavigationEvent navigationEvent = new NavigationEvent(source, this); currentSpinePos = newSpinePos; currentPagePos = newPagePos; currentResource = book.getSpine().getResource(currentSpinePos); handleEventListeners(navigationEvent); return currentSpinePos; } public int gotoLastSpineSection(Object source) { return gotoSpineSection(book.getSpine().size() - 1, source); } public void gotoBook(EpubBook book, Object source) { NavigationEvent navigationEvent = new NavigationEvent(source, this); this.book = book; this.currentFragmentId = null; this.currentPagePos = 0; this.currentResource = null; this.currentSpinePos = book.getSpine().getResourceIndex(currentResource); handleEventListeners(navigationEvent); } /** * The current position within the spine. * * @return something < 0 if the current position is not within the spine. */ public int getCurrentSpinePos() { return currentSpinePos; } public Resource getCurrentResource() { return currentResource; } /** * Sets the current index and resource without calling the eventlisteners. * * If you want the eventListeners called use gotoSection(index); * * @param currentIndex f */ public void setCurrentSpinePos(int currentIndex) { this.currentSpinePos = currentIndex; this.currentResource = book.getSpine().getResource(currentIndex); } public EpubBook getBook() { return book; } /** * Sets the current index and resource without calling the eventlisteners. * * If you want the eventListeners called use gotoSection(index); * */ public int setCurrentResource(Resource currentResource) { this.currentSpinePos = book.getSpine().getResourceIndex(currentResource); this.currentResource = currentResource; return currentSpinePos; } public String getCurrentFragmentId() { return currentFragmentId; } public int getCurrentSectionPos() { return currentPagePos; } } ================================================ FILE: src/main/java/me/ag2s/epublib/browsersupport/package-info.java ================================================ /** * Provides classes that help make an epub reader application. * * These classes have no dependencies on graphic toolkits, they're purely * to help with the browsing/navigation logic. */ package me.ag2s.epublib.browsersupport; ================================================ FILE: src/main/java/me/ag2s/epublib/domain/Author.java ================================================ package me.ag2s.epublib.domain; import me.ag2s.epublib.util.StringUtil; import java.io.Serializable; /** * Represents one of the authors of the book * * @author paul */ public class Author implements Serializable { private static final long serialVersionUID = 6663408501416574200L; private String firstname; private String lastname; private Relator relator = Relator.AUTHOR; public Author(String singleName) { this("", singleName); } public Author(String firstname, String lastname) { this.firstname = firstname; this.lastname = lastname; } public String getFirstname() { return firstname; } public void setFirstname(String firstname) { this.firstname = firstname; } public String getLastname() { return lastname; } public void setLastname(String lastname) { this.lastname = lastname; } @Override @SuppressWarnings("NullableProblems") public String toString() { return this.lastname + ", " + this.firstname; } public int hashCode() { return StringUtil.hashCode(firstname, lastname); } public boolean equals(Object authorObject) { if (!(authorObject instanceof Author)) { return false; } Author other = (Author) authorObject; return StringUtil.equals(firstname, other.firstname) && StringUtil.equals(lastname, other.lastname); } /** * 设置贡献者的角色 * * @param code 角色编号 */ public void setRole(String code) { Relator result = Relator.byCode(code); if (result == null) { result = Relator.AUTHOR; } this.relator = result; } public Relator getRelator() { return relator; } public void setRelator(Relator relator) { this.relator = relator; } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/Date.java ================================================ package me.ag2s.epublib.domain; import java.io.Serializable; import java.text.SimpleDateFormat; import java.util.Locale; import me.ag2s.epublib.epub.PackageDocumentBase; /** * A Date used by the book's metadata. *

* Examples: creation-date, modification-date, etc * * @author paul */ public class Date implements Serializable { private static final long serialVersionUID = 7533866830395120136L; public enum Event { PUBLICATION("publication"), MODIFICATION("modification"), CREATION("creation"); private final String value; Event(String v) { value = v; } public static Event fromValue(String v) { for (Event c : Event.values()) { if (c.value.equals(v)) { return c; } } return null; } @Override @SuppressWarnings("NullableProblems") public String toString() { return value; } } private Event event; private String dateString; public Date() { this(new java.util.Date(), Event.CREATION); } public Date(java.util.Date date) { this(date, (Event) null); } public Date(String dateString) { this(dateString, (Event) null); } public Date(java.util.Date date, Event event) { this((new SimpleDateFormat(PackageDocumentBase.dateFormat, Locale.US)).format(date), event); } public Date(String dateString, Event event) { this.dateString = dateString; this.event = event; } public Date(java.util.Date date, String event) { this((new SimpleDateFormat(PackageDocumentBase.dateFormat, Locale.US)).format(date), event); } public Date(String dateString, String event) { this(checkDate(dateString), Event.fromValue(event)); this.dateString = dateString; } private static String checkDate(String dateString) { if (dateString == null) { throw new IllegalArgumentException( "Cannot create a date from a blank string"); } return dateString; } public String getValue() { return dateString; } public Event getEvent() { return event; } public void setEvent(Event event) { this.event = event; } @Override @SuppressWarnings("NullableProblems") public String toString() { if (event == null) { return dateString; } return "" + event + ":" + dateString; } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/EpubBook.java ================================================ package me.ag2s.epublib.domain; import java.io.Serializable; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Representation of a Book. *

* All resources of a Book (html, css, xml, fonts, images) are represented * as Resources. See getResources() for access to these.
* A Book as 3 indexes into these Resources, as per the epub specification.
*

*
Spine
*
these are the Resources to be shown when a user reads the book from * start to finish.
*
Table of Contents
*
The table of contents. Table of Contents references may be in a * different order and contain different Resources than the spine, and often do. *
Guide
*
The Guide has references to a set of special Resources like the * cover page, the Glossary, the copyright page, etc. *
*

* The complication is that these 3 indexes may and usually do point to * different pages. * A chapter may be split up in 2 pieces to fit it in to memory. Then the * spine will contain both pieces, but the Table of Contents only the first. *

* The Content page may be in the Table of Contents, the Guide, but not * in the Spine. * Etc. *

*

* Please see the illustration at: doc/schema.svg * * @author paul * @author jake */ public class EpubBook implements Serializable { private static final long serialVersionUID = 2068355170895770100L; private Resources resources = new Resources(); private Metadata metadata = new Metadata(); private Spine spine = new Spine(); private TableOfContents tableOfContents = new TableOfContents(); private final Guide guide = new Guide(); private Resource opfResource; private Resource ncxResource; private Resource coverImage; private String version = "2.0"; public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public boolean isEpub3() { return this.version.startsWith("3."); } @SuppressWarnings("UnusedReturnValue") public TOCReference addSection( TOCReference parentSection, String sectionTitle, Resource resource) { return addSection(parentSection, sectionTitle, resource, null); } /** * Adds the resource to the table of contents of the book as a child * section of the given parentSection * * @param parentSection parentSection * @param sectionTitle sectionTitle * @param resource resource * @param fragmentId fragmentId * @return The table of contents */ public TOCReference addSection( TOCReference parentSection, String sectionTitle, Resource resource, String fragmentId) { getResources().add(resource); if (spine.findFirstResourceById(resource.getId()) < 0) { spine.addSpineReference(new SpineReference(resource)); } return parentSection.addChildSection( new TOCReference(sectionTitle, resource, fragmentId)); } public TOCReference addSection(String title, Resource resource) { return addSection(title, resource, null); } /** * Adds a resource to the book's set of resources, table of contents and * if there is no resource with the id in the spine also adds it to the spine. * * @param title title * @param resource resource * @param fragmentId fragmentId * @return The table of contents */ public TOCReference addSection( String title, Resource resource, String fragmentId) { getResources().add(resource); TOCReference tocReference = tableOfContents .addTOCReference(new TOCReference(title, resource, fragmentId)); if (spine.findFirstResourceById(resource.getId()) < 0) { spine.addSpineReference(new SpineReference(resource)); } return tocReference; } @SuppressWarnings("unused") public void generateSpineFromTableOfContents() { Spine spine = new Spine(tableOfContents); // in case the tocResource was already found and assigned spine.setTocResource(this.spine.getTocResource()); this.spine = spine; } /** * The Book's metadata (titles, authors, etc) * * @return The Book's metadata (titles, authors, etc) */ public Metadata getMetadata() { return metadata; } public void setMetadata(Metadata metadata) { this.metadata = metadata; } public void setResources(Resources resources) { this.resources = resources; } @SuppressWarnings("unused") public Resource addResource(Resource resource) { return resources.add(resource); } /** * The collection of all images, chapters, sections, xhtml files, * stylesheets, etc that make up the book. * * @return The collection of all images, chapters, sections, xhtml files, * stylesheets, etc that make up the book. */ public Resources getResources() { return resources; } /** * The sections of the book that should be shown if a user reads the book * from start to finish. * * @return The Spine */ public Spine getSpine() { return spine; } public void setSpine(Spine spine) { this.spine = spine; } /** * The Table of Contents of the book. * * @return The Table of Contents of the book. */ public TableOfContents getTableOfContents() { return tableOfContents; } public void setTableOfContents(TableOfContents tableOfContents) { this.tableOfContents = tableOfContents; } /** * The book's cover page as a Resource. * An XHTML document containing a link to the cover image. * * @return The book's cover page as a Resource */ public Resource getCoverPage() { Resource coverPage = guide.getCoverPage(); if (coverPage == null) { coverPage = spine.getResource(0); } return coverPage; } public void setCoverPage(Resource coverPage) { if (coverPage == null) { return; } if (resources.notContainsByHref(coverPage.getHref())) { resources.add(coverPage); } guide.setCoverPage(coverPage); } /** * Gets the first non-blank title from the book's metadata. * * @return the first non-blank title from the book's metadata. */ public String getTitle() { return getMetadata().getFirstTitle(); } /** * The book's cover image. * * @return The book's cover image. */ public Resource getCoverImage() { return coverImage; } public void setCoverImage(Resource coverImage) { if (coverImage == null) { return; } if (resources.notContainsByHref(coverImage.getHref())) { resources.add(coverImage); } this.coverImage = coverImage; } /** * The guide; contains references to special sections of the book like * colophon, glossary, etc. * * @return The guide; contains references to special sections of the book * like colophon, glossary, etc. */ public Guide getGuide() { return guide; } /** * All Resources of the Book that can be reached via the Spine, the * TableOfContents or the Guide. *

* Consists of a list of "reachable" resources: *

    *
  • The coverpage
  • *
  • The resources of the Spine that are not already in the result
  • *
  • The resources of the Table of Contents that are not already in the * result
  • *
  • The resources of the Guide that are not already in the result
  • *
* To get all html files that make up the epub file use * {@link #getResources()} * * @return All Resources of the Book that can be reached via the Spine, * the TableOfContents or the Guide. */ public List getContents() { Map result = new LinkedHashMap<>(); addToContentsResult(getCoverPage(), result); for (SpineReference spineReference : getSpine().getSpineReferences()) { addToContentsResult(spineReference.getResource(), result); } for (Resource resource : getTableOfContents().getAllUniqueResources()) { addToContentsResult(resource, result); } for (GuideReference guideReference : getGuide().getReferences()) { addToContentsResult(guideReference.getResource(), result); } return new ArrayList<>(result.values()); } private static void addToContentsResult(Resource resource, Map allReachableResources) { if (resource != null && (!allReachableResources .containsKey(resource.getHref()))) { allReachableResources.put(resource.getHref(), resource); } } public Resource getOpfResource() { return opfResource; } public void setOpfResource(Resource opfResource) { this.opfResource = opfResource; } public void setNcxResource(Resource ncxResource) { this.ncxResource = ncxResource; } public Resource getNcxResource() { return ncxResource; } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/EpubResourceProvider.java ================================================ package me.ag2s.epublib.domain; import java.io.IOException; import java.io.InputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** * @author jake */ public class EpubResourceProvider implements LazyResourceProvider { private final String epubFilename; /** * @param epubFilename the file name for the epub we're created from. */ public EpubResourceProvider(String epubFilename) { this.epubFilename = epubFilename; } @Override public InputStream getResourceStream(String href) throws IOException { ZipFile zipFile = new ZipFile(epubFilename); ZipEntry zipEntry = zipFile.getEntry(href); if (zipEntry == null) { zipFile.close(); throw new IllegalStateException( "Cannot find entry " + href + " in epub file " + epubFilename); } return new ResourceInputStream(zipFile.getInputStream(zipEntry), zipFile); } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/FileResourceProvider.java ================================================ package me.ag2s.epublib.domain; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; /** * 用于创建epub,添加大文件(如大量图片)时容易OOM,使用LazyResource,避免OOM. * */ public class FileResourceProvider implements LazyResourceProvider { //需要导入资源的父目录 String dir; /** * 创建一个文件夹里面文件夹的LazyResourceProvider,用于LazyResource。 * @param parentDir 文件的目录 */ public FileResourceProvider(String parentDir) { this.dir = parentDir; } /** * 创建一个文件夹里面文件夹的LazyResourceProvider,用于LazyResource。 * @param parentFile 文件夹 */ @SuppressWarnings("unused") public FileResourceProvider(File parentFile) { this.dir = parentFile.getPath(); } /** * 根据子文件名href,再父目录下读取文件获取FileInputStream * @param href 子文件名href * @return 对应href的FileInputStream * @throws IOException 抛出IOException */ @Override public InputStream getResourceStream(String href) throws IOException { return new FileInputStream(new File(dir, href)); } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/Guide.java ================================================ package me.ag2s.epublib.domain; import java.io.Serializable; import java.util.ArrayList; import java.util.List; /** * The guide is a selection of special pages of the book. * Examples of these are the cover, list of illustrations, etc. * * It is an optional part of an epub, and support for the various types * of references varies by reader. * * The only part of this that is heavily used is the cover page. * * @author paul * */ public class Guide implements Serializable { /** * */ private static final long serialVersionUID = -6256645339915751189L; public static final String DEFAULT_COVER_TITLE = GuideReference.COVER; private List references = new ArrayList<>(); private static final int COVERPAGE_NOT_FOUND = -1; private static final int COVERPAGE_UNITIALIZED = -2; private int coverPageIndex = -1; public List getReferences() { return references; } public void setReferences(List references) { this.references = references; uncheckCoverPage(); } private void uncheckCoverPage() { coverPageIndex = COVERPAGE_UNITIALIZED; } public GuideReference getCoverReference() { checkCoverPage(); if (coverPageIndex >= 0) { return references.get(coverPageIndex); } return null; } @SuppressWarnings("UnusedReturnValue") public int setCoverReference(GuideReference guideReference) { if (coverPageIndex >= 0) { references.set(coverPageIndex, guideReference); } else { references.add(0, guideReference); coverPageIndex = 0; } return coverPageIndex; } private void checkCoverPage() { if (coverPageIndex == COVERPAGE_UNITIALIZED) { initCoverPage(); } } private void initCoverPage() { int result = COVERPAGE_NOT_FOUND; for (int i = 0; i < references.size(); i++) { GuideReference guideReference = references.get(i); if (guideReference.getType().equals(GuideReference.COVER)) { result = i; break; } } coverPageIndex = result; } /** * The coverpage of the book. * * @return The coverpage of the book. */ public Resource getCoverPage() { GuideReference guideReference = getCoverReference(); if (guideReference == null) { return null; } return guideReference.getResource(); } public void setCoverPage(Resource coverPage) { GuideReference coverpageGuideReference = new GuideReference(coverPage, GuideReference.COVER, DEFAULT_COVER_TITLE); setCoverReference(coverpageGuideReference); } @SuppressWarnings("UnusedReturnValue") public ResourceReference addReference(GuideReference reference) { this.references.add(reference); uncheckCoverPage(); return reference; } /** * A list of all GuideReferences that have the given * referenceTypeName (ignoring case). * * @param referenceTypeName referenceTypeName * @return A list of all GuideReferences that have the given * referenceTypeName (ignoring case). */ public List getGuideReferencesByType( String referenceTypeName) { List result = new ArrayList<>(); for (GuideReference guideReference : references) { if (referenceTypeName.equalsIgnoreCase(guideReference.getType())) { result.add(guideReference); } } return result; } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/GuideReference.java ================================================ package me.ag2s.epublib.domain; import me.ag2s.epublib.util.StringUtil; import java.io.Serializable; /** * These are references to elements of the book's guide. * * @see Guide * * @author paul * */ public class GuideReference extends TitledResourceReference implements Serializable { private static final long serialVersionUID = -316179702440631834L; /** * the book cover(s), jacket information, etc. */ public static final String COVER = "cover"; /** * human-readable page with title, author, publisher, and other metadata */ public static String TITLE_PAGE = "title-page"; /** * Human-readable table of contents. * Not to be confused the epub file table of contents * */ public static String TOC = "toc"; /** * back-of-book style index */ public static String INDEX = "index"; public static String GLOSSARY = "glossary"; public static String ACKNOWLEDGEMENTS = "acknowledgements"; public static String BIBLIOGRAPHY = "bibliography"; public static String COLOPHON = "colophon"; public static String COPYRIGHT_PAGE = "copyright-page"; public static String DEDICATION = "dedication"; /** * an epigraph is a phrase, quotation, or poem that is set at the * beginning of a document or component. * * source: http://en.wikipedia.org/wiki/Epigraph_%28literature%29 */ public static String EPIGRAPH = "epigraph"; public static String FOREWORD = "foreword"; /** * list of illustrations */ public static String LOI = "loi"; /** * list of tables */ public static String LOT = "lot"; public static String NOTES = "notes"; public static String PREFACE = "preface"; /** * A page of content (e.g. "Chapter 1") */ public static String TEXT = "text"; private String type; public GuideReference(Resource resource) { this(resource, null); } public GuideReference(Resource resource, String title) { super(resource, title); } public GuideReference(Resource resource, String type, String title) { this(resource, type, title, null); } public GuideReference(Resource resource, String type, String title, String fragmentId) { super(resource, title, fragmentId); this.type = StringUtil.isNotBlank(type) ? type.toLowerCase() : null; } public String getType() { return type; } public void setType(String type) { this.type = type; } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/Identifier.java ================================================ package me.ag2s.epublib.domain; import me.ag2s.epublib.util.StringUtil; import java.io.Serializable; import java.util.List; import java.util.UUID; /** * A Book's identifier. * * Defaults to a random UUID and scheme "UUID" * * @author paul */ public class Identifier implements Serializable { private static final long serialVersionUID = 955949951416391810L; @SuppressWarnings("unused") public interface Scheme { String UUID = "UUID"; String ISBN = "ISBN"; String URL = "URL"; String URI = "URI"; } private boolean bookId = false; private String scheme; private String value; /** * Creates an Identifier with as value a random UUID and scheme "UUID" */ public Identifier() { this(Scheme.UUID, UUID.randomUUID().toString()); } public Identifier(String scheme, String value) { this.scheme = scheme; this.value = value; } /** * The first identifier for which the bookId is true is made the * bookId identifier. * * If no identifier has bookId == true then the first bookId identifier * is written as the primary. * * @param identifiers i * @return The first identifier for which the bookId is true is made * the bookId identifier. */ public static Identifier getBookIdIdentifier(List identifiers) { if (identifiers == null || identifiers.isEmpty()) { return null; } Identifier result = null; for (Identifier identifier : identifiers) { if (identifier.isBookId()) { result = identifier; break; } } if (result == null) { result = identifiers.get(0); } return result; } public String getScheme() { return scheme; } public void setScheme(String scheme) { this.scheme = scheme; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } public void setBookId(boolean bookId) { this.bookId = bookId; } /** * This bookId property allows the book creator to add multiple ids and * tell the epubwriter which one to write out as the bookId. * * The Dublin Core metadata spec allows multiple identifiers for a Book. * The epub spec requires exactly one identifier to be marked as the book id. * * @return whether this is the unique book id. */ public boolean isBookId() { return bookId; } public int hashCode() { return StringUtil.defaultIfNull(scheme).hashCode() ^ StringUtil .defaultIfNull(value).hashCode(); } public boolean equals(Object otherIdentifier) { if (!(otherIdentifier instanceof Identifier)) { return false; } return StringUtil.equals(scheme, ((Identifier) otherIdentifier).scheme) && StringUtil.equals(value, ((Identifier) otherIdentifier).value); } @SuppressWarnings("NullableProblems") @Override public String toString() { if (StringUtil.isBlank(scheme)) { return "" + value; } return "" + scheme + ":" + value; } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/LazyResource.java ================================================ package me.ag2s.epublib.domain; import me.ag2s.epublib.util.IOUtil; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; /** * A Resource that loads its data only on-demand from a EPUB book file. * This way larger books can fit into memory and can be opened faster. */ public class LazyResource extends Resource { private static final long serialVersionUID = 5089400472352002866L; private final String TAG= getClass().getName(); private final LazyResourceProvider resourceProvider; private final long cachedSize; /** * Creates a lazy resource, when the size is unknown. * * @param resourceProvider The resource provider loads data on demand. * @param href The resource's href within the epub. */ public LazyResource(LazyResourceProvider resourceProvider, String href) { this(resourceProvider, -1, href); } public LazyResource(LazyResourceProvider resourceProvider, String href, String originalHref) { this(resourceProvider, -1, href, originalHref); } /** * Creates a Lazy resource, by not actually loading the data for this entry. * * The data will be loaded on the first call to getData() * * @param resourceProvider The resource provider loads data on demand. * @param size The size of this resource. * @param href The resource's href within the epub. */ public LazyResource( LazyResourceProvider resourceProvider, long size, String href) { super(null, null, href, MediaTypes.determineMediaType(href)); this.resourceProvider = resourceProvider; this.cachedSize = size; } public LazyResource( LazyResourceProvider resourceProvider, long size, String href, String originalHref) { super(null, null, href, originalHref, MediaTypes.determineMediaType(href)); this.resourceProvider = resourceProvider; this.cachedSize = size; } /** * Gets the contents of the Resource as an InputStream. * * @return The contents of the Resource. * * @throws IOException IOException */ public InputStream getInputStream() throws IOException { if (isInitialized()) { return new ByteArrayInputStream(getData()); } else { return resourceProvider.getResourceStream(this.originalHref); } } /** * Initializes the resource by loading its data into memory. * * @throws IOException IOException */ public void initialize() throws IOException { getData(); } /** * The contents of the resource as a byte[] * * If this resource was lazy-loaded and the data was not yet loaded, * it will be loaded into memory at this point. * This included opening the zip file, so expect a first load to be slow. * * @return The contents of the resource */ public byte[] getData() throws IOException { if (data == null) { // Log.d(TAG, "Initializing lazy resource: " + this.getHref()); InputStream in = resourceProvider.getResourceStream(this.originalHref); byte[] readData = IOUtil.toByteArray(in, (int) this.cachedSize); if (readData == null) { throw new IOException( "Could not load the contents of resource: " + this.getHref()); } else { this.data = readData; } in.close(); } return data; } /** * Tells this resource to release its cached data. * * If this resource was not lazy-loaded, this is a no-op. */ public void close() { if (this.resourceProvider != null) { this.data = null; } } /** * Returns if the data for this resource has been loaded into memory. * * @return true if data was loaded. */ public boolean isInitialized() { return data != null; } /** * Returns the size of this resource in bytes. * * @return the size. */ public long getSize() { if (data != null) { return data.length; } return cachedSize; } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/LazyResourceProvider.java ================================================ package me.ag2s.epublib.domain; import java.io.IOException; import java.io.InputStream; /** * @author jake */ public interface LazyResourceProvider { InputStream getResourceStream(String href) throws IOException; } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/ManifestItemProperties.java ================================================ package me.ag2s.epublib.domain; @SuppressWarnings("unused") public enum ManifestItemProperties implements ManifestProperties { COVER_IMAGE("cover-image"), MATHML("mathml"), NAV("nav"), REMOTE_RESOURCES("remote-resources"), SCRIPTED("scripted"), SVG("svg"), SWITCH("switch"); private final String name; ManifestItemProperties(String name) { this.name = name; } public String getName() { return name; } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/ManifestItemRefProperties.java ================================================ package me.ag2s.epublib.domain; @SuppressWarnings("unused") public enum ManifestItemRefProperties implements ManifestProperties { PAGE_SPREAD_LEFT("page-spread-left"), PAGE_SPREAD_RIGHT("page-spread-right"); private final String name; ManifestItemRefProperties(String name) { this.name = name; } public String getName() { return name; } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/ManifestProperties.java ================================================ package me.ag2s.epublib.domain; public interface ManifestProperties { String getName(); } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/MediaType.java ================================================ package me.ag2s.epublib.domain; import java.io.Serializable; import java.util.Arrays; import java.util.Collection; /** * MediaType is used to tell the type of content a resource is. * * Examples of mediatypes are image/gif, text/css and application/xhtml+xml * * All allowed mediaTypes are maintained bye the MediaTypeService. * * @see MediaTypes * * @author paul */ public class MediaType implements Serializable { private static final long serialVersionUID = -7256091153727506788L; private final String name; private final String defaultExtension; private final Collection extensions; public MediaType(String name, String defaultExtension) { this(name, defaultExtension, new String[]{defaultExtension}); } public MediaType(String name, String defaultExtension, String[] extensions) { this(name, defaultExtension, Arrays.asList(extensions)); } public int hashCode() { if (name == null) { return 0; } return name.hashCode(); } public MediaType(String name, String defaultExtension, Collection mextensions) { super(); this.name = name; this.defaultExtension = defaultExtension; this.extensions = mextensions; } public String getName() { return name; } public String getDefaultExtension() { return defaultExtension; } public Collection getExtensions() { return extensions; } public boolean equals(Object otherMediaType) { if (!(otherMediaType instanceof MediaType)) { return false; } return name.equals(((MediaType) otherMediaType).getName()); } @SuppressWarnings("NullableProblems") public String toString() { return name; } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/MediaTypes.java ================================================ package me.ag2s.epublib.domain; import me.ag2s.epublib.util.StringUtil; import java.util.HashMap; import java.util.Map; /** * Manages mediatypes that are used by epubs * * @author paul */ public class MediaTypes { public static final MediaType XHTML = new MediaType("application/xhtml+xml", ".xhtml", new String[]{".htm", ".html", ".xhtml"}); public static final MediaType EPUB = new MediaType("application/epub+zip", ".epub"); public static final MediaType NCX = new MediaType("application/x-dtbncx+xml", ".ncx"); public static final MediaType JAVASCRIPT = new MediaType("text/javascript", ".js"); public static final MediaType CSS = new MediaType("text/css", ".css"); // images public static final MediaType JPG = new MediaType("image/jpeg", ".jpg", new String[]{".jpg", ".jpeg"}); public static final MediaType PNG = new MediaType("image/png", ".png"); public static final MediaType GIF = new MediaType("image/gif", ".gif"); public static final MediaType SVG = new MediaType("image/svg+xml", ".svg"); // fonts public static final MediaType TTF = new MediaType( "application/x-truetype-font", ".ttf"); public static final MediaType OPENTYPE = new MediaType( "application/vnd.ms-opentype", ".otf"); public static final MediaType WOFF = new MediaType("application/font-woff", ".woff"); // audio public static final MediaType MP3 = new MediaType("audio/mpeg", ".mp3"); public static final MediaType OGG = new MediaType("audio/ogg", ".ogg"); // video public static final MediaType MP4 = new MediaType("video/mp4", ".mp4"); public static final MediaType SMIL = new MediaType("application/smil+xml", ".smil"); public static final MediaType XPGT = new MediaType( "application/adobe-page-template+xml", ".xpgt"); public static final MediaType PLS = new MediaType("application/pls+xml", ".pls"); public static final MediaType[] mediaTypes = new MediaType[]{ XHTML, EPUB, JPG, PNG, GIF, CSS, SVG, TTF, NCX, XPGT, OPENTYPE, WOFF, SMIL, PLS, JAVASCRIPT, MP3, MP4, OGG }; public static final Map mediaTypesByName = new HashMap<>(); static { for (MediaType mediaType : mediaTypes) { mediaTypesByName.put(mediaType.getName(), mediaType); } } public static boolean isBitmapImage(MediaType mediaType) { return mediaType == JPG || mediaType == PNG || mediaType == GIF; } /** * Gets the MediaType based on the file extension. * Null of no matching extension found. * * @param filename filename * @return the MediaType based on the file extension. */ public static MediaType determineMediaType(String filename) { for (MediaType mediaType : mediaTypesByName.values()) { for (String extension : mediaType.getExtensions()) { if (StringUtil.endsWithIgnoreCase(filename, extension)) { return mediaType; } } } return null; } public static MediaType getMediaTypeByName(String mediaTypeName) { return mediaTypesByName.get(mediaTypeName); } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/Metadata.java ================================================ package me.ag2s.epublib.domain; import me.ag2s.epublib.util.StringUtil; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.xml.namespace.QName; /** * A Book's collection of Metadata. * In the future it should contain all Dublin Core attributes, for now * it contains a set of often-used ones. * * @author paul */ public class Metadata implements Serializable { private static final long serialVersionUID = -2437262888962149444L; public static final String DEFAULT_LANGUAGE = "en"; private boolean autoGeneratedId;//true; private List authors = new ArrayList<>(); private List contributors = new ArrayList<>(); private List dates = new ArrayList<>(); private String language = DEFAULT_LANGUAGE; private Map otherProperties = new HashMap<>(); private List rights = new ArrayList<>(); private List titles = new ArrayList<>(); private List identifiers = new ArrayList<>(); private List subjects = new ArrayList<>(); private String format = MediaTypes.EPUB.getName(); private List types = new ArrayList<>(); private List descriptions = new ArrayList<>(); private List publishers = new ArrayList<>(); private Map metaAttributes = new HashMap<>(); public Metadata() { identifiers.add(new Identifier()); autoGeneratedId = true; } @SuppressWarnings("unused") public boolean isAutoGeneratedId() { return autoGeneratedId; } /** * Metadata properties not hard-coded like the author, title, etc. * * @return Metadata properties not hard-coded like the author, title, etc. */ public Map getOtherProperties() { return otherProperties; } public void setOtherProperties(Map otherProperties) { this.otherProperties = otherProperties; } @SuppressWarnings("unused") public Date addDate(Date date) { this.dates.add(date); return date; } public List getDates() { return dates; } public void setDates(List dates) { this.dates = dates; } @SuppressWarnings("UnusedReturnValue") public Author addAuthor(Author author) { authors.add(author); return author; } public List getAuthors() { return authors; } public void setAuthors(List authors) { this.authors = authors; } @SuppressWarnings("UnusedReturnValue") public Author addContributor(Author contributor) { contributors.add(contributor); return contributor; } public List getContributors() { return contributors; } public void setContributors(List contributors) { this.contributors = contributors; } public String getLanguage() { return language; } public void setLanguage(String language) { this.language = language; } public List getSubjects() { return subjects; } public void setSubjects(List subjects) { this.subjects = subjects; } public void setRights(List rights) { this.rights = rights; } public List getRights() { return rights; } /** * Gets the first non-blank title of the book. * Will return "" if no title found. * * @return the first non-blank title of the book. */ public String getFirstTitle() { if (titles == null || titles.isEmpty()) { return ""; } for (String title : titles) { if (StringUtil.isNotBlank(title)) { return title; } } return ""; } public String addTitle(String title) { this.titles.add(title); return title; } public void setTitles(List titles) { this.titles = titles; } public List getTitles() { return titles; } @SuppressWarnings("UnusedReturnValue") public String addPublisher(String publisher) { this.publishers.add(publisher); return publisher; } public void setPublishers(List publishers) { this.publishers = publishers; } public List getPublishers() { return publishers; } @SuppressWarnings("UnusedReturnValue") public String addDescription(String description) { this.descriptions.add(description); return description; } public void setDescriptions(List descriptions) { this.descriptions = descriptions; } public List getDescriptions() { return descriptions; } @SuppressWarnings("unused") public Identifier addIdentifier(Identifier identifier) { if (autoGeneratedId && (!(identifiers.isEmpty()))) { identifiers.set(0, identifier); } else { identifiers.add(identifier); } autoGeneratedId = false; return identifier; } public void setIdentifiers(List identifiers) { this.identifiers = identifiers; autoGeneratedId = false; } public List getIdentifiers() { return identifiers; } public void setFormat(String format) { this.format = format; } public String getFormat() { return format; } @SuppressWarnings("UnusedReturnValue") public String addType(String type) { this.types.add(type); return type; } public List getTypes() { return types; } public void setTypes(List types) { this.types = types; } @SuppressWarnings("unused") public String getMetaAttribute(String name) { return metaAttributes.get(name); } public void setMetaAttributes(Map metaAttributes) { this.metaAttributes = metaAttributes; } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/Relator.java ================================================ package me.ag2s.epublib.domain; /** * A relator denotes which role a certain individual had in the creation/modification of the ebook. * * Examples are 'creator', 'blurb writer', etc. * * This is contains the complete Library of Concress relator list. * * @see MARC Code List for Relators * * @author paul */ public enum Relator { /** * Use for a person or organization who principally exhibits acting skills in a musical or dramatic presentation or entertainment. */ ACTOR("act", "Actor"), /** * Use for a person or organization who 1) reworks a musical composition, usually for a different medium, or 2) rewrites novels or stories for motion pictures or other audiovisual medium. */ ADAPTER("adp", "Adapter"), /** * Use for a person or organization that reviews, examines and interprets data or information in a specific area. */ ANALYST("anl", "Analyst"), /** * Use for a person or organization who draws the two-dimensional figures, manipulates the three dimensional objects and/or also programs the computer to move objects and images for the purpose of animated film processing. Animation cameras, stands, celluloid screens, transparencies and inks are some of the tools of the animator. */ ANIMATOR("anm", "Animator"), /** * Use for a person who writes manuscript annotations on a printed item. */ ANNOTATOR("ann", "Annotator"), /** * Use for a person or organization responsible for the submission of an application or who is named as eligible for the results of the processing of the application (e.g., bestowing of rights, reward, title, position). */ APPLICANT("app", "Applicant"), /** * Use for a person or organization who designs structures or oversees their construction. */ ARCHITECT("arc", "Architect"), /** * Use for a person or organization who transcribes a musical composition, usually for a different medium from that of the original; in an arrangement the musical substance remains essentially unchanged. */ ARRANGER("arr", "Arranger"), /** * Use for a person (e.g., a painter or sculptor) who makes copies of works of visual art. */ ART_COPYIST("acp", "Art copyist"), /** * Use for a person (e.g., a painter) or organization who conceives, and perhaps also implements, an original graphic design or work of art, if specific codes (e.g., [egr], [etr]) are not desired. For book illustrators, prefer Illustrator [ill]. */ ARTIST("art", "Artist"), /** * Use for a person responsible for controlling the development of the artistic style of an entire production, including the choice of works to be presented and selection of senior production staff. */ ARTISTIC_DIRECTOR("ard", "Artistic director"), /** * Use for a person or organization to whom a license for printing or publishing has been transferred. */ ASSIGNEE("asg", "Assignee"), /** * Use for a person or organization associated with or found in an item or collection, which cannot be determined to be that of a Former owner [fmo] or other designated relator indicative of provenance. */ ASSOCIATED_NAME("asn", "Associated name"), /** * Use for an author, artist, etc., relating him/her to a work for which there is or once was substantial authority for designating that person as author, creator, etc. of the work. */ ATTRIBUTED_NAME("att", "Attributed name"), /** * Use for a person or organization in charge of the estimation and public auctioning of goods, particularly books, artistic works, etc. */ AUCTIONEER("auc", "Auctioneer"), /** * Use for a person or organization chiefly responsible for the intellectual or artistic content of a work, usually printed text. This term may also be used when more than one person or body bears such responsibility. */ AUTHOR("aut", "Author"), /** * Use for a person or organization whose work is largely quoted or extracted in works to which he or she did not contribute directly. Such quotations are found particularly in exhibition catalogs, collections of photographs, etc. */ AUTHOR_IN_QUOTATIONS_OR_TEXT_EXTRACTS("aqt", "Author in quotations or text extracts"), /** * Use for a person or organization responsible for an afterword, postface, colophon, etc. but who is not the chief author of a work. */ AUTHOR_OF_AFTERWORD_COLOPHON_ETC("aft", "Author of afterword, colophon, etc."), /** * Use for a person or organization responsible for the dialog or spoken commentary for a screenplay or sound recording. */ AUTHOR_OF_DIALOG("aud", "Author of dialog"), /** * Use for a person or organization responsible for an introduction, preface, foreword, or other critical introductory matter, but who is not the chief author. */ AUTHOR_OF_INTRODUCTION_ETC("aui", "Author of introduction, etc."), /** * Use for a person or organization responsible for a motion picture screenplay, dialog, spoken commentary, etc. */ AUTHOR_OF_SCREENPLAY_ETC("aus", "Author of screenplay, etc."), /** * Use for a person or organization responsible for a work upon which the work represented by the catalog record is based. This may be appropriate for adaptations, sequels, continuations, indexes, etc. */ BIBLIOGRAPHIC_ANTECEDENT("ant", "Bibliographic antecedent"), /** * Use for a person or organization responsible for the binding of printed or manuscript materials. */ BINDER("bnd", "Binder"), /** * Use for a person or organization responsible for the binding design of a book, including the type of binding, the type of materials used, and any decorative aspects of the binding. */ BINDING_DESIGNER("bdd", "Binding designer"), /** * Use for the named entity responsible for writing a commendation or testimonial for a work, which appears on or within the publication itself, frequently on the back or dust jacket of print publications or on advertising material for all media. */ BLURB_WRITER("blw", "Blurb writer"), /** * Use for a person or organization responsible for the entire graphic design of a book, including arrangement of type and illustration, choice of materials, and process used. */ BOOK_DESIGNER("bkd", "Book designer"), /** * Use for a person or organization responsible for the production of books and other print media, if specific codes (e.g., [bkd], [egr], [tyd], [prt]) are not desired. */ BOOK_PRODUCER("bkp", "Book producer"), /** * Use for a person or organization responsible for the design of flexible covers designed for or published with a book, including the type of materials used, and any decorative aspects of the bookjacket. */ BOOKJACKET_DESIGNER("bjd", "Bookjacket designer"), /** * Use for a person or organization responsible for the design of a book owner's identification label that is most commonly pasted to the inside front cover of a book. */ BOOKPLATE_DESIGNER("bpd", "Bookplate designer"), /** * Use for a person or organization who makes books and other bibliographic materials available for purchase. Interest in the materials is primarily lucrative. */ BOOKSELLER("bsl", "Bookseller"), /** * Use for a person or organization who writes in an artistic hand, usually as a copyist and or engrosser. */ CALLIGRAPHER("cll", "Calligrapher"), /** * Use for a person or organization responsible for the creation of maps and other cartographic materials. */ CARTOGRAPHER("ctg", "Cartographer"), /** * Use for a censor, bowdlerizer, expurgator, etc., official or private. */ CENSOR("cns", "Censor"), /** * Use for a person or organization who composes or arranges dances or other movements (e.g., "master of swords") for a musical or dramatic presentation or entertainment. */ CHOREOGRAPHER("chr", "Choreographer"), /** * Use for a person or organization who is in charge of the images captured for a motion picture film. The cinematographer works under the supervision of a director, and may also be referred to as director of photography. Do not confuse with videographer. */ CINEMATOGRAPHER("cng", "Cinematographer"), /** * Use for a person or organization for whom another person or organization is acting. */ CLIENT("cli", "Client"), /** * Use for a person or organization that takes a limited part in the elaboration of a work of another person or organization that brings complements (e.g., appendices, notes) to the work. */ COLLABORATOR("clb", "Collaborator"), /** * Use for a person or organization who has brought together material from various sources that has been arranged, described, and cataloged as a collection. A collector is neither the creator of the material nor a person to whom manuscripts in the collection may have been addressed. */ COLLECTOR("col", "Collector"), /** * Use for a person or organization responsible for the production of photographic prints from film or other colloid that has ink-receptive and ink-repellent surfaces. */ COLLOTYPER("clt", "Collotyper"), /** * Use for the named entity responsible for applying color to drawings, prints, photographs, maps, moving images, etc. */ COLORIST("clr", "Colorist"), /** * Use for a person or organization who provides interpretation, analysis, or a discussion of the subject matter on a recording, motion picture, or other audiovisual medium. */ COMMENTATOR("cmm", "Commentator"), /** * Use for a person or organization responsible for the commentary or explanatory notes about a text. For the writer of manuscript annotations in a printed book, use Annotator [ann]. */ COMMENTATOR_FOR_WRITTEN_TEXT("cwt", "Commentator for written text"), /** * Use for a person or organization who produces a work or publication by selecting and putting together material from the works of various persons or bodies. */ COMPILER("com", "Compiler"), /** * Use for the party who applies to the courts for redress, usually in an equity proceeding. */ COMPLAINANT("cpl", "Complainant"), /** * Use for a complainant who takes an appeal from one court or jurisdiction to another to reverse the judgment, usually in an equity proceeding. */ COMPLAINANT_APPELLANT("cpt", "Complainant-appellant"), /** * Use for a complainant against whom an appeal is taken from one court or jurisdiction to another to reverse the judgment, usually in an equity proceeding. */ COMPLAINANT_APPELLEE("cpe", "Complainant-appellee"), /** * Use for a person or organization who creates a musical work, usually a piece of music in manuscript or printed form. */ COMPOSER("cmp", "Composer"), /** * Use for a person or organization responsible for the creation of metal slug, or molds made of other materials, used to produce the text and images in printed matter. */ COMPOSITOR("cmt", "Compositor"), /** * Use for a person or organization responsible for the original idea on which a work is based, this includes the scientific author of an audio-visual item and the conceptor of an advertisement. */ CONCEPTOR("ccp", "Conceptor"), /** * Use for a person who directs a performing group (orchestra, chorus, opera, etc.) in a musical or dramatic presentation or entertainment. */ CONDUCTOR("cnd", "Conductor"), /** * Use for the named entity responsible for documenting, preserving, or treating printed or manuscript material, works of art, artifacts, or other media. */ CONSERVATOR("con", "Conservator"), /** * Use for a person or organization relevant to a resource, who is called upon for professional advice or services in a specialized field of knowledge or training. */ CONSULTANT("csl", "Consultant"), /** * Use for a person or organization relevant to a resource, who is engaged specifically to provide an intellectual overview of a strategic or operational task and by analysis, specification, or instruction, to create or propose a cost-effective course of action or solution. */ CONSULTANT_TO_A_PROJECT("csp", "Consultant to a project"), /** * Use for the party who opposes, resists, or disputes, in a court of law, a claim, decision, result, etc. */ CONTESTANT("cos", "Contestant"), /** * Use for a contestant who takes an appeal from one court of law or jurisdiction to another to reverse the judgment. */ CONTESTANT_APPELLANT("cot", "Contestant-appellant"), /** * Use for a contestant against whom an appeal is taken from one court of law or jurisdiction to another to reverse the judgment. */ CONTESTANT_APPELLEE("coe", "Contestant-appellee"), /** * Use for the party defending a claim, decision, result, etc. being opposed, resisted, or disputed in a court of law. */ CONTESTEE("cts", "Contestee"), /** * Use for a contestee who takes an appeal from one court or jurisdiction to another to reverse the judgment. */ CONTESTEE_APPELLANT("ctt", "Contestee-appellant"), /** * Use for a contestee against whom an appeal is taken from one court or jurisdiction to another to reverse the judgment. */ CONTESTEE_APPELLEE("cte", "Contestee-appellee"), /** * Use for a person or organization relevant to a resource, who enters into a contract with another person or organization to perform a specific task. */ CONTRACTOR("ctr", "Contractor"), /** * Use for a person or organization one whose work has been contributed to a larger work, such as an anthology, serial publication, or other compilation of individual works. Do not use if the sole function in relation to a work is as author, editor, compiler or translator. */ CONTRIBUTOR("ctb", "Contributor"), /** * Use for a person or organization listed as a copyright owner at the time of registration. Copyright can be granted or later transferred to another person or organization, at which time the claimant becomes the copyright holder. */ COPYRIGHT_CLAIMANT("cpc", "Copyright claimant"), /** * Use for a person or organization to whom copy and legal rights have been granted or transferred for the intellectual content of a work. The copyright holder, although not necessarily the creator of the work, usually has the exclusive right to benefit financially from the sale and use of the work to which the associated copyright protection applies. */ COPYRIGHT_HOLDER("cph", "Copyright holder"), /** * Use for a person or organization who is a corrector of manuscripts, such as the scriptorium official who corrected the work of a scribe. For printed matter, use Proofreader. */ CORRECTOR("crr", "Corrector"), /** * Use for a person or organization who was either the writer or recipient of a letter or other communication. */ CORRESPONDENT("crp", "Correspondent"), /** * Use for a person or organization who designs or makes costumes, fixes hair, etc., for a musical or dramatic presentation or entertainment. */ COSTUME_DESIGNER("cst", "Costume designer"), /** * Use for a person or organization responsible for the graphic design of a book cover, album cover, slipcase, box, container, etc. For a person or organization responsible for the graphic design of an entire book, use Book designer; for book jackets, use Bookjacket designer. */ COVER_DESIGNER("cov", "Cover designer"), /** * Use for a person or organization responsible for the intellectual or artistic content of a work. */ CREATOR("cre", "Creator"), /** * Use for a person or organization responsible for conceiving and organizing an exhibition. */ CURATOR_OF_AN_EXHIBITION("cur", "Curator of an exhibition"), /** * Use for a person or organization who principally exhibits dancing skills in a musical or dramatic presentation or entertainment. */ DANCER("dnc", "Dancer"), /** * Use for a person or organization that submits data for inclusion in a database or other collection of data. */ DATA_CONTRIBUTOR("dtc", "Data contributor"), /** * Use for a person or organization responsible for managing databases or other data sources. */ DATA_MANAGER("dtm", "Data manager"), /** * Use for a person or organization to whom a book, manuscript, etc., is dedicated (not the recipient of a gift). */ DEDICATEE("dte", "Dedicatee"), /** * Use for the author of a dedication, which may be a formal statement or in epistolary or verse form. */ DEDICATOR("dto", "Dedicator"), /** * Use for the party defending or denying allegations made in a suit and against whom relief or recovery is sought in the courts, usually in a legal action. */ DEFENDANT("dfd", "Defendant"), /** * Use for a defendant who takes an appeal from one court or jurisdiction to another to reverse the judgment, usually in a legal action. */ DEFENDANT_APPELLANT("dft", "Defendant-appellant"), /** * Use for a defendant against whom an appeal is taken from one court or jurisdiction to another to reverse the judgment, usually in a legal action. */ DEFENDANT_APPELLEE("dfe", "Defendant-appellee"), /** * Use for the organization granting a degree for which the thesis or dissertation described was presented. */ DEGREE_GRANTOR("dgg", "Degree grantor"), /** * Use for a person or organization executing technical drawings from others' designs. */ DELINEATOR("dln", "Delineator"), /** * Use for an entity depicted or portrayed in a work, particularly in a work of art. */ DEPICTED("dpc", "Depicted"), /** * Use for a person or organization placing material in the physical custody of a library or repository without transferring the legal title. */ DEPOSITOR("dpt", "Depositor"), /** * Use for a person or organization responsible for the design if more specific codes (e.g., [bkd], [tyd]) are not desired. */ DESIGNER("dsr", "Designer"), /** * Use for a person or organization who is responsible for the general management of a work or who supervises the production of a performance for stage, screen, or sound recording. */ DIRECTOR("drt", "Director"), /** * Use for a person who presents a thesis for a university or higher-level educational degree. */ DISSERTANT("dis", "Dissertant"), /** * Use for the name of a place from which a resource, e.g., a serial, is distributed. */ DISTRIBUTION_PLACE("dbp", "Distribution place"), /** * Use for a person or organization that has exclusive or shared marketing rights for an item. */ DISTRIBUTOR("dst", "Distributor"), /** * Use for a person or organization who is the donor of a book, manuscript, etc., to its present owner. Donors to previous owners are designated as Former owner [fmo] or Inscriber [ins]. */ DONOR("dnr", "Donor"), /** * Use for a person or organization who prepares artistic or technical drawings. */ DRAFTSMAN("drm", "Draftsman"), /** * Use for a person or organization to which authorship has been dubiously or incorrectly ascribed. */ DUBIOUS_AUTHOR("dub", "Dubious author"), /** * Use for a person or organization who prepares for publication a work not primarily his/her own, such as by elucidating text, adding introductory or other critical matter, or technically directing an editorial staff. */ EDITOR("edt", "Editor"), /** * Use for a person responsible for setting up a lighting rig and focusing the lights for a production, and running the lighting at a performance. */ ELECTRICIAN("elg", "Electrician"), /** * Use for a person or organization who creates a duplicate printing surface by pressure molding and electrodepositing of metal that is then backed up with lead for printing. */ ELECTROTYPER("elt", "Electrotyper"), /** * Use for a person or organization that is responsible for technical planning and design, particularly with construction. */ ENGINEER("eng", "Engineer"), /** * Use for a person or organization who cuts letters, figures, etc. on a surface, such as a wooden or metal plate, for printing. */ ENGRAVER("egr", "Engraver"), /** * Use for a person or organization who produces text or images for printing by subjecting metal, glass, or some other surface to acid or the corrosive action of some other substance. */ ETCHER("etr", "Etcher"), /** * Use for the name of the place where an event such as a conference or a concert took place. */ EVENT_PLACE("evp", "Event place"), /** * Use for a person or organization in charge of the description and appraisal of the value of goods, particularly rare items, works of art, etc. */ EXPERT("exp", "Expert"), /** * Use for a person or organization that executed the facsimile. */ FACSIMILIST("fac", "Facsimilist"), /** * Use for a person or organization that manages or supervises the work done to collect raw data or do research in an actual setting or environment (typically applies to the natural and social sciences). */ FIELD_DIRECTOR("fld", "Field director"), /** * Use for a person or organization who is an editor of a motion picture film. This term is used regardless of the medium upon which the motion picture is produced or manufactured (e.g., acetate film, video tape). */ FILM_EDITOR("flm", "Film editor"), /** * Use for a person or organization who is identified as the only party or the party of the first part. In the case of transfer of right, this is the assignor, transferor, licensor, grantor, etc. Multiple parties can be named jointly as the first party */ FIRST_PARTY("fpy", "First party"), /** * Use for a person or organization who makes or imitates something of value or importance, especially with the intent to defraud. */ FORGER("frg", "Forger"), /** * Use for a person or organization who owned an item at any time in the past. Includes those to whom the material was once presented. A person or organization giving the item to the present owner is designated as Donor [dnr] */ FORMER_OWNER("fmo", "Former owner"), /** * Use for a person or organization that furnished financial support for the production of the work. */ FUNDER("fnd", "Funder"), /** * Use for a person responsible for geographic information system (GIS) development and integration with global positioning system data. */ GEOGRAPHIC_INFORMATION_SPECIALIST("gis", "Geographic information specialist"), /** * Use for a person or organization in memory or honor of whom a book, manuscript, etc. is donated. */ HONOREE("hnr", "Honoree"), /** * Use for a person who is invited or regularly leads a program (often broadcast) that includes other guests, performers, etc. (e.g., talk show host). */ HOST("hst", "Host"), /** * Use for a person or organization responsible for the decoration of a work (especially manuscript material) with precious metals or color, usually with elaborate designs and motifs. */ ILLUMINATOR("ilu", "Illuminator"), /** * Use for a person or organization who conceives, and perhaps also implements, a design or illustration, usually to accompany a written text. */ ILLUSTRATOR("ill", "Illustrator"), /** * Use for a person who signs a presentation statement. */ INSCRIBER("ins", "Inscriber"), /** * Use for a person or organization who principally plays an instrument in a musical or dramatic presentation or entertainment. */ INSTRUMENTALIST("itr", "Instrumentalist"), /** * Use for a person or organization who is interviewed at a consultation or meeting, usually by a reporter, pollster, or some other information gathering agent. */ INTERVIEWEE("ive", "Interviewee"), /** * Use for a person or organization who acts as a reporter, pollster, or other information gathering agent in a consultation or meeting involving one or more individuals. */ INTERVIEWER("ivr", "Interviewer"), /** * Use for a person or organization who first produces a particular useful item, or develops a new process for obtaining a known item or result. */ INVENTOR("inv", "Inventor"), /** * Use for an institution that provides scientific analyses of material samples. */ LABORATORY("lbr", "Laboratory"), /** * Use for a person or organization that manages or supervises work done in a controlled setting or environment. */ LABORATORY_DIRECTOR("ldr", "Laboratory director"), /** * Use for a person or organization whose work involves coordinating the arrangement of existing and proposed land features and structures. */ LANDSCAPE_ARCHITECT("lsa", "Landscape architect"), /** * Use to indicate that a person or organization takes primary responsibility for a particular activity or endeavor. Use with another relator term or code to show the greater importance this person or organization has regarding that particular role. If more than one relator is assigned to a heading, use the Lead relator only if it applies to all the relators. */ LEAD("led", "Lead"), /** * Use for a person or organization permitting the temporary use of a book, manuscript, etc., such as for photocopying or microfilming. */ LENDER("len", "Lender"), /** * Use for the party who files a libel in an ecclesiastical or admiralty case. */ LIBELANT("lil", "Libelant"), /** * Use for a libelant who takes an appeal from one ecclesiastical court or admiralty to another to reverse the judgment. */ LIBELANT_APPELLANT("lit", "Libelant-appellant"), /** * Use for a libelant against whom an appeal is taken from one ecclesiastical court or admiralty to another to reverse the judgment. */ LIBELANT_APPELLEE("lie", "Libelant-appellee"), /** * Use for a party against whom a libel has been filed in an ecclesiastical court or admiralty. */ LIBELEE("lel", "Libelee"), /** * Use for a libelee who takes an appeal from one ecclesiastical court or admiralty to another to reverse the judgment. */ LIBELEE_APPELLANT("let", "Libelee-appellant"), /** * Use for a libelee against whom an appeal is taken from one ecclesiastical court or admiralty to another to reverse the judgment. */ LIBELEE_APPELLEE("lee", "Libelee-appellee"), /** * Use for a person or organization who is a writer of the text of an opera, oratorio, etc. */ LIBRETTIST("lbt", "Librettist"), /** * Use for a person or organization who is an original recipient of the right to print or publish. */ LICENSEE("lse", "Licensee"), /** * Use for person or organization who is a signer of the license, imprimatur, etc. */ LICENSOR("lso", "Licensor"), /** * Use for a person or organization who designs the lighting scheme for a theatrical presentation, entertainment, motion picture, etc. */ LIGHTING_DESIGNER("lgd", "Lighting designer"), /** * Use for a person or organization who prepares the stone or plate for lithographic printing, including a graphic artist creating a design directly on the surface from which printing will be done. */ LITHOGRAPHER("ltg", "Lithographer"), /** * Use for a person or organization who is a writer of the text of a song. */ LYRICIST("lyr", "Lyricist"), /** * Use for a person or organization that makes an artifactual work (an object made or modified by one or more persons). Examples of artifactual works include vases, cannons or pieces of furniture. */ MANUFACTURER("mfr", "Manufacturer"), /** * Use for the named entity responsible for marbling paper, cloth, leather, etc. used in construction of a resource. */ MARBLER("mrb", "Marbler"), /** * Use for a person or organization performing the coding of SGML, HTML, or XML markup of metadata, text, etc. */ MARKUP_EDITOR("mrk", "Markup editor"), /** * Use for a person or organization primarily responsible for compiling and maintaining the original description of a metadata set (e.g., geospatial metadata set). */ METADATA_CONTACT("mdc", "Metadata contact"), /** * Use for a person or organization responsible for decorations, illustrations, letters, etc. cut on a metal surface for printing or decoration. */ METAL_ENGRAVER("mte", "Metal-engraver"), /** * Use for a person who leads a program (often broadcast) where topics are discussed, usually with participation of experts in fields related to the discussion. */ MODERATOR("mod", "Moderator"), /** * Use for a person or organization that supervises compliance with the contract and is responsible for the report and controls its distribution. Sometimes referred to as the grantee, or controlling agency. */ MONITOR("mon", "Monitor"), /** * Use for a person who transcribes or copies musical notation */ MUSIC_COPYIST("mcp", "Music copyist"), /** * Use for a person responsible for basic music decisions about a production, including coordinating the work of the composer, the sound editor, and sound mixers, selecting musicians, and organizing and/or conducting sound for rehearsals and performances. */ MUSICAL_DIRECTOR("msd", "Musical director"), /** * Use for a person or organization who performs music or contributes to the musical content of a work when it is not possible or desirable to identify the function more precisely. */ MUSICIAN("mus", "Musician"), /** * Use for a person who is a speaker relating the particulars of an act, occurrence, or course of events. */ NARRATOR("nrt", "Narrator"), /** * Use for a person or organization responsible for opposing a thesis or dissertation. */ OPPONENT("opn", "Opponent"), /** * Use for a person or organization responsible for organizing a meeting for which an item is the report or proceedings. */ ORGANIZER_OF_MEETING("orm", "Organizer of meeting"), /** * Use for a person or organization performing the work, i.e., the name of a person or organization associated with the intellectual content of the work. This category does not include the publisher or personal affiliation, or sponsor except where it is also the corporate author. */ ORIGINATOR("org", "Originator"), /** * Use for relator codes from other lists which have no equivalent in the MARC list or for terms which have not been assigned a code. */ OTHER("oth", "Other"), /** * Use for a person or organization that currently owns an item or collection. */ OWNER("own", "Owner"), /** * Use for a person or organization responsible for the production of paper, usually from wood, cloth, or other fibrous material. */ PAPERMAKER("ppm", "Papermaker"), /** * Use for a person or organization that applied for a patent. */ PATENT_APPLICANT("pta", "Patent applicant"), /** * Use for a person or organization that was granted the patent referred to by the item. */ PATENT_HOLDER("pth", "Patent holder"), /** * Use for a person or organization responsible for commissioning a work. Usually a patron uses his or her means or influence to support the work of artists, writers, etc. This includes those who commission and pay for individual works. */ PATRON("pat", "Patron"), /** * Use for a person or organization who exhibits musical or acting skills in a musical or dramatic presentation or entertainment, if specific codes for those functions ([act], [dnc], [itr], [voc], etc.) are not used. If specific codes are used, [prf] is used for a person whose principal skill is not known or specified. */ PERFORMER("prf", "Performer"), /** * Use for an authority (usually a government agency) that issues permits under which work is accomplished. */ PERMITTING_AGENCY("pma", "Permitting agency"), /** * Use for a person or organization responsible for taking photographs, whether they are used in their original form or as reproductions. */ PHOTOGRAPHER("pht", "Photographer"), /** * Use for the party who complains or sues in court in a personal action, usually in a legal proceeding. */ PLAINTIFF("ptf", "Plaintiff"), /** * Use for a plaintiff who takes an appeal from one court or jurisdiction to another to reverse the judgment, usually in a legal proceeding. */ PLAINTIFF_APPELLANT("ptt", "Plaintiff-appellant"), /** * Use for a plaintiff against whom an appeal is taken from one court or jurisdiction to another to reverse the judgment, usually in a legal proceeding. */ PLAINTIFF_APPELLEE("pte", "Plaintiff-appellee"), /** * Use for a person or organization responsible for the production of plates, usually for the production of printed images and/or text. */ PLATEMAKER("plt", "Platemaker"), /** * Use for a person or organization who prints texts, whether from type or plates. */ PRINTER("prt", "Printer"), /** * Use for a person or organization who prints illustrations from plates. */ PRINTER_OF_PLATES("pop", "Printer of plates"), /** * Use for a person or organization who makes a relief, intaglio, or planographic printing surface. */ PRINTMAKER("prm", "Printmaker"), /** * Use for a person or organization primarily responsible for performing or initiating a process, such as is done with the collection of metadata sets. */ PROCESS_CONTACT("prc", "Process contact"), /** * Use for a person or organization responsible for the making of a motion picture, including business aspects, management of the productions, and the commercial success of the work. */ PRODUCER("pro", "Producer"), /** * Use for a person responsible for all technical and business matters in a production. */ PRODUCTION_MANAGER("pmn", "Production manager"), /** * Use for a person or organization associated with the production (props, lighting, special effects, etc.) of a musical or dramatic presentation or entertainment. */ PRODUCTION_PERSONNEL("prd", "Production personnel"), /** * Use for a person or organization responsible for the creation and/or maintenance of computer program design documents, source code, and machine-executable digital files and supporting documentation. */ PROGRAMMER("prg", "Programmer"), /** * Use for a person or organization with primary responsibility for all essential aspects of a project, or that manages a very large project that demands senior level responsibility, or that has overall responsibility for managing projects, or provides overall direction to a project manager. */ PROJECT_DIRECTOR("pdr", "Project director"), /** * Use for a person who corrects printed matter. For manuscripts, use Corrector [crr]. */ PROOFREADER("pfr", "Proofreader"), /** * Use for the name of the place where a resource is published. */ PUBLICATION_PLACE("pup", "Publication place"), /** * Use for a person or organization that makes printed matter, often text, but also printed music, artwork, etc. available to the public. */ PUBLISHER("pbl", "Publisher"), /** * Use for a person or organization who presides over the elaboration of a collective work to ensure its coherence or continuity. This includes editors-in-chief, literary editors, editors of series, etc. */ PUBLISHING_DIRECTOR("pbd", "Publishing director"), /** * Use for a person or organization who manipulates, controls, or directs puppets or marionettes in a musical or dramatic presentation or entertainment. */ PUPPETEER("ppt", "Puppeteer"), /** * Use for a person or organization to whom correspondence is addressed. */ RECIPIENT("rcp", "Recipient"), /** * Use for a person or organization who supervises the technical aspects of a sound or video recording session. */ RECORDING_ENGINEER("rce", "Recording engineer"), /** * Use for a person or organization who writes or develops the framework for an item without being intellectually responsible for its content. */ REDACTOR("red", "Redactor"), /** * Use for a person or organization who prepares drawings of architectural designs (i.e., renderings) in accurate, representational perspective to show what the project will look like when completed. */ RENDERER("ren", "Renderer"), /** * Use for a person or organization who writes or presents reports of news or current events on air or in print. */ REPORTER("rpt", "Reporter"), /** * Use for an agency that hosts data or material culture objects and provides services to promote long term, consistent and shared use of those data or objects. */ REPOSITORY("rps", "Repository"), /** * Use for a person who directed or managed a research project. */ RESEARCH_TEAM_HEAD("rth", "Research team head"), /** * Use for a person who participated in a research project but whose role did not involve direction or management of it. */ RESEARCH_TEAM_MEMBER("rtm", "Research team member"), /** * Use for a person or organization responsible for performing research. */ RESEARCHER("res", "Researcher"), /** * Use for the party who makes an answer to the courts pursuant to an application for redress, usually in an equity proceeding. */ RESPONDENT("rsp", "Respondent"), /** * Use for a respondent who takes an appeal from one court or jurisdiction to another to reverse the judgment, usually in an equity proceeding. */ RESPONDENT_APPELLANT("rst", "Respondent-appellant"), /** * Use for a respondent against whom an appeal is taken from one court or jurisdiction to another to reverse the judgment, usually in an equity proceeding. */ RESPONDENT_APPELLEE("rse", "Respondent-appellee"), /** * Use for a person or organization legally responsible for the content of the published material. */ RESPONSIBLE_PARTY("rpy", "Responsible party"), /** * Use for a person or organization, other than the original choreographer or director, responsible for restaging a choreographic or dramatic work and who contributes minimal new content. */ RESTAGER("rsg", "Restager"), /** * Use for a person or organization responsible for the review of a book, motion picture, performance, etc. */ REVIEWER("rev", "Reviewer"), /** * Use for a person or organization responsible for parts of a work, often headings or opening parts of a manuscript, that appear in a distinctive color, usually red. */ RUBRICATOR("rbr", "Rubricator"), /** * Use for a person or organization who is the author of a motion picture screenplay. */ SCENARIST("sce", "Scenarist"), /** * Use for a person or organization who brings scientific, pedagogical, or historical competence to the conception and realization on a work, particularly in the case of audio-visual items. */ SCIENTIFIC_ADVISOR("sad", "Scientific advisor"), /** * Use for a person who is an amanuensis and for a writer of manuscripts proper. For a person who makes pen-facsimiles, use Facsimilist [fac]. */ SCRIBE("scr", "Scribe"), /** * Use for a person or organization who models or carves figures that are three-dimensional representations. */ SCULPTOR("scl", "Sculptor"), /** * Use for a person or organization who is identified as the party of the second part. In the case of transfer of right, this is the assignee, transferee, licensee, grantee, etc. Multiple parties can be named jointly as the second party. */ SECOND_PARTY("spy", "Second party"), /** * Use for a person or organization who is a recorder, redactor, or other person responsible for expressing the views of a organization. */ SECRETARY("sec", "Secretary"), /** * Use for a person or organization who translates the rough sketches of the art director into actual architectural structures for a theatrical presentation, entertainment, motion picture, etc. Set designers draw the detailed guides and specifications for building the set. */ SET_DESIGNER("std", "Set designer"), /** * Use for a person whose signature appears without a presentation or other statement indicative of provenance. When there is a presentation statement, use Inscriber [ins]. */ SIGNER("sgn", "Signer"), /** * Use for a person or organization who uses his/her/their voice with or without instrumental accompaniment to produce music. A performance may or may not include actual words. */ SINGER("sng", "Singer"), /** * Use for a person who produces and reproduces the sound score (both live and recorded), the installation of microphones, the setting of sound levels, and the coordination of sources of sound for a production. */ SOUND_DESIGNER("sds", "Sound designer"), /** * Use for a person who participates in a program (often broadcast) and makes a formalized contribution or presentation generally prepared in advance. */ SPEAKER("spk", "Speaker"), /** * Use for a person or organization that issued a contract or under the auspices of which a work has been written, printed, published, etc. */ SPONSOR("spn", "Sponsor"), /** * Use for a person who is in charge of everything that occurs on a performance stage, and who acts as chief of all crews and assistant to a director during rehearsals. */ STAGE_MANAGER("stm", "Stage manager"), /** * Use for an organization responsible for the development or enforcement of a standard. */ STANDARDS_BODY("stn", "Standards body"), /** * Use for a person or organization who creates a new plate for printing by molding or copying another printing surface. */ STEREOTYPER("str", "Stereotyper"), /** * Use for a person relaying a story with creative and/or theatrical interpretation. */ STORYTELLER("stl", "Storyteller"), /** * Use for a person or organization that supports (by allocating facilities, staff, or other resources) a project, program, meeting, event, data objects, material culture objects, or other entities capable of support. */ SUPPORTING_HOST("sht", "Supporting host"), /** * Use for a person or organization who does measurements of tracts of land, etc. to determine location, forms, and boundaries. */ SURVEYOR("srv", "Surveyor"), /** * Use for a person who, in the context of a resource, gives instruction in an intellectual subject or demonstrates while teaching physical skills. */ TEACHER("tch", "Teacher"), /** * Use for a person who is ultimately in charge of scenery, props, lights and sound for a production. */ TECHNICAL_DIRECTOR("tcd", "Technical director"), /** * Use for a person under whose supervision a degree candidate develops and presents a thesis, mémoire, or text of a dissertation. */ THESIS_ADVISOR("ths", "Thesis advisor"), /** * Use for a person who prepares a handwritten or typewritten copy from original material, including from dictated or orally recorded material. For makers of pen-facsimiles, use Facsimilist [fac]. */ TRANSCRIBER("trc", "Transcriber"), /** * Use for a person or organization who renders a text from one language into another, or from an older form of a language into the modern form. */ TRANSLATOR("trl", "Translator"), /** * Use for a person or organization who designed the type face used in a particular item. */ TYPE_DESIGNER("tyd", "Type designer"), /** * Use for a person or organization primarily responsible for choice and arrangement of type used in an item. If the typographer is also responsible for other aspects of the graphic design of a book (e.g., Book designer [bkd]), codes for both functions may be needed. */ TYPOGRAPHER("tyg", "Typographer"), /** * Use for the name of a place where a university that is associated with a resource is located, for example, a university where an academic dissertation or thesis was presented. */ UNIVERSITY_PLACE("uvp", "University place"), /** * Use for a person or organization in charge of a video production, e.g. the video recording of a stage production as opposed to a commercial motion picture. The videographer may be the camera operator or may supervise one or more camera operators. Do not confuse with cinematographer. */ VIDEOGRAPHER("vdg", "Videographer"), /** * Use for a person or organization who principally exhibits singing skills in a musical or dramatic presentation or entertainment. */ VOCALIST("voc", "Vocalist"), /** * Use for a person who verifies the truthfulness of an event or action. */ WITNESS("wit", "Witness"), /** * Use for a person or organization who makes prints by cutting the image in relief on the end-grain of a wood block. */ WOOD_ENGRAVER("wde", "Wood-engraver"), /** * Use for a person or organization who makes prints by cutting the image in relief on the plank side of a wood block. */ WOODCUTTER("wdc", "Woodcutter"), /** * Use for a person or organization who writes significant material which accompanies a sound recording or other audiovisual material. */ WRITER_OF_ACCOMPANYING_MATERIAL("wam", "Writer of accompanying material"); private final String code; private final String name; Relator(String code, String name) { this.code = code; this.name = name; } public String getCode() { return code; } public String getName() { return name; } public static Relator byCode(String code) { for (Relator relator : Relator.values()) { if (relator.getCode().equalsIgnoreCase(code)) { return relator; } } return null; } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/Resource.java ================================================ package me.ag2s.epublib.domain; import me.ag2s.epublib.Constants; import me.ag2s.epublib.util.IOUtil; import me.ag2s.epublib.util.StringUtil; import me.ag2s.epublib.util.commons.io.XmlStreamReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.Serializable; /** * Represents a resource that is part of the epub. * A resource can be a html file, image, xml, etc. * * @author paul * */ public class Resource implements Serializable { private static final long serialVersionUID = 1043946707835004037L; private String id; private String title; private String href; private String properties; protected final String originalHref; private MediaType mediaType; private String inputEncoding; protected byte[] data; /** * Creates an empty Resource with the given href. * * Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8 * * @param href The location of the resource within the epub. Example: "chapter1.html". */ public Resource(String href) { this(null, new byte[0], href, MediaTypes.determineMediaType(href)); } /** * Creates a Resource with the given data and MediaType. * The href will be automatically generated. * * Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8 * * @param data The Resource's contents * @param mediaType The MediaType of the Resource */ public Resource(byte[] data, MediaType mediaType) { this(null, data, null, mediaType); } /** * Creates a resource with the given data at the specified href. * The MediaType will be determined based on the href extension. * * Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8 * * @see MediaTypes#determineMediaType(String) * * @param data The Resource's contents * @param href The location of the resource within the epub. Example: "chapter1.html". */ public Resource(byte[] data, String href) { this(null, data, href, MediaTypes.determineMediaType(href), Constants.CHARACTER_ENCODING); } /** * Creates a resource with the data from the given Reader at the specified href. * The MediaType will be determined based on the href extension. * * @see MediaTypes#determineMediaType(String) * * @param in The Resource's contents * @param href The location of the resource within the epub. Example: "cover.jpg". */ public Resource(Reader in, String href) throws IOException { this(null, IOUtil.toByteArray(in, Constants.CHARACTER_ENCODING), href, MediaTypes.determineMediaType(href), Constants.CHARACTER_ENCODING); } /** * Creates a resource with the data from the given InputStream at the specified href. * The MediaType will be determined based on the href extension. * * @see MediaTypes#determineMediaType(String) * * Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8 * * It is recommended to us the {@link #Resource(Reader, String)} method for creating textual * (html/css/etc) resources to prevent encoding problems. * Use this method only for binary Resources like images, fonts, etc. * * * @param in The Resource's contents * @param href The location of the resource within the epub. Example: "cover.jpg". */ public Resource(InputStream in, String href) throws IOException { this(null, IOUtil.toByteArray(in), href, MediaTypes.determineMediaType(href)); } /** * Creates a resource with the given id, data, mediatype at the specified href. * Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8 * * @param id The id of the Resource. Internal use only. Will be auto-generated if it has a null-value. * @param data The Resource's contents * @param href The location of the resource within the epub. Example: "chapter1.html". * @param mediaType The resources MediaType */ public Resource(String id, byte[] data, String href, MediaType mediaType) { this(id, data, href, mediaType, Constants.CHARACTER_ENCODING); } public Resource(String id, byte[] data, String href, String originalHref, MediaType mediaType) { this(id, data, href, originalHref, mediaType, Constants.CHARACTER_ENCODING); } /** * Creates a resource with the given id, data, mediatype at the specified href. * If the data is of a text type (html/css/etc) then it will use the given inputEncoding. * * @param id The id of the Resource. Internal use only. Will be auto-generated if it has a null-value. * @param data The Resource's contents * @param href The location of the resource within the epub. Example: "chapter1.html". * @param mediaType The resources MediaType * @param inputEncoding If the data is of a text type (html/css/etc) then it will use the given inputEncoding. */ public Resource(String id, byte[] data, String href, MediaType mediaType, String inputEncoding) { this.id = id; this.href = href; this.originalHref = href; this.mediaType = mediaType; this.inputEncoding = inputEncoding; this.data = data; } public Resource(String id, byte[] data, String href, String originalHref, MediaType mediaType, String inputEncoding) { this.id = id; this.href = href; this.originalHref = originalHref; this.mediaType = mediaType; this.inputEncoding = inputEncoding; this.data = data; } /** * Gets the contents of the Resource as an InputStream. * * @return The contents of the Resource. * * @throws IOException IOException */ public InputStream getInputStream() throws IOException { return new ByteArrayInputStream(getData()); } /** * The contents of the resource as a byte[] * * @return The contents of the resource */ public byte[] getData() throws IOException { return data; } /** * Tells this resource to release its cached data. * * If this resource was not lazy-loaded, this is a no-op. */ public void close() { } /** * Sets the data of the Resource. * If the data is a of a different type then the original data then make sure to change the MediaType. * * @param data the data of the Resource */ public void setData(byte[] data) { this.data = data; } /** * Returns the size of this resource in bytes. * * @return the size. */ public long getSize() { return data.length; } /** * If the title is found by scanning the underlying html document then it is cached here. * * @return the title */ public String getTitle() { return title; } /** * Sets the Resource's id: Make sure it is unique and a valid identifier. * * @param id Resource's id */ public void setId(String id) { this.id = id; } /** * The resources Id. * * Must be both unique within all the resources of this book and a valid identifier. * @return The resources Id. */ public String getId() { return id; } /** * The location of the resource within the contents folder of the epub file. * * Example:
* images/cover.jpg
* content/chapter1.xhtml
* * @return The location of the resource within the contents folder of the epub file. */ public String getHref() { return href; } /** * Sets the Resource's href. * * @param href Resource's href. */ public void setHref(String href) { this.href = href; } /** * The character encoding of the resource. * Is allowed to be null for non-text resources like images. * * @return The character encoding of the resource. */ public String getInputEncoding() { return inputEncoding; } /** * Sets the Resource's input character encoding. * * @param encoding Resource's input character encoding. */ public void setInputEncoding(String encoding) { this.inputEncoding = encoding; } /** * Gets the contents of the Resource as Reader. * * Does all sorts of smart things (courtesy of apache commons io XMLStreamREader) to handle encodings, byte order markers, etc. * * @return the contents of the Resource as Reader. * @throws IOException IOException */ public Reader getReader() throws IOException { return new XmlStreamReader(new ByteArrayInputStream(getData()), getInputEncoding()); } /** * Gets the hashCode of the Resource's href. * */ public int hashCode() { return href.hashCode(); } /** * Checks to see of the given resourceObject is a resource and whether its href is equal to this one. * * @return whether the given resourceObject is a resource and whether its href is equal to this one. */ public boolean equals(Object resourceObject) { if (!(resourceObject instanceof Resource)) { return false; } return href.equals(((Resource) resourceObject).getHref()); } /** * This resource's mediaType. * * @return This resource's mediaType. */ public MediaType getMediaType() { return mediaType; } public void setMediaType(MediaType mediaType) { this.mediaType = mediaType; } public void setTitle(String title) { this.title = title; } public String getProperties() { return properties; } public void setProperties(String properties) { this.properties = properties; } @SuppressWarnings("NullableProblems") public String toString() { return StringUtil.toString("id", id, "title", title, "encoding", inputEncoding, "mediaType", mediaType, "href", href, "size", (data == null ? 0 : data.length)); } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/ResourceInputStream.java ================================================ package me.ag2s.epublib.domain; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.util.zip.ZipFile; /** * A wrapper class for closing a ZipFile object when the InputStream derived * from it is closed. * * @author ttopalov */ public class ResourceInputStream extends FilterInputStream { private final ZipFile zipFile; /** * Constructor. * * @param in * The InputStream object. * @param zipFile * The ZipFile object. */ public ResourceInputStream(InputStream in, ZipFile zipFile) { super(in); this.zipFile = zipFile; } @Override public void close() throws IOException { super.close(); zipFile.close(); } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/ResourceReference.java ================================================ package me.ag2s.epublib.domain; import java.io.Serializable; public class ResourceReference implements Serializable { private static final long serialVersionUID = 2596967243557743048L; protected Resource resource; public ResourceReference(Resource resource) { this.resource = resource; } public Resource getResource() { return resource; } /** * Besides setting the resource it also sets the fragmentId to null. * * @param resource resource */ public void setResource(Resource resource) { this.resource = resource; } /** * The id of the reference referred to. * * null of the reference is null or has a null id itself. * * @return The id of the reference referred to. */ public String getResourceId() { if (resource != null) { return resource.getId(); } return null; } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/Resources.java ================================================ package me.ag2s.epublib.domain; import me.ag2s.epublib.Constants; import me.ag2s.epublib.util.StringUtil; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; /** * All the resources that make up the book. * XHTML files, images and epub xml documents must be here. * * @author paul */ public class Resources implements Serializable { private static final long serialVersionUID = 2450876953383871451L; private static final String IMAGE_PREFIX = "image_"; private static final String ITEM_PREFIX = "item_"; private int lastId = 1; private Map resources = new HashMap<>(); /** * Adds a resource to the resources. *

* Fixes the resources id and href if necessary. * * @param resource resource * @return the newly added resource */ public Resource add(Resource resource) { fixResourceHref(resource); fixResourceId(resource); this.resources.put(resource.getHref(), resource); return resource; } /** * Checks the id of the given resource and changes to a unique identifier if it isn't one already. * * @param resource resource */ public void fixResourceId(Resource resource) { String resourceId = resource.getId(); // first try and create a unique id based on the resource's href if (StringUtil.isBlank(resource.getId())) { resourceId = StringUtil.substringBeforeLast(resource.getHref(), '.'); resourceId = StringUtil.substringAfterLast(resourceId, '/'); } resourceId = makeValidId(resourceId, resource); // check if the id is unique. if not: create one from scratch if (StringUtil.isBlank(resourceId) || containsId(resourceId)) { resourceId = createUniqueResourceId(resource); } resource.setId(resourceId); } /** * Check if the id is a valid identifier. if not: prepend with valid identifier * * @param resource resource * @return a valid id */ private String makeValidId(String resourceId, Resource resource) { if (StringUtil.isNotBlank(resourceId) && !Character .isJavaIdentifierStart(resourceId.charAt(0))) { resourceId = getResourceItemPrefix(resource) + resourceId; } return resourceId; } private String getResourceItemPrefix(Resource resource) { String result; if (MediaTypes.isBitmapImage(resource.getMediaType())) { result = IMAGE_PREFIX; } else { result = ITEM_PREFIX; } return result; } /** * Creates a new resource id that is guaranteed to be unique for this set of Resources * * @param resource resource * @return a new resource id that is guaranteed to be unique for this set of Resources */ private String createUniqueResourceId(Resource resource) { int counter = lastId; if (counter == Integer.MAX_VALUE) { if (resources.size() == Integer.MAX_VALUE) { throw new IllegalArgumentException( "Resources contains " + Integer.MAX_VALUE + " elements: no new elements can be added"); } else { counter = 1; } } String prefix = getResourceItemPrefix(resource); String result = prefix + counter; while (containsId(result)) { result = prefix + (++counter); } lastId = counter; return result; } /** * Whether the map of resources already contains a resource with the given id. * * @param id id * @return Whether the map of resources already contains a resource with the given id. */ public boolean containsId(String id) { if (StringUtil.isBlank(id)) { return false; } for (Resource resource : resources.values()) { if (id.equals(resource.getId())) { return true; } } return false; } /** * Gets the resource with the given id. * * @param id id * @return null if not found */ public Resource getById(String id) { if (StringUtil.isBlank(id)) { return null; } for (Resource resource : resources.values()) { if (id.equals(resource.getId())) { return resource; } } return null; } public Resource getByProperties(String properties) { if (StringUtil.isBlank(properties)) { return null; } for (Resource resource : resources.values()) { if (properties.equals(resource.getProperties())) { return resource; } } return null; } /** * Remove the resource with the given href. * * @param href href * @return the removed resource, null if not found */ public Resource remove(String href) { return resources.remove(href); } private void fixResourceHref(Resource resource) { if (StringUtil.isNotBlank(resource.getHref()) && !resources.containsKey(resource.getHref())) { return; } if (StringUtil.isBlank(resource.getHref())) { if (resource.getMediaType() == null) { throw new IllegalArgumentException( "Resource must have either a MediaType or a href"); } int i = 1; String href = createHref(resource.getMediaType(), i); while (resources.containsKey(href)) { href = createHref(resource.getMediaType(), (++i)); } resource.setHref(href); } } private String createHref(MediaType mediaType, int counter) { if (MediaTypes.isBitmapImage(mediaType)) { return IMAGE_PREFIX + counter + mediaType.getDefaultExtension(); } else { return ITEM_PREFIX + counter + mediaType.getDefaultExtension(); } } public boolean isEmpty() { return resources.isEmpty(); } /** * The number of resources * * @return The number of resources */ public int size() { return resources.size(); } /** * The resources that make up this book. * Resources can be xhtml pages, images, xml documents, etc. * * @return The resources that make up this book. */ @SuppressWarnings("unused") public Map getResourceMap() { return resources; } public Collection getAll() { return resources.values(); } /** * Whether there exists a resource with the given href * * @param href href * @return Whether there exists a resource with the given href */ public boolean notContainsByHref(String href) { if (StringUtil.isBlank(href)) { return true; } else { return !resources.containsKey( StringUtil.substringBefore(href, Constants.FRAGMENT_SEPARATOR_CHAR)); } } /** * Whether there exists a resource with the given href * * @param href href * @return Whether there exists a resource with the given href */ @SuppressWarnings("unused") public boolean containsByHref(String href) { return !notContainsByHref(href); } /** * Sets the collection of Resources to the given collection of resources * * @param resources resources */ public void set(Collection resources) { this.resources.clear(); addAll(resources); } /** * Adds all resources from the given Collection of resources to the existing collection. * * @param resources resources */ public void addAll(Collection resources) { for (Resource resource : resources) { fixResourceHref(resource); this.resources.put(resource.getHref(), resource); } } /** * Sets the collection of Resources to the given collection of resources * * @param resources A map with as keys the resources href and as values the Resources */ public void set(Map resources) { this.resources = new HashMap<>(resources); } /** * First tries to find a resource with as id the given idOrHref, if that * fails it tries to find one with the idOrHref as href. * * @param idOrHref idOrHref * @return the found Resource */ public Resource getByIdOrHref(String idOrHref) { Resource resource = getById(idOrHref); if (resource == null) { resource = getByHref(idOrHref); } return resource; } /** * Gets the resource with the given href. * If the given href contains a fragmentId then that fragment id will be ignored. * * @param href href * @return null if not found. */ public Resource getByHref(String href) { if (StringUtil.isBlank(href)) { return null; } href = StringUtil.substringBefore(href, Constants.FRAGMENT_SEPARATOR_CHAR); return resources.get(href); } /** * Gets the first resource (random order) with the give mediatype. *

* Useful for looking up the table of contents as it's supposed to be the only resource with NCX mediatype. * * @param mediaType mediaType * @return the first resource (random order) with the give mediatype. */ public Resource findFirstResourceByMediaType(MediaType mediaType) { return findFirstResourceByMediaType(resources.values(), mediaType); } /** * Gets the first resource (random order) with the give mediatype. *

* Useful for looking up the table of contents as it's supposed to be the only resource with NCX mediatype. * * @param mediaType mediaType * @return the first resource (random order) with the give mediatype. */ public static Resource findFirstResourceByMediaType( Collection resources, MediaType mediaType) { for (Resource resource : resources) { if (resource.getMediaType() == mediaType) { return resource; } } return null; } /** * All resources that have the given MediaType. * * @param mediaType mediaType * @return All resources that have the given MediaType. */ public List getResourcesByMediaType(MediaType mediaType) { List result = new ArrayList<>(); if (mediaType == null) { return result; } for (Resource resource : getAll()) { if (resource.getMediaType() == mediaType) { result.add(resource); } } return result; } /** * All Resources that match any of the given list of MediaTypes * * @param mediaTypes mediaType * @return All Resources that match any of the given list of MediaTypes */ @SuppressWarnings("unused") public List getResourcesByMediaTypes(MediaType[] mediaTypes) { List result = new ArrayList<>(); if (mediaTypes == null) { return result; } // this is the fastest way of doing this according to // http://stackoverflow.com/questions/1128723/in-java-how-can-i-test-if-an-array-contains-a-certain-value List mediaTypesList = Arrays.asList(mediaTypes); for (Resource resource : getAll()) { if (mediaTypesList.contains(resource.getMediaType())) { result.add(resource); } } return result; } /** * All resource hrefs * * @return all resource hrefs */ public Collection getAllHrefs() { return resources.keySet(); } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/Spine.java ================================================ package me.ag2s.epublib.domain; import me.ag2s.epublib.util.StringUtil; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * The spine sections are the sections of the book in the order in which the book should be read. * * This contrasts with the Table of Contents sections which is an index into the Book's sections. * * @see TableOfContents * * @author paul */ public class Spine implements Serializable { private static final long serialVersionUID = 3878483958947357246L; private Resource tocResource; private List spineReferences; public Spine() { this(new ArrayList<>()); } /** * Creates a spine out of all the resources in the table of contents. * * @param tableOfContents tableOfContents */ public Spine(TableOfContents tableOfContents) { this.spineReferences = createSpineReferences( tableOfContents.getAllUniqueResources()); } public Spine(List spineReferences) { this.spineReferences = spineReferences; } public static List createSpineReferences( Collection resources) { List result = new ArrayList<>( resources.size()); for (Resource resource : resources) { result.add(new SpineReference(resource)); } return result; } public List getSpineReferences() { return spineReferences; } public void setSpineReferences(List spineReferences) { this.spineReferences = spineReferences; } /** * Gets the resource at the given index. * Null if not found. * * @param index index * @return the resource at the given index. */ public Resource getResource(int index) { if (index < 0 || index >= spineReferences.size()) { return null; } return spineReferences.get(index).getResource(); } /** * Finds the first resource that has the given resourceId. * * Null if not found. * * @param resourceId resourceId * @return the first resource that has the given resourceId. */ public int findFirstResourceById(String resourceId) { if (StringUtil.isBlank(resourceId)) { return -1; } for (int i = 0; i < spineReferences.size(); i++) { SpineReference spineReference = spineReferences.get(i); if (resourceId.equals(spineReference.getResourceId())) { return i; } } return -1; } /** * Adds the given spineReference to the spine references and returns it. * * @param spineReference spineReference * @return the given spineReference */ public SpineReference addSpineReference(SpineReference spineReference) { if (spineReferences == null) { this.spineReferences = new ArrayList<>(); } spineReferences.add(spineReference); return spineReference; } /** * Adds the given resource to the spine references and returns it. * * @return the given spineReference */ @SuppressWarnings("unused") public SpineReference addResource(Resource resource) { return addSpineReference(new SpineReference(resource)); } /** * The number of elements in the spine. * * @return The number of elements in the spine. */ public int size() { return spineReferences.size(); } /** * As per the epub file format the spine officially maintains a reference to the Table of Contents. * The epubwriter will look for it here first, followed by some clever tricks to find it elsewhere if not found. * Put it here to be sure of the expected behaviours. * * @param tocResource tocResource */ public void setTocResource(Resource tocResource) { this.tocResource = tocResource; } /** * The resource containing the XML for the tableOfContents. * When saving an epub file this resource needs to be in this place. * * @return The resource containing the XML for the tableOfContents. */ public Resource getTocResource() { return tocResource; } /** * The position within the spine of the given resource. * * @param currentResource currentResource * @return something < 0 if not found. * */ public int getResourceIndex(Resource currentResource) { if (currentResource == null) { return -1; } return getResourceIndex(currentResource.getHref()); } /** * The first position within the spine of a resource with the given href. * * @return something < 0 if not found. * */ public int getResourceIndex(String resourceHref) { int result = -1; if (StringUtil.isBlank(resourceHref)) { return result; } for (int i = 0; i < spineReferences.size(); i++) { if (resourceHref.equals(spineReferences.get(i).getResource().getHref())) { result = i; break; } } return result; } /** * Whether the spine has any references * @return Whether the spine has any references */ public boolean isEmpty() { return spineReferences.isEmpty(); } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/SpineReference.java ================================================ package me.ag2s.epublib.domain; import java.io.Serializable; /** * A Section of a book. * Represents both an item in the package document and a item in the index. * * @author paul */ public class SpineReference extends ResourceReference implements Serializable { private static final long serialVersionUID = -7921609197351510248L; private boolean linear;//default = true; public SpineReference(Resource resource) { this(resource, true); } public SpineReference(Resource resource, boolean linear) { super(resource); this.linear = linear; } /** * Linear denotes whether the section is Primary or Auxiliary. * Usually the cover page has linear set to false and all the other sections * have it set to true. *

* It's an optional property that readers may also ignore. * *

primary or auxiliary is useful for Reading Systems which * opt to present auxiliary content differently than primary content. * For example, a Reading System might opt to render auxiliary content in * a popup window apart from the main window which presents the primary * content. (For an example of the types of content that may be considered * auxiliary, refer to the example below and the subsequent discussion.)
* * @return whether the section is Primary or Auxiliary. * @see OPF Spine specification */ public boolean isLinear() { return linear; } public void setLinear(boolean linear) { this.linear = linear; } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/TOCReference.java ================================================ package me.ag2s.epublib.domain; import java.io.Serializable; import java.util.ArrayList; import java.util.Comparator; import java.util.List; /** * An item in the Table of Contents. * * @see TableOfContents * * @author paul */ public class TOCReference extends TitledResourceReference implements Serializable { private static final long serialVersionUID = 5787958246077042456L; private List children; private static final Comparator COMPARATOR_BY_TITLE_IGNORE_CASE = (tocReference1, tocReference2) -> String.CASE_INSENSITIVE_ORDER.compare(tocReference1.getTitle(), tocReference2.getTitle()); @Deprecated public TOCReference() { this(null, null, null); } public TOCReference(String name, Resource resource) { this(name, resource, null); } public TOCReference(String name, Resource resource, String fragmentId) { this(name, resource, fragmentId, new ArrayList<>()); } public TOCReference(String title, Resource resource, String fragmentId, List children) { super(resource, title, fragmentId); this.children = children; } @SuppressWarnings("unused") public static Comparator getComparatorByTitleIgnoreCase() { return COMPARATOR_BY_TITLE_IGNORE_CASE; } public List getChildren() { return children; } public TOCReference addChildSection(TOCReference childSection) { this.children.add(childSection); return childSection; } public void setChildren(List children) { this.children = children; } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/TableOfContents.java ================================================ package me.ag2s.epublib.domain; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; /** * The table of contents of the book. * The TableOfContents is a tree structure at the root it is a list of TOCReferences, each if which may have as children another list of TOCReferences. * * The table of contents is used by epub as a quick index to chapters and sections within chapters. * It may contain duplicate entries, may decide to point not to certain chapters, etc. * * See the spine for the complete list of sections in the order in which they should be read. * * @see Spine * * @author paul */ public class TableOfContents implements Serializable { private static final long serialVersionUID = -3147391239966275152L; public static final String DEFAULT_PATH_SEPARATOR = "/"; private List tocReferences; public TableOfContents() { this(new ArrayList<>()); } public TableOfContents(List tocReferences) { this.tocReferences = tocReferences; } public List getTocReferences() { return tocReferences; } public void setTocReferences(List tocReferences) { this.tocReferences = tocReferences; } /** * Calls addTOCReferenceAtLocation after splitting the path using the DEFAULT_PATH_SEPARATOR. * @return the new TOCReference */ @SuppressWarnings("unused") public TOCReference addSection(Resource resource, String path) { return addSection(resource, path, DEFAULT_PATH_SEPARATOR); } /** * Calls addTOCReferenceAtLocation after splitting the path using the given pathSeparator. * * @param resource resource * @param path path * @param pathSeparator pathSeparator * @return the new TOCReference */ public TOCReference addSection(Resource resource, String path, String pathSeparator) { String[] pathElements = path.split(pathSeparator); return addSection(resource, pathElements); } /** * Finds the first TOCReference in the given list that has the same title as the given Title. * * @param title title * @param tocReferences tocReferences * @return null if not found. */ private static TOCReference findTocReferenceByTitle(String title, List tocReferences) { for (TOCReference tocReference : tocReferences) { if (title.equals(tocReference.getTitle())) { return tocReference; } } return null; } /** * Adds the given Resources to the TableOfContents at the location specified by the pathElements. * * Example: * Calling this method with a Resource and new String[] {"chapter1", "paragraph1"} will result in the following: *
    *
  • a TOCReference with the title "chapter1" at the root level.
    * If this TOCReference did not yet exist it will have been created and does not point to any resource
  • *
  • A TOCReference that has the title "paragraph1". This TOCReference will be the child of TOCReference "chapter1" and * will point to the given Resource
  • *
* * @param resource resource * @param pathElements pathElements * @return the new TOCReference */ public TOCReference addSection(Resource resource, String[] pathElements) { if (pathElements == null || pathElements.length == 0) { return null; } TOCReference result = null; List currentTocReferences = this.tocReferences; for (String currentTitle : pathElements) { result = findTocReferenceByTitle(currentTitle, currentTocReferences); if (result == null) { result = new TOCReference(currentTitle, null); currentTocReferences.add(result); } currentTocReferences = result.getChildren(); } result.setResource(resource); return result; } /** * Adds the given Resources to the TableOfContents at the location specified by the pathElements. * * Example: * Calling this method with a Resource and new int[] {0, 0} will result in the following: *
    *
  • a TOCReference at the root level.
    * If this TOCReference did not yet exist it will have been created with a title of "" and does not point to any resource
  • *
  • A TOCReference that points to the given resource and is a child of the previously created TOCReference.
    * If this TOCReference didn't exist yet it will be created and have a title of ""
  • *
* * @param resource resource * @param pathElements pathElements * @return the new TOCReference */ @SuppressWarnings("unused") public TOCReference addSection(Resource resource, int[] pathElements, String sectionTitlePrefix, String sectionNumberSeparator) { if (pathElements == null || pathElements.length == 0) { return null; } TOCReference result = null; List currentTocReferences = this.tocReferences; for (int i = 0; i < pathElements.length; i++) { int currentIndex = pathElements[i]; if (currentIndex > 0 && currentIndex < (currentTocReferences.size() - 1)) { result = currentTocReferences.get(currentIndex); } else { result = null; } if (result == null) { paddTOCReferences(currentTocReferences, pathElements, i, sectionTitlePrefix, sectionNumberSeparator); result = currentTocReferences.get(currentIndex); } currentTocReferences = result.getChildren(); } result.setResource(resource); return result; } private void paddTOCReferences(List currentTocReferences, int[] pathElements, int pathPos, String sectionPrefix, String sectionNumberSeparator) { for (int i = currentTocReferences.size(); i <= pathElements[pathPos]; i++) { String sectionTitle = createSectionTitle(pathElements, pathPos, i, sectionPrefix, sectionNumberSeparator); currentTocReferences.add(new TOCReference(sectionTitle, null)); } } private String createSectionTitle(int[] pathElements, int pathPos, int lastPos, String sectionPrefix, String sectionNumberSeparator) { StringBuilder title = new StringBuilder(sectionPrefix); for (int i = 0; i < pathPos; i++) { if (i > 0) { title.append(sectionNumberSeparator); } title.append(pathElements[i] + 1); } if (pathPos > 0) { title.append(sectionNumberSeparator); } title.append(lastPos + 1); return title.toString(); } public TOCReference addTOCReference(TOCReference tocReference) { if (tocReferences == null) { tocReferences = new ArrayList<>(); } tocReferences.add(tocReference); return tocReference; } /** * All unique references (unique by href) in the order in which they are referenced to in the table of contents. * * @return All unique references (unique by href) in the order in which they are referenced to in the table of contents. */ public List getAllUniqueResources() { Set uniqueHrefs = new HashSet<>(); List result = new ArrayList<>(); getAllUniqueResources(uniqueHrefs, result, tocReferences); return result; } private static void getAllUniqueResources(Set uniqueHrefs, List result, List tocReferences) { for (TOCReference tocReference : tocReferences) { Resource resource = tocReference.getResource(); if (resource != null && !uniqueHrefs.contains(resource.getHref())) { uniqueHrefs.add(resource.getHref()); result.add(resource); } getAllUniqueResources(uniqueHrefs, result, tocReference.getChildren()); } } /** * The total number of references in this table of contents. * * @return The total number of references in this table of contents. */ public int size() { return getTotalSize(tocReferences); } private static int getTotalSize(Collection tocReferences) { int result = tocReferences.size(); for (TOCReference tocReference : tocReferences) { result += getTotalSize(tocReference.getChildren()); } return result; } /** * The maximum depth of the reference tree * @return The maximum depth of the reference tree */ public int calculateDepth() { return calculateDepth(tocReferences, 0); } private int calculateDepth(List tocReferences, int currentDepth) { int maxChildDepth = 0; for (TOCReference tocReference : tocReferences) { int childDepth = calculateDepth(tocReference.getChildren(), 1); if (childDepth > maxChildDepth) { maxChildDepth = childDepth; } } return currentDepth + maxChildDepth; } } ================================================ FILE: src/main/java/me/ag2s/epublib/domain/TitledResourceReference.java ================================================ package me.ag2s.epublib.domain; import java.io.Serializable; import me.ag2s.epublib.Constants; import me.ag2s.epublib.util.StringUtil; public class TitledResourceReference extends ResourceReference implements Serializable { private static final long serialVersionUID = 3918155020095190080L; private String fragmentId; private String title; /** * 这会使title为null * * @param resource resource */ @Deprecated @SuppressWarnings("unused") public TitledResourceReference(Resource resource) { this(resource, null); } public TitledResourceReference(Resource resource, String title) { this(resource, title, null); } public TitledResourceReference(Resource resource, String title, String fragmentId) { super(resource); this.title = title; this.fragmentId = fragmentId; } public String getFragmentId() { return fragmentId; } public void setFragmentId(String fragmentId) { this.fragmentId = fragmentId; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } /** * If the fragmentId is blank it returns the resource href, otherwise * it returns the resource href + '#' + the fragmentId. * * @return If the fragmentId is blank it returns the resource href, * otherwise it returns the resource href + '#' + the fragmentId. */ public String getCompleteHref() { if (StringUtil.isBlank(fragmentId)) { return resource.getHref(); } else { return resource.getHref() + Constants.FRAGMENT_SEPARATOR_CHAR + fragmentId; } } @Override public Resource getResource() { //resource为null时不设置标题 if(this.resource!=null&&this.title!=null){ resource.setTitle(title); } return resource; } public void setResource(Resource resource, String fragmentId) { super.setResource(resource); this.fragmentId = fragmentId; } /** * Sets the resource to the given resource and sets the fragmentId to null. */ public void setResource(Resource resource) { setResource(resource, null); } } ================================================ FILE: src/main/java/me/ag2s/epublib/epub/BookProcessor.java ================================================ package me.ag2s.epublib.epub; import me.ag2s.epublib.domain.EpubBook; /** * Post-processes a book. * * Can be used to clean up a book after reading or before writing. * * @author paul */ public interface BookProcessor { /** * A BookProcessor that returns the input book unchanged. */ BookProcessor IDENTITY_BOOKPROCESSOR = book -> book; EpubBook processBook(EpubBook book); } ================================================ FILE: src/main/java/me/ag2s/epublib/epub/BookProcessorPipeline.java ================================================ package me.ag2s.epublib.epub; import java.util.ArrayList; import java.util.Collection; import java.util.List; import me.ag2s.epublib.domain.EpubBook; /** * A book processor that combines several other bookprocessors *

* Fixes coverpage/coverimage. * Cleans up the XHTML. * * @author paul.siegmann */ @SuppressWarnings("unused declaration") public class BookProcessorPipeline implements BookProcessor { private static final String TAG= BookProcessorPipeline.class.getName(); private List bookProcessors; public BookProcessorPipeline() { this(null); } public BookProcessorPipeline(List bookProcessingPipeline) { this.bookProcessors = bookProcessingPipeline; } @Override public EpubBook processBook(EpubBook book) { if (bookProcessors == null) { return book; } for (BookProcessor bookProcessor : bookProcessors) { try { book = bookProcessor.processBook(book); } catch (Exception e) { // Log.e(TAG, e.getMessage(), e); e.printStackTrace(); } } return book; } public void addBookProcessor(BookProcessor bookProcessor) { if (this.bookProcessors == null) { bookProcessors = new ArrayList<>(); } this.bookProcessors.add(bookProcessor); } public void addBookProcessors(Collection bookProcessors) { if (this.bookProcessors == null) { this.bookProcessors = new ArrayList<>(); } this.bookProcessors.addAll(bookProcessors); } public List getBookProcessors() { return bookProcessors; } public void setBookProcessingPipeline( List bookProcessingPipeline) { this.bookProcessors = bookProcessingPipeline; } } ================================================ FILE: src/main/java/me/ag2s/epublib/epub/DOMUtil.java ================================================ package me.ag2s.epublib.epub; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; import java.util.ArrayList; import java.util.List; import me.ag2s.epublib.util.StringUtil; /** * Utility methods for working with the DOM. * * @author paul */ // package class DOMUtil { /** * First tries to get the attribute value by doing an getAttributeNS on the element, if that gets an empty element it does a getAttribute without namespace. * * @param element element * @param namespace namespace * @param attribute attribute * @return String Attribute */ public static String getAttribute(Element element, String namespace, String attribute) { String result = element.getAttributeNS(namespace, attribute); if (StringUtil.isEmpty(result)) { result = element.getAttribute(attribute); } return result; } /** * Gets all descendant elements of the given parentElement with the given namespace and tagname and returns their text child as a list of String. * * @param parentElement parentElement * @param namespace namespace * @param tagName tagName * @return List */ public static List getElementsTextChild(Element parentElement, String namespace, String tagName) { NodeList elements = parentElement .getElementsByTagNameNS(namespace, tagName); //ArrayList 初始化时指定长度提高性能 List result = new ArrayList<>(elements.getLength()); for (int i = 0; i < elements.getLength(); i++) { result.add(getTextChildrenContent((Element) elements.item(i))); } return result; } /** * Finds in the current document the first element with the given namespace and elementName and with the given findAttributeName and findAttributeValue. * It then returns the value of the given resultAttributeName. * * @param document document * @param namespace namespace * @param elementName elementName * @param findAttributeName findAttributeName * @param findAttributeValue findAttributeValue * @param resultAttributeName resultAttributeName * @return String value */ public static String getFindAttributeValue(Document document, String namespace, String elementName, String findAttributeName, String findAttributeValue, String resultAttributeName) { NodeList metaTags = document.getElementsByTagNameNS(namespace, elementName); for (int i = 0; i < metaTags.getLength(); i++) { Element metaElement = (Element) metaTags.item(i); if (findAttributeValue .equalsIgnoreCase(metaElement.getAttribute(findAttributeName)) && StringUtil .isNotBlank(metaElement.getAttribute(resultAttributeName))) { return metaElement.getAttribute(resultAttributeName); } } return null; } /** * Gets the first element that is a child of the parentElement and has the given namespace and tagName * * @param parentElement parentElement * @param namespace namespace * @param tagName tagName * @return Element */ public static NodeList getElementsByTagNameNS(Element parentElement, String namespace, String tagName) { NodeList nodes = parentElement.getElementsByTagNameNS(namespace, tagName); if (nodes.getLength() != 0) { return nodes; } nodes = parentElement.getElementsByTagName(tagName); if (nodes.getLength() == 0) { return null; } return nodes; } /** * Gets the first element that is a child of the parentElement and has the given namespace and tagName * * @param parentElement parentElement * @param namespace namespace * @param tagName tagName * @return Element */ public static NodeList getElementsByTagNameNS(Document parentElement, String namespace, String tagName) { NodeList nodes = parentElement.getElementsByTagNameNS(namespace, tagName); if (nodes.getLength() != 0) { return nodes; } nodes = parentElement.getElementsByTagName(tagName); if (nodes.getLength() == 0) { return null; } return nodes; } /** * Gets the first element that is a child of the parentElement and has the given namespace and tagName * * @param parentElement parentElement * @param namespace namespace * @param tagName tagName * @return Element */ public static Element getFirstElementByTagNameNS(Element parentElement, String namespace, String tagName) { NodeList nodes = parentElement.getElementsByTagNameNS(namespace, tagName); if (nodes.getLength() != 0) { return (Element) nodes.item(0); } nodes = parentElement.getElementsByTagName(tagName); if (nodes.getLength() == 0) { return null; } return (Element) nodes.item(0); } /** * The contents of all Text nodes that are children of the given parentElement. * The result is trim()-ed. *

* The reason for this more complicated procedure instead of just returning the data of the firstChild is that * when the text is Chinese characters then on Android each Characater is represented in the DOM as * an individual Text node. * * @param parentElement parentElement * @return String value */ public static String getTextChildrenContent(Element parentElement) { if (parentElement == null) { return null; } StringBuilder result = new StringBuilder(); NodeList childNodes = parentElement.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { Node node = childNodes.item(i); if ((node == null) || (node.getNodeType() != Node.TEXT_NODE)) { continue; } result.append(((Text) node).getData()); } return result.toString().trim(); } } ================================================ FILE: src/main/java/me/ag2s/epublib/epub/EpubProcessorSupport.java ================================================ package me.ag2s.epublib.epub; import me.ag2s.epublib.Constants; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.net.URL; import java.util.Objects; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlSerializer; /** * Various low-level support methods for reading/writing epubs. * * @author paul.siegmann */ public class EpubProcessorSupport { private static final String TAG = EpubProcessorSupport.class.getName(); protected static DocumentBuilderFactory documentBuilderFactory; static { init(); } static class EntityResolverImpl implements EntityResolver { private String previousLocation; @Override public InputSource resolveEntity(String publicId, String systemId) throws IOException { String resourcePath; if (systemId.startsWith("http:")) { URL url = new URL(systemId); resourcePath = "dtd/" + url.getHost() + url.getPath(); previousLocation = resourcePath .substring(0, resourcePath.lastIndexOf('/')); } else { resourcePath = previousLocation + systemId.substring(systemId.lastIndexOf('/')); } if (Objects.requireNonNull(this.getClass().getClassLoader()).getResource(resourcePath) == null) { throw new RuntimeException( "remote resource is not cached : [" + systemId + "] cannot continue"); } InputStream in = Objects.requireNonNull(EpubProcessorSupport.class.getClassLoader()) .getResourceAsStream(resourcePath); return new InputSource(in); } } private static void init() { EpubProcessorSupport.documentBuilderFactory = DocumentBuilderFactory .newInstance(); documentBuilderFactory.setNamespaceAware(true); documentBuilderFactory.setValidating(false); } public static XmlSerializer createXmlSerializer(OutputStream out) throws UnsupportedEncodingException { return createXmlSerializer( new OutputStreamWriter(out, Constants.CHARACTER_ENCODING)); } public static XmlSerializer createXmlSerializer(Writer out) { XmlSerializer result = null; try { /* * Disable XmlPullParserFactory here before it doesn't work when * building native image using GraalVM */ XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); factory.setValidating(true); result = factory.newSerializer(); //result = new KXmlSerializer(); result.setFeature( "http://xmlpull.org/v1/doc/features.html#indent-output", true); result.setOutput(out); } catch (Exception e) { e.printStackTrace(); // Log.e(TAG, // "When creating XmlSerializer: " + e.getClass().getName() + ": " + e // .getMessage()); } return result; } /** * Gets an EntityResolver that loads dtd's and such from the epub4j classpath. * In order to enable the loading of relative urls the given EntityResolver contains the previousLocation. * Because of a new EntityResolver is created every time this method is called. * Fortunately the EntityResolver created uses up very little memory per instance. * * @return an EntityResolver that loads dtd's and such from the epub4j classpath. */ public static EntityResolver getEntityResolver() { return new EntityResolverImpl(); } @SuppressWarnings("unused") public DocumentBuilderFactory getDocumentBuilderFactory() { return documentBuilderFactory; } /** * Creates a DocumentBuilder that looks up dtd's and schema's from epub4j's classpath. * * @return a DocumentBuilder that looks up dtd's and schema's from epub4j's classpath. */ public static DocumentBuilder createDocumentBuilder() { DocumentBuilder result = null; try { result = documentBuilderFactory.newDocumentBuilder(); result.setEntityResolver(getEntityResolver()); } catch (ParserConfigurationException e) { e.printStackTrace(); // Log.e(TAG, e.getMessage()); } return result; } } ================================================ FILE: src/main/java/me/ag2s/epublib/epub/EpubReader.java ================================================ package me.ag2s.epublib.epub; import org.w3c.dom.Document; import org.w3c.dom.Element; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.List; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; import me.ag2s.epublib.Constants; import me.ag2s.epublib.domain.EpubBook; import me.ag2s.epublib.domain.MediaType; import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.domain.Resources; import me.ag2s.epublib.util.ResourceUtil; import me.ag2s.epublib.util.StringUtil; /** * Reads an epub file. * * @author paul */ @SuppressWarnings("ALL") public class EpubReader { private static final String TAG = EpubReader.class.getName(); private final BookProcessor bookProcessor = BookProcessor.IDENTITY_BOOKPROCESSOR; public EpubBook readEpub(InputStream in) throws IOException { return readEpub(in, Constants.CHARACTER_ENCODING); } public EpubBook readEpub(ZipInputStream in) throws IOException { return readEpub(in, Constants.CHARACTER_ENCODING); } public EpubBook readEpub(ZipFile zipfile) throws IOException { return readEpub(zipfile, Constants.CHARACTER_ENCODING); } /** * Read epub from inputstream * * @param in the inputstream from which to read the epub * @param encoding the encoding to use for the html files within the epub * @return the Book as read from the inputstream * @throws IOException IOException */ public EpubBook readEpub(InputStream in, String encoding) throws IOException { return readEpub(new ZipInputStream(in), encoding); } /** * Reads this EPUB without loading any resources into memory. * * @param zipFile the file to load * @param encoding the encoding for XHTML files * @return this Book without loading all resources into memory. * @throws IOException IOException */ public EpubBook readEpubLazy(ZipFile zipFile, String encoding) throws IOException { return readEpubLazy(zipFile, encoding, Arrays.asList(MediaTypes.mediaTypes)); } public EpubBook readEpub(ZipInputStream in, String encoding) throws IOException { return readEpub(ResourcesLoader.loadResources(in, encoding)); } public EpubBook readEpub(ZipFile in, String encoding) throws IOException { return readEpub(ResourcesLoader.loadResources(in, encoding)); } /** * Reads this EPUB without loading all resources into memory. * * @param zipFile the file to load * @param encoding the encoding for XHTML files * @param lazyLoadedTypes a list of the MediaType to load lazily * @return this Book without loading all resources into memory. * @throws IOException IOException */ public EpubBook readEpubLazy(ZipFile zipFile, String encoding, List lazyLoadedTypes) throws IOException { Resources resources = ResourcesLoader .loadResources(zipFile, encoding, lazyLoadedTypes); return readEpub(resources); } public EpubBook readEpub(Resources resources) { return readEpub(resources, new EpubBook()); } public EpubBook readEpub(Resources resources, EpubBook result) { if (result == null) { result = new EpubBook(); } handleMimeType(result, resources); String packageResourceHref = getPackageResourceHref(resources); Resource packageResource = processPackageResource(packageResourceHref, result, resources); result.setOpfResource(packageResource); Resource ncxResource = processNcxResource(packageResource, result); result.setNcxResource(ncxResource); result = postProcessBook(result); return result; } private EpubBook postProcessBook(EpubBook book) { if (bookProcessor != null) { book = bookProcessor.processBook(book); } return book; } private Resource processNcxResource(Resource packageResource, EpubBook book) { System.out.println(TAG + " OPF:getHref()" + packageResource.getHref()); if (book.isEpub3()) { return NCXDocumentV3.read(book, this); } else { return NCXDocumentV2.read(book, this); } } private Resource processPackageResource(String packageResourceHref, EpubBook book, Resources resources) { Resource packageResource = resources.remove(packageResourceHref); try { PackageDocumentReader.read(packageResource, this, book, resources); } catch (Exception e) { e.printStackTrace(); // Log.e(TAG, e.getMessage(), e); } return packageResource; } private String getPackageResourceHref(Resources resources) { String defaultResult = "OEBPS/content.opf"; String result = defaultResult; Resource containerResource = resources.remove("META-INF/container.xml"); if (containerResource == null) { return result; } try { Document document = ResourceUtil.getAsDocument(containerResource); Element rootFileElement = (Element) ((Element) document .getDocumentElement().getElementsByTagName("rootfiles").item(0)) .getElementsByTagName("rootfile").item(0); result = rootFileElement.getAttribute("full-path"); } catch (Exception e) { e.printStackTrace(); // Log.e(TAG, e.getMessage(), e); } if (StringUtil.isBlank(result)) { result = defaultResult; } return result; } private void handleMimeType(EpubBook result, Resources resources) { resources.remove("mimetype"); //result.setResources(resources); } } ================================================ FILE: src/main/java/me/ag2s/epublib/epub/EpubWriter.java ================================================ package me.ag2s.epublib.epub; import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.zip.CRC32; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import me.ag2s.epublib.domain.EpubBook; import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.util.IOUtil; /** * Generates an epub file. Not thread-safe, single use object. * * @author paul */ public class EpubWriter { private static final String TAG= EpubWriter.class.getName(); // package static final String EMPTY_NAMESPACE_PREFIX = ""; private BookProcessor bookProcessor; public EpubWriter() { this(BookProcessor.IDENTITY_BOOKPROCESSOR); } public EpubWriter(BookProcessor bookProcessor) { this.bookProcessor = bookProcessor; } public void write(EpubBook book, OutputStream out) throws IOException { book = processBook(book); ZipOutputStream resultStream = new ZipOutputStream(out); writeMimeType(resultStream); writeContainer(resultStream); initTOCResource(book); writeResources(book, resultStream); writePackageDocument(book, resultStream); resultStream.close(); } private EpubBook processBook(EpubBook book) { if (bookProcessor != null) { book = bookProcessor.processBook(book); } return book; } private void initTOCResource(EpubBook book) { Resource tocResource; try { if (book.isEpub3()) { tocResource = NCXDocumentV3.createNCXResource(book); } else { tocResource = NCXDocumentV2.createNCXResource(book); } Resource currentTocResource = book.getSpine().getTocResource(); if (currentTocResource != null) { book.getResources().remove(currentTocResource.getHref()); } book.getSpine().setTocResource(tocResource); book.getResources().add(tocResource); } catch (Exception e) { e.printStackTrace(); // Log.e(TAG, // "Error writing table of contents: " // + ex.getClass().getName() + ": " + ex.getMessage(), ex); } } private void writeResources(EpubBook book, ZipOutputStream resultStream) { for (Resource resource : book.getResources().getAll()) { writeResource(resource, resultStream); } } /** * Writes the resource to the resultStream. * * @param resource resource * @param resultStream resultStream */ private void writeResource(Resource resource, ZipOutputStream resultStream) { if (resource == null) { return; } try { resultStream.putNextEntry(new ZipEntry("OEBPS/" + resource.getHref())); InputStream inputStream = resource.getInputStream(); IOUtil.copy(inputStream, resultStream); inputStream.close(); } catch (Exception e) { e.printStackTrace(); // Log.e(TAG,e.getMessage(), e); } } private void writePackageDocument(EpubBook book, ZipOutputStream resultStream) throws IOException { resultStream.putNextEntry(new ZipEntry("OEBPS/content.opf")); XmlSerializer xmlSerializer = EpubProcessorSupport .createXmlSerializer(resultStream); PackageDocumentWriter.write(this, xmlSerializer, book); xmlSerializer.flush(); // String resultAsString = result.toString(); // resultStream.write(resultAsString.getBytes(Constants.ENCODING)); } /** * Writes the META-INF/container.xml file. * * @param resultStream resultStream * @throws IOException IOException */ private void writeContainer(ZipOutputStream resultStream) throws IOException { resultStream.putNextEntry(new ZipEntry("META-INF/container.xml")); Writer out = new OutputStreamWriter(resultStream); out.write("\n"); out.write( "\n"); out.write("\t\n"); out.write( "\t\t\n"); out.write("\t\n"); out.write(""); out.flush(); } /** * Stores the mimetype as an uncompressed file in the ZipOutputStream. * * @param resultStream resultStream * @throws IOException IOException */ private void writeMimeType(ZipOutputStream resultStream) throws IOException { ZipEntry mimetypeZipEntry = new ZipEntry("mimetype"); mimetypeZipEntry.setMethod(ZipEntry.STORED); byte[] mimetypeBytes = MediaTypes.EPUB.getName().getBytes(); mimetypeZipEntry.setSize(mimetypeBytes.length); mimetypeZipEntry.setCrc(calculateCrc(mimetypeBytes)); resultStream.putNextEntry(mimetypeZipEntry); resultStream.write(mimetypeBytes); } private long calculateCrc(byte[] data) { CRC32 crc = new CRC32(); crc.update(data); return crc.getValue(); } String getNcxId() { return "ncx"; } String getNcxHref() { return "toc.ncx"; } String getNcxMediaType() { return MediaTypes.NCX.getName(); } @SuppressWarnings("unused") public BookProcessor getBookProcessor() { return bookProcessor; } @SuppressWarnings("unused") public void setBookProcessor(BookProcessor bookProcessor) { this.bookProcessor = bookProcessor; } } ================================================ FILE: src/main/java/me/ag2s/epublib/epub/HtmlProcessor.java ================================================ package me.ag2s.epublib.epub; import me.ag2s.epublib.domain.Resource; import java.io.OutputStream; @SuppressWarnings("unused") public interface HtmlProcessor { void processHtmlResource(Resource resource, OutputStream out); } ================================================ FILE: src/main/java/me/ag2s/epublib/epub/NCXDocumentV2.java ================================================ package me.ag2s.epublib.epub; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xmlpull.v1.XmlSerializer; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import me.ag2s.epublib.Constants; import me.ag2s.epublib.domain.Author; import me.ag2s.epublib.domain.EpubBook; import me.ag2s.epublib.domain.Identifier; import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.domain.TOCReference; import me.ag2s.epublib.domain.TableOfContents; import me.ag2s.epublib.util.ResourceUtil; import me.ag2s.epublib.util.StringUtil; /** * Writes the ncx document as defined by namespace http://www.daisy.org/z3986/2005/ncx/ * * @author paul */ public class NCXDocumentV2 { public static final String NAMESPACE_NCX = "http://www.daisy.org/z3986/2005/ncx/"; @SuppressWarnings("unused") public static final String PREFIX_NCX = "ncx"; public static final String NCX_ITEM_ID = "ncx"; public static final String DEFAULT_NCX_HREF = "toc.ncx"; public static final String PREFIX_DTB = "dtb"; private static final String TAG = NCXDocumentV2.class.getName(); private interface NCXTags { String ncx = "ncx"; String meta = "meta"; String navPoint = "navPoint"; String navMap = "navMap"; String navLabel = "navLabel"; String content = "content"; String text = "text"; String docTitle = "docTitle"; String docAuthor = "docAuthor"; String head = "head"; } private interface NCXAttributes { String src = "src"; String name = "name"; String content = "content"; String id = "id"; String playOrder = "playOrder"; String clazz = "class"; String version = "version"; } private interface NCXAttributeValues { String chapter = "chapter"; String version = "2005-1"; } @SuppressWarnings("unused") public static Resource read(EpubBook book, EpubReader epubReader) { Resource ncxResource = null; if (book.getSpine().getTocResource() == null) { // Log.e(TAG, "Book does not contain a table of contents file"); System.err.println(TAG + " Book does not contain a table of contents file"); return null; } try { ncxResource = book.getSpine().getTocResource(); if (ncxResource == null) { return null; } // Log.d(TAG, ncxResource.getHref()); System.out.println(TAG + " ncxResource.getHref()" + ncxResource.getHref()); Document ncxDocument = ResourceUtil.getAsDocument(ncxResource); Element navMapElement = DOMUtil .getFirstElementByTagNameNS(ncxDocument.getDocumentElement(), NAMESPACE_NCX, NCXTags.navMap); if (navMapElement == null) { return null; } TableOfContents tableOfContents = new TableOfContents( readTOCReferences(navMapElement.getChildNodes(), book)); book.setTableOfContents(tableOfContents); } catch (Exception e) { e.printStackTrace(); // Log.e(TAG, e.getMessage(), e); } return ncxResource; } static List readTOCReferences(NodeList navpoints, EpubBook book) { if (navpoints == null) { return new ArrayList<>(); } List result = new ArrayList<>( navpoints.getLength()); for (int i = 0; i < navpoints.getLength(); i++) { Node node = navpoints.item(i); if (node.getNodeType() != Document.ELEMENT_NODE) { continue; } if (!(node.getLocalName().equals(NCXTags.navPoint))) { continue; } TOCReference tocReference = readTOCReference((Element) node, book); result.add(tocReference); } return result; } static TOCReference readTOCReference(Element navpointElement, EpubBook book) { String label = readNavLabel(navpointElement); //Log.d(TAG,"label:"+label); String tocResourceRoot = StringUtil .substringBeforeLast(book.getSpine().getTocResource().getHref(), '/'); if (tocResourceRoot.length() == book.getSpine().getTocResource().getHref() .length()) { tocResourceRoot = ""; } else { tocResourceRoot = tocResourceRoot + "/"; } String reference = StringUtil .collapsePathDots(tocResourceRoot + readNavReference(navpointElement)); String href = StringUtil .substringBefore(reference, Constants.FRAGMENT_SEPARATOR_CHAR); String fragmentId = StringUtil .substringAfter(reference, Constants.FRAGMENT_SEPARATOR_CHAR); Resource resource = book.getResources().getByHref(href); if (resource == null) { System.err.println(TAG + " Resource with href " + href + " in NCX document not found"); // Log.e(TAG, "Resource with href " + href + " in NCX document not found"); } System.out.println(TAG + " label:" + label); System.out.println(TAG + " href:" + href); System.out.println(TAG + " fragmentId:" + fragmentId); TOCReference result = new TOCReference(label, resource, fragmentId); List childTOCReferences = readTOCReferences( navpointElement.getChildNodes(), book); result.setChildren(childTOCReferences); return result; } private static String readNavReference(Element navpointElement) { Element contentElement = DOMUtil .getFirstElementByTagNameNS(navpointElement, NAMESPACE_NCX, NCXTags.content); if (contentElement == null) { return null; } String result = DOMUtil .getAttribute(contentElement, NAMESPACE_NCX, NCXAttributes.src); try { result = URLDecoder.decode(result, Constants.CHARACTER_ENCODING); } catch (UnsupportedEncodingException e) { e.printStackTrace(); // Log.e(TAG, e.getMessage()); } return result; } private static String readNavLabel(Element navpointElement) { //Log.d(TAG,navpointElement.getTagName()); Element navLabel = DOMUtil .getFirstElementByTagNameNS(navpointElement, NAMESPACE_NCX, NCXTags.navLabel); assert navLabel != null; return DOMUtil.getTextChildrenContent(DOMUtil .getFirstElementByTagNameNS(navLabel, NAMESPACE_NCX, NCXTags.text)); } @SuppressWarnings("unused") public static void write(EpubWriter epubWriter, EpubBook book, ZipOutputStream resultStream) throws IOException { resultStream .putNextEntry(new ZipEntry(book.getSpine().getTocResource().getHref())); XmlSerializer out = EpubProcessorSupport.createXmlSerializer(resultStream); write(out, book); out.flush(); } /** * Generates a resource containing an xml document containing the table of contents of the book in ncx format. * * @param xmlSerializer the serializer used * @param book the book to serialize * @throws IOException IOException * @throws IllegalStateException IllegalStateException * @throws IllegalArgumentException IllegalArgumentException */ public static void write(XmlSerializer xmlSerializer, EpubBook book) throws IllegalArgumentException, IllegalStateException, IOException { write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(), book.getMetadata().getAuthors(), book.getTableOfContents()); } public static Resource createNCXResource(EpubBook book) throws IllegalArgumentException, IllegalStateException, IOException { return createNCXResource(book.getMetadata().getIdentifiers(), book.getTitle(), book.getMetadata().getAuthors(), book.getTableOfContents()); } public static Resource createNCXResource(List identifiers, String title, List authors, TableOfContents tableOfContents) throws IllegalArgumentException, IllegalStateException, IOException { ByteArrayOutputStream data = new ByteArrayOutputStream(); XmlSerializer out = EpubProcessorSupport.createXmlSerializer(data); write(out, identifiers, title, authors, tableOfContents); return new Resource(NCX_ITEM_ID, data.toByteArray(), DEFAULT_NCX_HREF, MediaTypes.NCX); } public static void write(XmlSerializer serializer, List identifiers, String title, List authors, TableOfContents tableOfContents) throws IllegalArgumentException, IllegalStateException, IOException { serializer.startDocument(Constants.CHARACTER_ENCODING, false); serializer.setPrefix(EpubWriter.EMPTY_NAMESPACE_PREFIX, NAMESPACE_NCX); serializer.startTag(NAMESPACE_NCX, NCXTags.ncx); // serializer.writeNamespace("ncx", NAMESPACE_NCX); // serializer.attribute("xmlns", NAMESPACE_NCX); serializer .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.version, NCXAttributeValues.version); serializer.startTag(NAMESPACE_NCX, NCXTags.head); for (Identifier identifier : identifiers) { writeMetaElement(identifier.getScheme(), identifier.getValue(), serializer); } writeMetaElement("generator", Constants.EPUB_GENERATOR_NAME, serializer); writeMetaElement("depth", String.valueOf(tableOfContents.calculateDepth()), serializer); writeMetaElement("totalPageCount", "0", serializer); writeMetaElement("maxPageNumber", "0", serializer); serializer.endTag(NAMESPACE_NCX, "head"); serializer.startTag(NAMESPACE_NCX, NCXTags.docTitle); serializer.startTag(NAMESPACE_NCX, NCXTags.text); // write the first title serializer.text(StringUtil.defaultIfNull(title)); serializer.endTag(NAMESPACE_NCX, NCXTags.text); serializer.endTag(NAMESPACE_NCX, NCXTags.docTitle); for (Author author : authors) { serializer.startTag(NAMESPACE_NCX, NCXTags.docAuthor); serializer.startTag(NAMESPACE_NCX, NCXTags.text); serializer.text(author.getLastname() + ", " + author.getFirstname()); serializer.endTag(NAMESPACE_NCX, NCXTags.text); serializer.endTag(NAMESPACE_NCX, NCXTags.docAuthor); } serializer.startTag(NAMESPACE_NCX, NCXTags.navMap); writeNavPoints(tableOfContents.getTocReferences(), 1, serializer); serializer.endTag(NAMESPACE_NCX, NCXTags.navMap); serializer.endTag(NAMESPACE_NCX, "ncx"); serializer.endDocument(); } private static void writeMetaElement(String dtbName, String content, XmlSerializer serializer) throws IllegalArgumentException, IllegalStateException, IOException { serializer.startTag(NAMESPACE_NCX, NCXTags.meta); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.name, PREFIX_DTB + ":" + dtbName); serializer .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.content, content); serializer.endTag(NAMESPACE_NCX, NCXTags.meta); } private static int writeNavPoints(List tocReferences, int playOrder, XmlSerializer serializer) throws IllegalArgumentException, IllegalStateException, IOException { for (TOCReference tocReference : tocReferences) { if (tocReference.getResource() == null) { playOrder = writeNavPoints(tocReference.getChildren(), playOrder, serializer); continue; } writeNavPointStart(tocReference, playOrder, serializer); playOrder++; if (!tocReference.getChildren().isEmpty()) { playOrder = writeNavPoints(tocReference.getChildren(), playOrder, serializer); } writeNavPointEnd(tocReference, serializer); } return playOrder; } private static void writeNavPointStart(TOCReference tocReference, int playOrder, XmlSerializer serializer) throws IllegalArgumentException, IllegalStateException, IOException { serializer.startTag(NAMESPACE_NCX, NCXTags.navPoint); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.id, "navPoint-" + playOrder); serializer .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.playOrder, String.valueOf(playOrder)); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.clazz, NCXAttributeValues.chapter); serializer.startTag(NAMESPACE_NCX, NCXTags.navLabel); serializer.startTag(NAMESPACE_NCX, NCXTags.text); serializer.text(tocReference.getTitle()); serializer.endTag(NAMESPACE_NCX, NCXTags.text); serializer.endTag(NAMESPACE_NCX, NCXTags.navLabel); serializer.startTag(NAMESPACE_NCX, NCXTags.content); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.src, tocReference.getCompleteHref()); serializer.endTag(NAMESPACE_NCX, NCXTags.content); } @SuppressWarnings("unused") private static void writeNavPointEnd(TOCReference tocReference, XmlSerializer serializer) throws IllegalArgumentException, IllegalStateException, IOException { serializer.endTag(NAMESPACE_NCX, NCXTags.navPoint); } } ================================================ FILE: src/main/java/me/ag2s/epublib/epub/NCXDocumentV3.java ================================================ package me.ag2s.epublib.epub; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xmlpull.v1.XmlSerializer; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.List; import me.ag2s.epublib.Constants; import me.ag2s.epublib.domain.Author; import me.ag2s.epublib.domain.EpubBook; import me.ag2s.epublib.domain.Identifier; import me.ag2s.epublib.domain.MediaType; import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.domain.TOCReference; import me.ag2s.epublib.domain.TableOfContents; import me.ag2s.epublib.util.ResourceUtil; import me.ag2s.epublib.util.StringUtil; /** * Writes the ncx document as defined by namespace http://www.daisy.org/z3986/2005/ncx/ * * @author Ag2S20150909 */ public class NCXDocumentV3 { public static final String NAMESPACE_XHTML = "http://www.w3.org/1999/xhtml"; public static final String NAMESPACE_EPUB = "http://www.idpf.org/2007/ops"; public static final String LANGUAGE = "en"; @SuppressWarnings("unused") public static final String PREFIX_XHTML = "html"; public static final String NCX_ITEM_ID = "htmltoc"; public static final String DEFAULT_NCX_HREF = "toc.xhtml"; public static final String V3_NCX_PROPERTIES = "nav"; public static final MediaType V3_NCX_MEDIATYPE = MediaTypes.XHTML; private static final String TAG = NCXDocumentV3.class.getName(); private interface XHTMLTgs { String html = "html"; String head = "head"; String title = "title"; String meta = "meta"; String link = "link"; String body = "body"; String h1 = "h1"; String h2 = "h2"; String nav = "nav"; String ol = "ol"; String li = "li"; String a = "a"; String span = "span"; } private interface XHTMLAttributes { String xmlns = "xmlns"; String xmlns_epub = "xmlns:epub"; String lang = "lang"; String xml_lang = "xml:lang"; String rel = "rel"; String type = "type"; String epub_type = "epub:type";//nav的必须属性 String id = "id"; String role = "role"; String href = "href"; String http_equiv = "http-equiv"; String content = "content"; } private interface XHTMLAttributeValues { String Content_Type = "Content-Type"; String HTML_UTF8 = "text/html; charset=utf-8"; String lang = "en"; String epub_type = "toc"; String role_toc = "doc-toc"; } /** * 解析epub的目录文件 * * @param book Book * @param epubReader epubreader * @return Resource */ @SuppressWarnings("unused") public static Resource read(EpubBook book, EpubReader epubReader) { Resource ncxResource = null; if (book.getSpine().getTocResource() == null) { // Log.e(TAG, "Book does not contain a table of contents file"); System.err.println(TAG + " Book does not contain a table of contents file"); return null; } try { ncxResource = book.getSpine().getTocResource(); if (ncxResource == null) { return null; } //一些epub 3 文件没有按照epub3的标准使用删除掉ncx目录文件 if (ncxResource.getHref().endsWith(".ncx")){ // Log.v(TAG,"该epub文件不标准,使用了epub2的目录文件"); System.err.println(TAG + " 该epub文件不标准,使用了epub2的目录文件"); return NCXDocumentV2.read(book, epubReader); } // Log.d(TAG, ncxResource.getHref()); System.out.println(TAG + " " + ncxResource.getHref()); Document ncxDocument = ResourceUtil.getAsDocument(ncxResource); // Log.d(TAG, ncxDocument.getNodeName()); System.out.println(TAG + " " + ncxDocument.getNodeName()); Element navMapElement = (Element) ncxDocument.getElementsByTagName(XHTMLTgs.nav).item(0); if(navMapElement==null){ // Log.d(TAG,"epub3目录文件未发现nav节点,尝试使用epub2的规则解析"); System.out.println(TAG + " " + "epub3目录文件未发现nav节点,尝试使用epub2的规则解析"); return NCXDocumentV2.read(book, epubReader); } navMapElement = (Element) navMapElement.getElementsByTagName(XHTMLTgs.ol).item(0); // Log.d(TAG, navMapElement.getTagName()); System.out.println(TAG + " " + navMapElement.getTagName()); TableOfContents tableOfContents = new TableOfContents( readTOCReferences(navMapElement.getChildNodes(), book)); // Log.d(TAG, tableOfContents.toString()); System.out.println(TAG + " " + tableOfContents.toString()); book.setTableOfContents(tableOfContents); } catch (Exception e) { e.printStackTrace(); // Log.e(TAG, e.getMessage(), e); } return ncxResource; } private static List doToc(Node n, EpubBook book) { List result = new ArrayList<>(); if (n == null || n.getNodeType() != Document.ELEMENT_NODE) { return result; } else { Element el = (Element) n; NodeList nodeList = el.getElementsByTagName(XHTMLTgs.li); for (int i = 0; i < nodeList.getLength(); i++) { result.add(readTOCReference((Element) nodeList.item(i), book)); } } return result; } static List readTOCReferences(NodeList navpoints, EpubBook book) { if (navpoints == null) { return new ArrayList<>(); } //Log.d(TAG, "readTOCReferences:navpoints.getLength()" + navpoints.getLength()); List result = new ArrayList<>(navpoints.getLength()); for (int i = 0; i < navpoints.getLength(); i++) { Node node = navpoints.item(i); //如果该node是null,或者不是Element,跳出本次循环 if (node == null || node.getNodeType() != Document.ELEMENT_NODE) { continue; } Element el = (Element) node; //如果该Element的name为”li“,将其添加到目录结果 if (el.getTagName().equals(XHTMLTgs.li)) { result.add(readTOCReference(el, book)); } } return result; } static TOCReference readTOCReference(Element navpointElement, EpubBook book) { //章节的名称 String label = readNavLabel(navpointElement); //Log.d(TAG, "label:" + label); String tocResourceRoot = StringUtil .substringBeforeLast(book.getSpine().getTocResource().getHref(), '/'); if (tocResourceRoot.length() == book.getSpine().getTocResource().getHref() .length()) { tocResourceRoot = ""; } else { tocResourceRoot = tocResourceRoot + "/"; } String reference = StringUtil .collapsePathDots(tocResourceRoot + readNavReference(navpointElement)); String href = StringUtil .substringBefore(reference, Constants.FRAGMENT_SEPARATOR_CHAR); String fragmentId = StringUtil .substringAfter(reference, Constants.FRAGMENT_SEPARATOR_CHAR); Resource resource = book.getResources().getByHref(href); if (resource == null) { System.err.println(TAG + " " + "Resource with href " + href + " in NCX document not found"); // Log.e(TAG, "Resource with href " + href + " in NCX document not found"); } System.out.println(TAG + " label:" + label); System.out.println(TAG + " href:" + href); System.out.println(TAG + " fragmentId:" + fragmentId); //父级目录 TOCReference result = new TOCReference(label, resource, fragmentId); //解析子级目录 List childTOCReferences = doToc(navpointElement, book); //readTOCReferences( //navpointElement.getChildNodes(), book); result.setChildren(childTOCReferences); return result; } /** * 获取目录节点的href * * @param navpointElement navpointElement * @return String */ private static String readNavReference(Element navpointElement) { //https://www.w3.org/publishing/epub/epub-packages.html#sec-package-nav //父级节点必须是 "li" //Log.d(TAG, "readNavReference:" + navpointElement.getTagName()); Element contentElement = DOMUtil .getFirstElementByTagNameNS(navpointElement, "", XHTMLTgs.a); if (contentElement == null) { return null; } String result = DOMUtil .getAttribute(contentElement, "", XHTMLAttributes.href); try { result = URLDecoder.decode(result, Constants.CHARACTER_ENCODING); } catch (UnsupportedEncodingException e) { // Log.e(TAG, e.getMessage()); e.printStackTrace(); } return result; } /** * 获取目录节点里面的章节名 * * @param navpointElement navpointElement * @return String */ private static String readNavLabel(Element navpointElement) { //https://www.w3.org/publishing/epub/epub-packages.html#sec-package-nav //父级节点必须是 "li" //Log.d(TAG, "readNavLabel:" + navpointElement.getTagName()); String label; Element labelElement = DOMUtil.getFirstElementByTagNameNS(navpointElement, "", "a"); assert labelElement != null; label = labelElement.getTextContent(); if (StringUtil.isNotBlank(label)) { return label; } else { labelElement = DOMUtil.getFirstElementByTagNameNS(navpointElement, "", "span"); } assert labelElement != null; label = labelElement.getTextContent(); //如果通过 a 标签无法获取章节列表,则是无href章节名 return label; } public static Resource createNCXResource(EpubBook book) throws IllegalArgumentException, IllegalStateException, IOException { return createNCXResource(book.getMetadata().getIdentifiers(), book.getTitle(), book.getMetadata().getAuthors(), book.getTableOfContents()); } public static Resource createNCXResource(List identifiers, String title, List authors, TableOfContents tableOfContents) throws IllegalArgumentException, IllegalStateException, IOException { ByteArrayOutputStream data = new ByteArrayOutputStream(); XmlSerializer out = EpubProcessorSupport.createXmlSerializer(data); write(out, identifiers, title, authors, tableOfContents); Resource resource = new Resource(NCX_ITEM_ID, data.toByteArray(), DEFAULT_NCX_HREF, V3_NCX_MEDIATYPE); resource.setProperties(V3_NCX_PROPERTIES); return resource; } /** * Generates a resource containing an xml document containing the table of contents of the book in ncx format. * * @param xmlSerializer the serializer used * @param book the book to serialize * @throws IOException IOException * @throws IllegalStateException IllegalStateException * @throws IllegalArgumentException IllegalArgumentException */ public static void write(XmlSerializer xmlSerializer, EpubBook book) throws IllegalArgumentException, IllegalStateException, IOException { write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(), book.getMetadata().getAuthors(), book.getTableOfContents()); } /** * 写入 * * @param serializer serializer * @param identifiers identifiers * @param title title * @param authors authors * @param tableOfContents tableOfContents */ @SuppressWarnings("unused") public static void write(XmlSerializer serializer, List identifiers, String title, List authors, TableOfContents tableOfContents) throws IllegalArgumentException, IllegalStateException, IOException { serializer.startDocument(Constants.CHARACTER_ENCODING, false); serializer.setPrefix(EpubWriter.EMPTY_NAMESPACE_PREFIX, NAMESPACE_XHTML); serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.html); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, XHTMLAttributes.xmlns_epub, NAMESPACE_EPUB); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, XHTMLAttributes.xml_lang, XHTMLAttributeValues.lang); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, XHTMLAttributes.lang, LANGUAGE); //写入头部head标签 writeHead(title, serializer); //body开始 serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.body); //h1开始 serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.h1); serializer.text(title); serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.h1); //h1关闭 //nav开始 serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.nav); serializer.attribute("", XHTMLAttributes.epub_type, XHTMLAttributeValues.epub_type); serializer.attribute("", XHTMLAttributes.id, XHTMLAttributeValues.epub_type); serializer.attribute("", XHTMLAttributes.role, XHTMLAttributeValues.role_toc); //h2开始 serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.h2); serializer.text("目录"); serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.h2); writeNavPoints(tableOfContents.getTocReferences(), 1, serializer); serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.nav); //body关闭 serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.body); serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.html); serializer.endDocument(); } private static int writeNavPoints(List tocReferences, int playOrder, XmlSerializer serializer) throws IOException { writeOlStart(serializer); for (TOCReference tocReference : tocReferences) { if (tocReference.getResource() == null) { playOrder = writeNavPoints(tocReference.getChildren(), playOrder, serializer); continue; } writeNavPointStart(tocReference, serializer); playOrder++; if (!tocReference.getChildren().isEmpty()) { playOrder = writeNavPoints(tocReference.getChildren(), playOrder, serializer); } writeNavPointEnd(tocReference, serializer); } writeOlSEnd(serializer); return playOrder; } private static void writeNavPointStart(TOCReference tocReference, XmlSerializer serializer) throws IOException { writeLiStart(serializer); String title = tocReference.getTitle(); String href = tocReference.getCompleteHref(); if (StringUtil.isNotBlank(href)) { writeLabel(title, href, serializer); } else { writeLabel(title, serializer); } } @SuppressWarnings("unused") private static void writeNavPointEnd(TOCReference tocReference, XmlSerializer serializer) throws IOException { writeLiEnd(serializer); } protected static void writeLabel(String title, String href, XmlSerializer serializer) throws IOException { serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.a); serializer.attribute("", XHTMLAttributes.href, href); //attribute必须在Text之前设置。 serializer.text(title); //serializer.attribute(NAMESPACE_XHTML, XHTMLAttributes.href, href); serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.a); } protected static void writeLabel(String title, XmlSerializer serializer) throws IOException { serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.span); serializer.text(title); serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.span); } private static void writeLiStart(XmlSerializer serializer) throws IOException { serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.li); // Log.d(TAG, "writeLiStart"); System.out.println(TAG + " writeLiStart"); } private static void writeLiEnd(XmlSerializer serializer) throws IOException { serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.li); // Log.d(TAG, "writeLiEND"); System.out.println(TAG + " writeLiEND"); } private static void writeOlStart(XmlSerializer serializer) throws IOException { serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.ol); // Log.d(TAG, "writeOlStart"); System.out.println(TAG + " writeOlStart"); } private static void writeOlSEnd(XmlSerializer serializer) throws IOException { serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.ol); // Log.d(TAG, "writeOlEnd"); System.out.println(TAG + " writeOlEnd"); } private static void writeHead(String title, XmlSerializer serializer) throws IOException { serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.head); //title serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.title); serializer.text(StringUtil.defaultIfNull(title)); serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.title); //link serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.link); serializer.attribute("", XHTMLAttributes.rel, "stylesheet"); serializer.attribute("", XHTMLAttributes.type, "text/css"); serializer.attribute("", XHTMLAttributes.href, "css/style.css"); serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.link); //meta serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.meta); serializer.attribute("", XHTMLAttributes.http_equiv, XHTMLAttributeValues.Content_Type); serializer.attribute("", XHTMLAttributes.content, XHTMLAttributeValues.HTML_UTF8); serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.meta); serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.head); } } ================================================ FILE: src/main/java/me/ag2s/epublib/epub/PackageDocumentBase.java ================================================ package me.ag2s.epublib.epub; /** * Functionality shared by the PackageDocumentReader and the PackageDocumentWriter * * @author paul * */ public class PackageDocumentBase { public static final String BOOK_ID_ID = "duokan-book-id"; public static final String NAMESPACE_OPF = "http://www.idpf.org/2007/opf"; public static final String NAMESPACE_DUBLIN_CORE = "http://purl.org/dc/elements/1.1/"; public static final String PREFIX_DUBLIN_CORE = "dc"; //public static final String PREFIX_OPF = "opf"; //在EPUB3标准中,packge前面没有opf头,一些epub阅读器也不支持opf头。 //Some Epub Reader not reconize op:packge,So just let it empty; public static final String PREFIX_OPF = ""; //添加 version 变量来区分Epub文件的版本 //Add the version field to distinguish the version of EPUB file public static final String version="version"; public static final String dateFormat = "yyyy-MM-dd"; protected interface DCTags { String title = "title"; String creator = "creator"; String subject = "subject"; String description = "description"; String publisher = "publisher"; String contributor = "contributor"; String date = "date"; String type = "type"; String format = "format"; String identifier = "identifier"; String source = "source"; String language = "language"; String relation = "relation"; String coverage = "coverage"; String rights = "rights"; } protected interface DCAttributes { String scheme = "scheme"; String id = "id"; } protected interface OPFTags { String metadata = "metadata"; String meta = "meta"; String manifest = "manifest"; String packageTag = "package"; String itemref = "itemref"; String spine = "spine"; String reference = "reference"; String guide = "guide"; String item = "item"; } protected interface OPFAttributes { String uniqueIdentifier = "unique-identifier"; String idref = "idref"; String name = "name"; String content = "content"; String type = "type"; String href = "href"; String linear = "linear"; String event = "event"; String role = "role"; String file_as = "file-as"; String id = "id"; String media_type = "media-type"; String title = "title"; String toc = "toc"; String version = "version"; String scheme = "scheme"; String property = "property"; //add for epub3 /** * add for epub3 */ String properties="properties"; } protected interface OPFValues { String meta_cover = "cover"; String reference_cover = "cover"; String no = "no"; String generator = "generator"; String duokan = "duokan-body-font"; } } ================================================ FILE: src/main/java/me/ag2s/epublib/epub/PackageDocumentMetadataReader.java ================================================ package me.ag2s.epublib.epub; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.xml.namespace.QName; import me.ag2s.epublib.domain.Author; import me.ag2s.epublib.domain.Date; import me.ag2s.epublib.domain.Identifier; import me.ag2s.epublib.domain.Metadata; import me.ag2s.epublib.util.StringUtil; /** * Reads the package document metadata. *

* In its own separate class because the PackageDocumentReader became a bit large and unwieldy. * * @author paul */ // package class PackageDocumentMetadataReader extends PackageDocumentBase { private static final String TAG = PackageDocumentMetadataReader.class.getName(); public static Metadata readMetadata(Document packageDocument) { Metadata result = new Metadata(); Element metadataElement = DOMUtil .getFirstElementByTagNameNS(packageDocument.getDocumentElement(), NAMESPACE_OPF, OPFTags.metadata); if (metadataElement == null) { // Log.e(TAG, "Package does not contain element " + OPFTags.metadata); System.err.println(TAG + " " + "Package does not contain element " + OPFTags.metadata); return result; } result.setTitles(DOMUtil .getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE, DCTags.title)); result.setPublishers(DOMUtil .getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE, DCTags.publisher)); result.setDescriptions(DOMUtil .getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE, DCTags.description)); result.setRights(DOMUtil .getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE, DCTags.rights)); result.setTypes(DOMUtil .getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE, DCTags.type)); result.setSubjects(DOMUtil .getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE, DCTags.subject)); result.setIdentifiers(readIdentifiers(metadataElement)); result.setAuthors(readCreators(metadataElement)); result.setContributors(readContributors(metadataElement)); result.setDates(readDates(metadataElement)); result.setOtherProperties(readOtherProperties(metadataElement)); result.setMetaAttributes(readMetaProperties(metadataElement)); Element languageTag = DOMUtil .getFirstElementByTagNameNS(metadataElement, NAMESPACE_DUBLIN_CORE, DCTags.language); if (languageTag != null) { result.setLanguage(DOMUtil.getTextChildrenContent(languageTag)); } return result; } /** * consumes meta tags that have a property attribute as defined in the standard. For example: * <meta property="rendition:layout">pre-paginated</meta> * * @param metadataElement metadataElement * @return Map */ private static Map readOtherProperties( Element metadataElement) { Map result = new HashMap<>(); NodeList metaTags = metadataElement.getElementsByTagName(OPFTags.meta); for (int i = 0; i < metaTags.getLength(); i++) { Node metaNode = metaTags.item(i); Node property = metaNode.getAttributes() .getNamedItem(OPFAttributes.property); if (property != null) { String name = property.getNodeValue(); String value = metaNode.getTextContent(); result.put(new QName(name), value); } } return result; } /** * consumes meta tags that have a property attribute as defined in the standard. For example: * <meta property="rendition:layout">pre-paginated</meta> * * @param metadataElement metadataElement * @return Map */ private static Map readMetaProperties( Element metadataElement) { Map result = new HashMap<>(); NodeList metaTags = metadataElement.getElementsByTagName(OPFTags.meta); for (int i = 0; i < metaTags.getLength(); i++) { Element metaElement = (Element) metaTags.item(i); String name = metaElement.getAttribute(OPFAttributes.name); String value = metaElement.getAttribute(OPFAttributes.content); result.put(name, value); } return result; } private static String getBookIdId(Document document) { Element packageElement = DOMUtil .getFirstElementByTagNameNS(document.getDocumentElement(), NAMESPACE_OPF, OPFTags.packageTag); if (packageElement == null) { return null; } return DOMUtil.getAttribute(packageElement, NAMESPACE_OPF, OPFAttributes.uniqueIdentifier); } private static List readCreators(Element metadataElement) { return readAuthors(DCTags.creator, metadataElement); } private static List readContributors(Element metadataElement) { return readAuthors(DCTags.contributor, metadataElement); } private static List readAuthors(String authorTag, Element metadataElement) { NodeList elements = metadataElement .getElementsByTagNameNS(NAMESPACE_DUBLIN_CORE, authorTag); List result = new ArrayList<>(elements.getLength()); for (int i = 0; i < elements.getLength(); i++) { Element authorElement = (Element) elements.item(i); Author author = createAuthor(authorElement); if (author != null) { result.add(author); } } return result; } private static List readDates(Element metadataElement) { NodeList elements = metadataElement .getElementsByTagNameNS(NAMESPACE_DUBLIN_CORE, DCTags.date); List result = new ArrayList<>(elements.getLength()); for (int i = 0; i < elements.getLength(); i++) { Element dateElement = (Element) elements.item(i); Date date; try { date = new Date(DOMUtil.getTextChildrenContent(dateElement), DOMUtil.getAttribute(dateElement, NAMESPACE_OPF, OPFAttributes.event)); result.add(date); } catch (IllegalArgumentException e) { // Log.e(TAG, e.getMessage()); e.printStackTrace(); } } return result; } private static Author createAuthor(Element authorElement) { String authorString = DOMUtil.getTextChildrenContent(authorElement); if (StringUtil.isBlank(authorString)) { return null; } int spacePos = authorString.lastIndexOf(' '); Author result; if (spacePos < 0) { result = new Author(authorString); } else { result = new Author(authorString.substring(0, spacePos), authorString.substring(spacePos + 1)); } result.setRole( DOMUtil.getAttribute(authorElement, NAMESPACE_OPF, OPFAttributes.role)); return result; } private static List readIdentifiers(Element metadataElement) { NodeList identifierElements = metadataElement .getElementsByTagNameNS(NAMESPACE_DUBLIN_CORE, DCTags.identifier); if (identifierElements.getLength() == 0) { // Log.e(TAG, "Package does not contain element " + DCTags.identifier); System.err.println(TAG + " " + "Package does not contain element " + DCTags.identifier); return new ArrayList<>(); } String bookIdId = getBookIdId(metadataElement.getOwnerDocument()); List result = new ArrayList<>( identifierElements.getLength()); for (int i = 0; i < identifierElements.getLength(); i++) { Element identifierElement = (Element) identifierElements.item(i); String schemeName = DOMUtil.getAttribute(identifierElement, NAMESPACE_OPF, DCAttributes.scheme); String identifierValue = DOMUtil .getTextChildrenContent(identifierElement); if (StringUtil.isBlank(identifierValue)) { continue; } Identifier identifier = new Identifier(schemeName, identifierValue); if (identifierElement.getAttribute("id").equals(bookIdId)) { identifier.setBookId(true); } result.add(identifier); } return result; } } ================================================ FILE: src/main/java/me/ag2s/epublib/epub/PackageDocumentMetadataWriter.java ================================================ package me.ag2s.epublib.epub; import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.util.List; import java.util.Map; import javax.xml.namespace.QName; import me.ag2s.epublib.Constants; import me.ag2s.epublib.domain.Author; import me.ag2s.epublib.domain.Date; import me.ag2s.epublib.domain.EpubBook; import me.ag2s.epublib.domain.Identifier; import me.ag2s.epublib.util.StringUtil; public class PackageDocumentMetadataWriter extends PackageDocumentBase { /** * Writes the book's metadata. * * @param book book * @param serializer serializer * @throws IOException IOException * @throws IllegalStateException IllegalStateException * @throws IllegalArgumentException IllegalArgumentException */ public static void writeMetaData(EpubBook book, XmlSerializer serializer) throws IllegalArgumentException, IllegalStateException, IOException { serializer.startTag(NAMESPACE_OPF, OPFTags.metadata); serializer.setPrefix(PREFIX_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE); serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF); writeIdentifiers(book.getMetadata().getIdentifiers(), serializer); writeSimpleMetdataElements(DCTags.title, book.getMetadata().getTitles(), serializer); writeSimpleMetdataElements(DCTags.subject, book.getMetadata().getSubjects(), serializer); writeSimpleMetdataElements(DCTags.description, book.getMetadata().getDescriptions(), serializer); writeSimpleMetdataElements(DCTags.publisher, book.getMetadata().getPublishers(), serializer); writeSimpleMetdataElements(DCTags.type, book.getMetadata().getTypes(), serializer); writeSimpleMetdataElements(DCTags.rights, book.getMetadata().getRights(), serializer); // write authors for (Author author : book.getMetadata().getAuthors()) { serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.creator); serializer.attribute(NAMESPACE_OPF, OPFAttributes.role, author.getRelator().getCode()); serializer.attribute(NAMESPACE_OPF, OPFAttributes.file_as, author.getLastname() + ", " + author.getFirstname()); serializer.text(author.getFirstname() + " " + author.getLastname()); serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.creator); } // write contributors for (Author author : book.getMetadata().getContributors()) { serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.contributor); serializer.attribute(NAMESPACE_OPF, OPFAttributes.role, author.getRelator().getCode()); serializer.attribute(NAMESPACE_OPF, OPFAttributes.file_as, author.getLastname() + ", " + author.getFirstname()); serializer.text(author.getFirstname() + " " + author.getLastname()); serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.contributor); } // write dates for (Date date : book.getMetadata().getDates()) { serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.date); if (date.getEvent() != null) { serializer.attribute(NAMESPACE_OPF, OPFAttributes.event, date.getEvent().toString()); } serializer.text(date.getValue()); serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.date); } // write language if (StringUtil.isNotBlank(book.getMetadata().getLanguage())) { serializer.startTag(NAMESPACE_DUBLIN_CORE, "language"); serializer.text(book.getMetadata().getLanguage()); serializer.endTag(NAMESPACE_DUBLIN_CORE, "language"); } // write other properties if (book.getMetadata().getOtherProperties() != null) { for (Map.Entry mapEntry : book.getMetadata() .getOtherProperties().entrySet()) { serializer.startTag(mapEntry.getKey().getNamespaceURI(), OPFTags.meta); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.property, mapEntry.getKey().getLocalPart()); serializer.text(mapEntry.getValue()); serializer.endTag(mapEntry.getKey().getNamespaceURI(), OPFTags.meta); } } // write coverimage if (book.getCoverImage() != null) { // write the cover image serializer.startTag(NAMESPACE_OPF, OPFTags.meta); serializer .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.name, OPFValues.meta_cover); serializer .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.content, book.getCoverImage().getId()); serializer.endTag(NAMESPACE_OPF, OPFTags.meta); } // write generator serializer.startTag(NAMESPACE_OPF, OPFTags.meta); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.name, OPFValues.generator); serializer .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.content, Constants.EPUB_GENERATOR_NAME); serializer.endTag(NAMESPACE_OPF, OPFTags.meta); // write duokan serializer.startTag(NAMESPACE_OPF, OPFTags.meta); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.name, OPFValues.duokan); serializer .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.content, Constants.EPUB_DUOKAN_NAME); serializer.endTag(NAMESPACE_OPF, OPFTags.meta); serializer.endTag(NAMESPACE_OPF, OPFTags.metadata); } private static void writeSimpleMetdataElements(String tagName, List values, XmlSerializer serializer) throws IllegalArgumentException, IllegalStateException, IOException { for (String value : values) { if (StringUtil.isBlank(value)) { continue; } serializer.startTag(NAMESPACE_DUBLIN_CORE, tagName); serializer.text(value); serializer.endTag(NAMESPACE_DUBLIN_CORE, tagName); } } /** * Writes out the complete list of Identifiers to the package document. * The first identifier for which the bookId is true is made the bookId identifier. * If no identifier has bookId == true then the first bookId identifier is written as the primary. * * @param identifiers identifiers * @param serializer serializer * @throws IllegalStateException e * @throws IllegalArgumentException e * @ */ private static void writeIdentifiers(List identifiers, XmlSerializer serializer) throws IllegalArgumentException, IllegalStateException, IOException { Identifier bookIdIdentifier = Identifier.getBookIdIdentifier(identifiers); if (bookIdIdentifier == null) { return; } serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, DCAttributes.id, BOOK_ID_ID); serializer.attribute(NAMESPACE_OPF, OPFAttributes.scheme, bookIdIdentifier.getScheme()); serializer.text(bookIdIdentifier.getValue()); serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier); for (Identifier identifier : identifiers.subList(1, identifiers.size())) { if (identifier == bookIdIdentifier) { continue; } serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier); serializer.attribute(NAMESPACE_OPF, "scheme", identifier.getScheme()); serializer.text(identifier.getValue()); serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier); } } } ================================================ FILE: src/main/java/me/ag2s/epublib/epub/PackageDocumentReader.java ================================================ package me.ag2s.epublib.epub; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import me.ag2s.epublib.Constants; import me.ag2s.epublib.domain.EpubBook; import me.ag2s.epublib.domain.Guide; import me.ag2s.epublib.domain.GuideReference; import me.ag2s.epublib.domain.MediaType; import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.domain.Resources; import me.ag2s.epublib.domain.Spine; import me.ag2s.epublib.domain.SpineReference; import me.ag2s.epublib.util.ResourceUtil; import me.ag2s.epublib.util.StringUtil; /** * Reads the opf package document as defined by namespace http://www.idpf.org/2007/opf * * @author paul */ public class PackageDocumentReader extends PackageDocumentBase { private static final String TAG = PackageDocumentReader.class.getName(); private static final String[] POSSIBLE_NCX_ITEM_IDS = new String[]{"toc", "ncx", "ncxtoc", "htmltoc"}; public static void read( Resource packageResource, EpubReader epubReader, EpubBook book, Resources resources) throws SAXException, IOException { Document packageDocument = ResourceUtil.getAsDocument(packageResource); String packageHref = packageResource.getHref(); resources = fixHrefs(packageHref, resources); readGuide(packageDocument, epubReader, book, resources); // Books sometimes use non-identifier ids. We map these here to legal ones Map idMapping = new HashMap<>(); String version = DOMUtil.getAttribute(packageDocument.getDocumentElement(), PREFIX_OPF, PackageDocumentBase.version); resources = readManifest(packageDocument, packageHref, epubReader, resources, idMapping); book.setResources(resources); book.setVersion(version); readCover(packageDocument, book); book.setMetadata( PackageDocumentMetadataReader.readMetadata(packageDocument)); book.setSpine(readSpine(packageDocument, book.getResources(), idMapping)); // if we did not find a cover page then we make the first page of the book the cover page if (book.getCoverPage() == null && book.getSpine().size() > 0) { book.setCoverPage(book.getSpine().getResource(0)); } } // private static Resource readCoverImage(Element metadataElement, Resources resources) { // String coverResourceId = DOMUtil.getFindAttributeValue(metadataElement.getOwnerDocument(), NAMESPACE_OPF, OPFTags.meta, OPFAttributes.name, OPFValues.meta_cover, OPFAttributes.content); // if (StringUtil.isBlank(coverResourceId)) { // return null; // } // Resource coverResource = resources.getByIdOrHref(coverResourceId); // return coverResource; // } /** * Reads the manifest containing the resource ids, hrefs and mediatypes. * * @param packageDocument e * @param packageHref e * @param epubReader e * @param resources e * @param idMapping e * @return a Map with resources, with their id's as key. */ @SuppressWarnings("unused") private static Resources readManifest(Document packageDocument, String packageHref, EpubReader epubReader, Resources resources, Map idMapping) { Element manifestElement = DOMUtil .getFirstElementByTagNameNS(packageDocument.getDocumentElement(), NAMESPACE_OPF, OPFTags.manifest); Resources result = new Resources(); if (manifestElement == null) { // Log.e(TAG, // "Package document does not contain element " + OPFTags.manifest); System.err.println(TAG + " " + "Package does not contain element " + OPFTags.manifest); return result; } NodeList itemElements = manifestElement .getElementsByTagNameNS(NAMESPACE_OPF, OPFTags.item); for (int i = 0; i < itemElements.getLength(); i++) { Element itemElement = (Element) itemElements.item(i); String id = DOMUtil .getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.id); String href = DOMUtil .getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.href); try { href = URLDecoder.decode(href, Constants.CHARACTER_ENCODING); } catch (UnsupportedEncodingException e) { // Log.e(TAG, e.getMessage()); e.printStackTrace(); } String mediaTypeName = DOMUtil .getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.media_type); Resource resource = resources.remove(href); if (resource == null) { // Log.e(TAG, "resource with href '" + href + "' not found"); System.err.println(TAG + " " + "resource with href '" + href + "' not found"); continue; } resource.setId(id); //for epub3 String properties = DOMUtil.getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.properties); resource.setProperties(properties); MediaType mediaType = MediaTypes.getMediaTypeByName(mediaTypeName); if (mediaType != null) { resource.setMediaType(mediaType); } result.add(resource); idMapping.put(id, resource.getId()); } return result; } /** * Reads the book's guide. * Here some more attempts are made at finding the cover page. * * @param packageDocument r * @param epubReader r * @param book r * @param resources g */ @SuppressWarnings("unused") private static void readGuide(Document packageDocument, EpubReader epubReader, EpubBook book, Resources resources) { Element guideElement = DOMUtil .getFirstElementByTagNameNS(packageDocument.getDocumentElement(), NAMESPACE_OPF, OPFTags.guide); if (guideElement == null) { return; } Guide guide = book.getGuide(); NodeList guideReferences = guideElement .getElementsByTagNameNS(NAMESPACE_OPF, OPFTags.reference); for (int i = 0; i < guideReferences.getLength(); i++) { Element referenceElement = (Element) guideReferences.item(i); String resourceHref = DOMUtil .getAttribute(referenceElement, NAMESPACE_OPF, OPFAttributes.href); if (StringUtil.isBlank(resourceHref)) { continue; } Resource resource = resources.getByHref(StringUtil .substringBefore(resourceHref, Constants.FRAGMENT_SEPARATOR_CHAR)); if (resource == null) { // Log.e(TAG, "Guide is referencing resource with href " + resourceHref // + " which could not be found"); System.err.println(TAG + " " + "Guide is referencing resource with href " + resourceHref + " which could not be found"); continue; } String type = DOMUtil .getAttribute(referenceElement, NAMESPACE_OPF, OPFAttributes.type); if (StringUtil.isBlank(type)) { // Log.e(TAG, "Guide is referencing resource with href " + resourceHref // + " which is missing the 'type' attribute"); System.err.println(TAG + " " + "Guide is referencing resource with href " + resourceHref + " which is missing the 'type' attribute"); continue; } String title = DOMUtil .getAttribute(referenceElement, NAMESPACE_OPF, OPFAttributes.title); if (GuideReference.COVER.equalsIgnoreCase(type)) { continue; // cover is handled elsewhere } GuideReference reference = new GuideReference(resource, type, title, StringUtil .substringAfter(resourceHref, Constants.FRAGMENT_SEPARATOR_CHAR)); guide.addReference(reference); } } /** * Strips off the package prefixes up to the href of the packageHref. *

* Example: * If the packageHref is "OEBPS/content.opf" then a resource href like "OEBPS/foo/bar.html" will be turned into "foo/bar.html" * * @param packageHref f * @param resourcesByHref g * @return The stripped package href */ static Resources fixHrefs(String packageHref, Resources resourcesByHref) { int lastSlashPos = packageHref.lastIndexOf('/'); if (lastSlashPos < 0) { return resourcesByHref; } Resources result = new Resources(); for (Resource resource : resourcesByHref.getAll()) { if (StringUtil.isNotBlank(resource.getHref()) && resource.getHref().length() > lastSlashPos) { resource.setHref(resource.getHref().substring(lastSlashPos + 1)); } result.add(resource); } return result; } /** * Reads the document's spine, containing all sections in reading order. * * @param packageDocument b * @param resources b * @param idMapping b * @return the document's spine, containing all sections in reading order. */ private static Spine readSpine(Document packageDocument, Resources resources, Map idMapping) { Element spineElement = DOMUtil .getFirstElementByTagNameNS(packageDocument.getDocumentElement(), NAMESPACE_OPF, OPFTags.spine); if (spineElement == null) { // Log.e(TAG, "Element " + OPFTags.spine // + " not found in package document, generating one automatically"); System.err.println(TAG + " " + "Element " + OPFTags.spine + " not found in package document, generating one automatically"); return generateSpineFromResources(resources); } Spine result = new Spine(); String tocResourceId = DOMUtil.getAttribute(spineElement, NAMESPACE_OPF, OPFAttributes.toc); // Log.v(TAG,tocResourceId); System.out.println(TAG + " " + tocResourceId); result.setTocResource(findTableOfContentsResource(tocResourceId, resources)); NodeList spineNodes = DOMUtil.getElementsByTagNameNS(packageDocument, NAMESPACE_OPF, OPFTags.itemref); if(spineNodes==null){ // Log.e(TAG,"spineNodes is null"); System.err.println(TAG + " " + "spineNodes is null"); return result; } List spineReferences = new ArrayList<>(spineNodes.getLength()); for (int i = 0; i < spineNodes.getLength(); i++) { Element spineItem = (Element) spineNodes.item(i); String itemref = DOMUtil.getAttribute(spineItem, NAMESPACE_OPF, OPFAttributes.idref); if (StringUtil.isBlank(itemref)) { // Log.e(TAG, "itemref with missing or empty idref"); // XXX System.err.println(TAG + " " + "itemref with missing or empty idref"); continue; } String id = idMapping.get(itemref); if (id == null) { id = itemref; } Resource resource = resources.getByIdOrHref(id); if (resource == null) { // Log.e(TAG, "resource with id '" + id + "' not found"); System.err.println(TAG + " " + "resource with id '" + id + "' not found"); continue; } SpineReference spineReference = new SpineReference(resource); if (OPFValues.no.equalsIgnoreCase(DOMUtil .getAttribute(spineItem, NAMESPACE_OPF, OPFAttributes.linear))) { spineReference.setLinear(false); } spineReferences.add(spineReference); } result.setSpineReferences(spineReferences); return result; } /** * Creates a spine out of all resources in the resources. * The generated spine consists of all XHTML pages in order of their href. * * @param resources f * @return a spine created out of all resources in the resources. */ private static Spine generateSpineFromResources(Resources resources) { Spine result = new Spine(); List resourceHrefs = new ArrayList<>(resources.getAllHrefs()); Collections.sort(resourceHrefs, String.CASE_INSENSITIVE_ORDER); for (String resourceHref : resourceHrefs) { Resource resource = resources.getByHref(resourceHref); if (resource.getMediaType() == MediaTypes.NCX) { result.setTocResource(resource); } else if (resource.getMediaType() == MediaTypes.XHTML) { result.addSpineReference(new SpineReference(resource)); } } return result; } /** * The spine tag should contain a 'toc' attribute with as value the resource id of the table of contents resource. *

* Here we try several ways of finding this table of contents resource. * We try the given attribute value, some often-used ones and finally look through all resources for the first resource with the table of contents mimetype. * * @param tocResourceId g * @param resources g * @return the Resource containing the table of contents */ static Resource findTableOfContentsResource(String tocResourceId, Resources resources) { Resource tocResource; //一些epub3的文件为了兼容epub2,保留的epub2的目录文件,这里优先选择epub3的xml目录 tocResource = resources.getByProperties("nav"); if (tocResource != null) { return tocResource; } if (StringUtil.isNotBlank(tocResourceId)) { tocResource = resources.getByIdOrHref(tocResourceId); } if (tocResource != null) { return tocResource; } // get the first resource with the NCX mediatype tocResource = resources.findFirstResourceByMediaType(MediaTypes.NCX); if (tocResource == null) { for (String possibleNcxItemId : POSSIBLE_NCX_ITEM_IDS) { tocResource = resources.getByIdOrHref(possibleNcxItemId); if (tocResource != null) { break; } tocResource = resources .getByIdOrHref(possibleNcxItemId.toUpperCase()); if (tocResource != null) { break; } } } if (tocResource == null) { System.err.println(TAG + " " + "Could not find table of contents resource. Tried resource with id '" + tocResourceId + "', " + Constants.DEFAULT_TOC_ID + ", " + Constants.DEFAULT_TOC_ID.toUpperCase() + " and any NCX resource."); } return tocResource; } /** * Find all resources that have something to do with the coverpage and the cover image. * Search the meta tags and the guide references * * @param packageDocument s * @return all resources that have something to do with the coverpage and the cover image. */ // package static Set findCoverHrefs(Document packageDocument) { Set result = new HashSet<>(); // try and find a meta tag with name = 'cover' and a non-blank id String coverResourceId = DOMUtil .getFindAttributeValue(packageDocument, NAMESPACE_OPF, OPFTags.meta, OPFAttributes.name, OPFValues.meta_cover, OPFAttributes.content); if (StringUtil.isNotBlank(coverResourceId)) { String coverHref = DOMUtil .getFindAttributeValue(packageDocument, NAMESPACE_OPF, OPFTags.item, OPFAttributes.id, coverResourceId, OPFAttributes.href); if (StringUtil.isNotBlank(coverHref)) { result.add(coverHref); } else { result.add( coverResourceId); // maybe there was a cover href put in the cover id attribute } } // try and find a reference tag with type is 'cover' and reference is not blank String coverHref = DOMUtil .getFindAttributeValue(packageDocument, NAMESPACE_OPF, OPFTags.reference, OPFAttributes.type, OPFValues.reference_cover, OPFAttributes.href); if (StringUtil.isNotBlank(coverHref)) { result.add(coverHref); } return result; } /** * Finds the cover resource in the packageDocument and adds it to the book if found. * Keeps the cover resource in the resources map * * @param packageDocument s * @param book x */ private static void readCover(Document packageDocument, EpubBook book) { Collection coverHrefs = findCoverHrefs(packageDocument); for (String coverHref : coverHrefs) { Resource resource = book.getResources().getByHref(coverHref); if (resource == null) { System.err.println(TAG + " " + "Cover resource " + coverHref + " not found"); continue; } if (resource.getMediaType() == MediaTypes.XHTML) { book.setCoverPage(resource); } else if (MediaTypes.isBitmapImage(resource.getMediaType())) { book.setCoverImage(resource); } } } } ================================================ FILE: src/main/java/me/ag2s/epublib/epub/PackageDocumentWriter.java ================================================ package me.ag2s.epublib.epub; import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import me.ag2s.epublib.Constants; import me.ag2s.epublib.domain.EpubBook; import me.ag2s.epublib.domain.Guide; import me.ag2s.epublib.domain.GuideReference; import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.domain.Spine; import me.ag2s.epublib.domain.SpineReference; import me.ag2s.epublib.util.StringUtil; /** * Writes the opf package document as defined by namespace http://www.idpf.org/2007/opf * * @author paul */ public class PackageDocumentWriter extends PackageDocumentBase { private static final String TAG = PackageDocumentWriter.class.getName(); public static void write(EpubWriter epubWriter, XmlSerializer serializer, EpubBook book) { try { serializer.startDocument(Constants.CHARACTER_ENCODING, false); serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF); serializer.setPrefix(PREFIX_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE); serializer.startTag(NAMESPACE_OPF, OPFTags.packageTag); serializer .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.version, book.getVersion()); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.uniqueIdentifier, BOOK_ID_ID); PackageDocumentMetadataWriter.writeMetaData(book, serializer); writeManifest(book, epubWriter, serializer); writeSpine(book, epubWriter, serializer); writeGuide(book, epubWriter, serializer); serializer.endTag(NAMESPACE_OPF, OPFTags.packageTag); serializer.endDocument(); serializer.flush(); } catch (IOException e) { e.printStackTrace(); } } /** * Writes the package's spine. * * @param book e * @param epubWriter g * @param serializer g * @throws IOException g * @throws IllegalStateException g * @throws IllegalArgumentException 1@throws XMLStreamException */ @SuppressWarnings("unused") private static void writeSpine(EpubBook book, EpubWriter epubWriter, XmlSerializer serializer) throws IllegalArgumentException, IllegalStateException, IOException { serializer.startTag(NAMESPACE_OPF, OPFTags.spine); Resource tocResource = book.getSpine().getTocResource(); String tocResourceId = tocResource.getId(); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.toc, tocResourceId); if (book.getCoverPage() != null // there is a cover page && book.getSpine().findFirstResourceById(book.getCoverPage().getId()) < 0) { // cover page is not already in the spine // write the cover html file serializer.startTag(NAMESPACE_OPF, OPFTags.itemref); serializer .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.idref, book.getCoverPage().getId()); serializer .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.linear, "no"); serializer.endTag(NAMESPACE_OPF, OPFTags.itemref); } writeSpineItems(book.getSpine(), serializer); serializer.endTag(NAMESPACE_OPF, OPFTags.spine); } private static void writeManifest(EpubBook book, EpubWriter epubWriter, XmlSerializer serializer) throws IllegalArgumentException, IllegalStateException, IOException { serializer.startTag(NAMESPACE_OPF, OPFTags.manifest); serializer.startTag(NAMESPACE_OPF, OPFTags.item); //For EPUB3 if (book.isEpub3()) { serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.properties, NCXDocumentV3.V3_NCX_PROPERTIES); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.id, NCXDocumentV3.NCX_ITEM_ID); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href, NCXDocumentV3.DEFAULT_NCX_HREF); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type, NCXDocumentV3.V3_NCX_MEDIATYPE.getName()); } else { serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.id, epubWriter.getNcxId()); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href, epubWriter.getNcxHref()); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type, epubWriter.getNcxMediaType()); } serializer.endTag(NAMESPACE_OPF, OPFTags.item); // writeCoverResources(book, serializer); for (Resource resource : getAllResourcesSortById(book)) { writeItem(book, resource, serializer); } serializer.endTag(NAMESPACE_OPF, OPFTags.manifest); } private static List getAllResourcesSortById(EpubBook book) { List allResources = new ArrayList<>( book.getResources().getAll()); Collections.sort(allResources, (resource1, resource2) -> resource1.getId().compareToIgnoreCase(resource2.getId())); return allResources; } /** * Writes a resources as an item element * * @param resource g * @param serializer g * @throws IOException g * @throws IllegalStateException g * @throws IllegalArgumentException 1@throws XMLStreamException */ private static void writeItem(EpubBook book, Resource resource, XmlSerializer serializer) throws IllegalArgumentException, IllegalStateException, IOException { if (resource == null || (resource.getMediaType() == MediaTypes.NCX && book.getSpine().getTocResource() != null)) { return; } if (StringUtil.isBlank(resource.getId())) { // log.error("resource id must not be empty (href: " + resource.getHref() // + ", mediatype:" + resource.getMediaType() + ")"); System.err.println(TAG + " " + "resource id must not be empty (href: " + resource.getHref() + ", mediatype:" + resource.getMediaType() + ")"); return; } if (StringUtil.isBlank(resource.getHref())) { // log.error("resource href must not be empty (id: " + resource.getId() // + ", mediatype:" + resource.getMediaType() + ")"); System.err.println(TAG + " " + "resource href must not be empty (id: " + resource.getId() + ", mediatype:" + resource.getMediaType() + ")"); return; } if (resource.getMediaType() == null) { // log.error("resource mediatype must not be empty (id: " + resource.getId() // + ", href:" + resource.getHref() + ")"); System.err.println(TAG + " " + "resource mediatype must not be empty (id: " + resource.getId() + ", href:" + resource.getHref() + ")"); return; } serializer.startTag(NAMESPACE_OPF, OPFTags.item); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.id, resource.getId()); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href, resource.getHref()); serializer .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type, resource.getMediaType().getName()); serializer.endTag(NAMESPACE_OPF, OPFTags.item); } /** * List all spine references * * @throws IOException f * @throws IllegalStateException f * @throws IllegalArgumentException f */ @SuppressWarnings("unused") private static void writeSpineItems(Spine spine, XmlSerializer serializer) throws IllegalArgumentException, IllegalStateException, IOException { for (SpineReference spineReference : spine.getSpineReferences()) { serializer.startTag(NAMESPACE_OPF, OPFTags.itemref); serializer .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.idref, spineReference.getResourceId()); if (!spineReference.isLinear()) { serializer .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.linear, OPFValues.no); } serializer.endTag(NAMESPACE_OPF, OPFTags.itemref); } } private static void writeGuide(EpubBook book, EpubWriter epubWriter, XmlSerializer serializer) throws IllegalArgumentException, IllegalStateException, IOException { serializer.startTag(NAMESPACE_OPF, OPFTags.guide); ensureCoverPageGuideReferenceWritten(book.getGuide(), epubWriter, serializer); for (GuideReference reference : book.getGuide().getReferences()) { writeGuideReference(reference, serializer); } serializer.endTag(NAMESPACE_OPF, OPFTags.guide); } @SuppressWarnings("unused") private static void ensureCoverPageGuideReferenceWritten(Guide guide, EpubWriter epubWriter, XmlSerializer serializer) throws IllegalArgumentException, IllegalStateException, IOException { if (!(guide.getGuideReferencesByType(GuideReference.COVER).isEmpty())) { return; } Resource coverPage = guide.getCoverPage(); if (coverPage != null) { writeGuideReference( new GuideReference(guide.getCoverPage(), GuideReference.COVER, GuideReference.COVER), serializer); } } private static void writeGuideReference(GuideReference reference, XmlSerializer serializer) throws IllegalArgumentException, IllegalStateException, IOException { if (reference == null) { return; } serializer.startTag(NAMESPACE_OPF, OPFTags.reference); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.type, reference.getType()); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href, reference.getCompleteHref()); if (StringUtil.isNotBlank(reference.getTitle())) { serializer .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.title, reference.getTitle()); } serializer.endTag(NAMESPACE_OPF, OPFTags.reference); } } ================================================ FILE: src/main/java/me/ag2s/epublib/epub/ResourcesLoader.java ================================================ package me.ag2s.epublib.epub; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; import me.ag2s.epublib.domain.EpubResourceProvider; import me.ag2s.epublib.domain.LazyResource; import me.ag2s.epublib.domain.LazyResourceProvider; import me.ag2s.epublib.domain.MediaType; import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.domain.Resources; import me.ag2s.epublib.util.CollectionUtil; import me.ag2s.epublib.util.ResourceUtil; /** * Loads Resources from inputStreams, ZipFiles, etc * * @author paul */ public class ResourcesLoader { private static final String TAG = ResourcesLoader.class.getName(); /** * Loads the entries of the zipFile as resources. *

* The MediaTypes that are in the lazyLoadedTypes will not get their * contents loaded, but are stored as references to entries into the * ZipFile and are loaded on demand by the Resource system. * * @param zipFile import epub zipfile * @param defaultHtmlEncoding epub xhtml default encoding * @param lazyLoadedTypes lazyLoadedTypes * @return Resources * @throws IOException IOException */ public static Resources loadResources(ZipFile zipFile, String defaultHtmlEncoding, List lazyLoadedTypes) throws IOException { LazyResourceProvider resourceProvider = new EpubResourceProvider(zipFile.getName()); Resources result = new Resources(); Enumeration entries = zipFile.entries(); while (entries.hasMoreElements()) { ZipEntry zipEntry = entries.nextElement(); if (zipEntry == null || zipEntry.isDirectory()) { continue; } String href = zipEntry.getName(); Resource resource; if (shouldLoadLazy(href, lazyLoadedTypes)) { resource = new LazyResource(resourceProvider, zipEntry.getSize(), href); } else { resource = ResourceUtil .createResource(zipEntry, zipFile.getInputStream(zipEntry)); /*掌上书苑有很多自制书OPF的nameSpace格式不标准,强制修复成正确的格式*/ if (href.endsWith("opf")) { String string = new String(resource.getData()).replace("smlns=\"", "xmlns=\""); resource.setData(string.getBytes()); } } if (resource.getMediaType() == MediaTypes.XHTML) { resource.setInputEncoding(defaultHtmlEncoding); } result.add(resource); } return result; } /** * Whether the given href will load a mediaType that is in the * collection of lazilyLoadedMediaTypes. * * @param href href * @param lazilyLoadedMediaTypes lazilyLoadedMediaTypes * @return Whether the given href will load a mediaType that is * in the collection of lazilyLoadedMediaTypes. */ private static boolean shouldLoadLazy(String href, Collection lazilyLoadedMediaTypes) { if (CollectionUtil.isEmpty(lazilyLoadedMediaTypes)) { return false; } MediaType mediaType = MediaTypes.determineMediaType(href); return lazilyLoadedMediaTypes.contains(mediaType); } /** * Loads all entries from the ZipInputStream as Resources. *

* Loads the contents of all ZipEntries into memory. * Is fast, but may lead to memory problems when reading large books * on devices with small amounts of memory. * * @param zipInputStream zipInputStream * @param defaultHtmlEncoding defaultHtmlEncoding * @return Resources * @throws IOException IOException */ public static Resources loadResources(ZipInputStream zipInputStream, String defaultHtmlEncoding) throws IOException { Resources result = new Resources(); ZipEntry zipEntry; do { // get next valid zipEntry zipEntry = getNextZipEntry(zipInputStream); if ((zipEntry == null) || zipEntry.isDirectory()) { continue; } String href = zipEntry.getName(); // store resource Resource resource = ResourceUtil.createResource(zipEntry, zipInputStream); ///*掌上书苑有很多自制书OPF的nameSpace格式不标准,强制修复成正确的格式*/ if (href.endsWith("opf")) { String string = new String(resource.getData()).replace("smlns=\"", "xmlns=\""); resource.setData(string.getBytes()); } if (resource.getMediaType() == MediaTypes.XHTML) { resource.setInputEncoding(defaultHtmlEncoding); } result.add(resource); } while (zipEntry != null); return result; } private static ZipEntry getNextZipEntry(ZipInputStream zipInputStream) throws IOException { try { return zipInputStream.getNextEntry(); } catch (ZipException e) { //see Issue #122 Infinite loop. //when reading a file that is not a real zip archive or a zero length file, zipInputStream.getNextEntry() //throws an exception and does not advance, so loadResources enters an infinite loop //log.error("Invalid or damaged zip file.", e); // Log.e(TAG, e.getLocalizedMessage()); e.printStackTrace(); try { zipInputStream.closeEntry(); } catch (Exception ignored) { } throw e; } } /** * Loads all entries from the ZipInputStream as Resources. *

* Loads the contents of all ZipEntries into memory. * Is fast, but may lead to memory problems when reading large books * on devices with small amounts of memory. * * @param zipFile zipFile * @param defaultHtmlEncoding defaultHtmlEncoding * @return Resources * @throws IOException IOException */ public static Resources loadResources(ZipFile zipFile, String defaultHtmlEncoding) throws IOException { List ls = new ArrayList<>(); return loadResources(zipFile, defaultHtmlEncoding, ls); } } ================================================ FILE: src/main/java/me/ag2s/epublib/util/CollectionUtil.java ================================================ package me.ag2s.epublib.util; import java.util.Collection; import java.util.Enumeration; import java.util.Iterator; import java.util.List; public class CollectionUtil { /** * Wraps an Enumeration around an Iterator * @author paul.siegmann * * @param */ private static class IteratorEnumerationAdapter implements Enumeration { private final Iterator iterator; public IteratorEnumerationAdapter(Iterator iter) { this.iterator = iter; } @Override public boolean hasMoreElements() { return iterator.hasNext(); } @Override public T nextElement() { return iterator.next(); } } /** * Creates an Enumeration out of the given Iterator. * @param g * @param it g * @return an Enumeration created out of the given Iterator. */ @SuppressWarnings("unused") public static Enumeration createEnumerationFromIterator( Iterator it) { return new IteratorEnumerationAdapter<>(it); } /** * Returns the first element of the list, null if the list is null or empty. * * @param f * @param list f * @return the first element of the list, null if the list is null or empty. */ public static T first(List list) { if (list == null || list.isEmpty()) { return null; } return list.get(0); } /** * Whether the given collection is null or has no elements. * * @param collection g * @return Whether the given collection is null or has no elements. */ public static boolean isEmpty(Collection collection) { return collection == null || collection.isEmpty(); } } ================================================ FILE: src/main/java/me/ag2s/epublib/util/IOUtil.java ================================================ package me.ag2s.epublib.util; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.StringWriter; import java.io.Writer; import java.net.HttpURLConnection; import java.net.URLConnection; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import me.ag2s.epublib.epub.PackageDocumentReader; import me.ag2s.epublib.util.commons.io.IOConsumer; /** * Most of the functions herein are re-implementations of the ones in * apache io IOUtils. *

* The reason for re-implementing this is that the functions are fairly simple * and using my own implementation saves the inclusion of a 200Kb jar file. */ public class IOUtil { private static final String TAG = IOUtil.class.getName(); /** * Represents the end-of-file (or stream). * * @since 2.5 (made public) */ public static final int EOF = -1; public static final int DEFAULT_BUFFER_SIZE = 1024 * 8; private static final byte[] SKIP_BYTE_BUFFER = new byte[DEFAULT_BUFFER_SIZE]; // Allocated in the relevant skip method if necessary. /* * These buffers are static and are shared between threads. * This is possible because the buffers are write-only - the contents are never read. * * N.B. there is no need to synchronize when creating these because: * - we don't care if the buffer is created multiple times (the data is ignored) * - we always use the same size buffer, so if it it is recreated it will still be OK * (if the buffer size were variable, we would need to synch. to ensure some other thread * did not create a smaller one) */ private static char[] SKIP_CHAR_BUFFER; /** * Gets the contents of the Reader as a byte[], with the given character encoding. * * @param in g * @param encoding g * @return the contents of the Reader as a byte[], with the given character encoding. * @throws IOException g */ public static byte[] toByteArray(Reader in, String encoding) throws IOException { StringWriter out = new StringWriter(); copy(in, out); out.flush(); return out.toString().getBytes(encoding); } /** * Returns the contents of the InputStream as a byte[] * * @param in f * @return the contents of the InputStream as a byte[] * @throws IOException f */ public static byte[] toByteArray(InputStream in) throws IOException { ByteArrayOutputStream result = new ByteArrayOutputStream(); copy(in, result); result.flush(); return result.toByteArray(); } /** * Reads data from the InputStream, using the specified buffer size. *

* This is meant for situations where memory is tight, since * it prevents buffer expansion. * * @param in the stream to read data from * @param size the size of the array to create * @return the array, or null * @throws IOException f */ public static byte[] toByteArray(InputStream in, int size) throws IOException { try { ByteArrayOutputStream result; if (size > 0) { result = new ByteArrayOutputStream(size); } else { result = new ByteArrayOutputStream(); } copy(in, result); result.flush(); return result.toByteArray(); } catch (OutOfMemoryError error) { //Return null so it gets loaded lazily. return null; } } /** * if totalNrRead < 0 then totalNrRead is returned, if * (nrRead + totalNrRead) < Integer.MAX_VALUE then nrRead + totalNrRead * is returned, -1 otherwise. * * @param nrRead f * @param totalNrNread f * @return if totalNrRead < 0 then totalNrRead is returned, if * (nrRead + totalNrRead) < Integer.MAX_VALUE then nrRead + totalNrRead * is returned, -1 otherwise. */ protected static int calcNewNrReadSize(int nrRead, int totalNrNread) { if (totalNrNread < 0) { return totalNrNread; } if (totalNrNread > (Integer.MAX_VALUE - nrRead)) { return -1; } else { return (totalNrNread + nrRead); } } // public static void copy(InputStream in, OutputStream result) throws IOException { copy(in, result,DEFAULT_BUFFER_SIZE); } /** * Copies bytes from an InputStream to an OutputStream using an internal buffer of the * given size. *

* This method buffers the input internally, so there is no need to use a BufferedInputStream. *

* * @param input the InputStream to read from * @param output the OutputStream to write to * @param bufferSize the bufferSize used to copy from the input to the output * @return the number of bytes copied. or {@code 0} if {@code input is null}. * @throws NullPointerException if the output is null * @throws IOException if an I/O error occurs * @since 2.5 */ public static long copy(final InputStream input, final OutputStream output, final int bufferSize) throws IOException { return copyLarge(input, output, new byte[bufferSize]); } /** * Copies bytes from an InputStream to chars on a * Writer using the default character encoding of the platform. *

* This method buffers the input internally, so there is no need to use a * BufferedInputStream. *

* This method uses {@link InputStreamReader}. * * @param input the InputStream to read from * @param output the Writer to write to * @throws NullPointerException if the input or output is null * @throws IOException if an I/O error occurs * @since 1.1 * @deprecated 2.5 use {@link #copy(InputStream, Writer, Charset)} instead */ @Deprecated public static void copy(final InputStream input, final Writer output) throws IOException { copy(input, output, Charset.defaultCharset()); } /** * Copies bytes from an InputStream to chars on a * Writer using the specified character encoding. *

* This method buffers the input internally, so there is no need to use a * BufferedInputStream. *

* This method uses {@link InputStreamReader}. * * @param input the InputStream to read from * @param output the Writer to write to * @param inputCharset the charset to use for the input stream, null means platform default * @throws NullPointerException if the input or output is null * @throws IOException if an I/O error occurs * @since 2.3 */ public static void copy(final InputStream input, final Writer output, final Charset inputCharset) throws IOException { final InputStreamReader in = new InputStreamReader(input, inputCharset.name()); copy(in, output); } /** * Copies bytes from an InputStream to chars on a * Writer using the specified character encoding. *

* This method buffers the input internally, so there is no need to use a * BufferedInputStream. *

* Character encoding names can be found at * IANA. *

* This method uses {@link InputStreamReader}. * * @param input the InputStream to read from * @param output the Writer to write to * @param inputCharsetName the name of the requested charset for the InputStream, null means platform default * @throws NullPointerException if the input or output is null * @throws IOException if an I/O error occurs * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io * .UnsupportedEncodingException} in version 2.2 if the * encoding is not supported. * @since 1.1 */ public static void copy(final InputStream input, final Writer output, final String inputCharsetName) throws IOException { copy(input, output, Charset.forName(inputCharsetName)); } /** * Copies chars from a Reader to a Appendable. *

* This method buffers the input internally, so there is no need to use a * BufferedReader. *

* Large streams (over 2GB) will return a chars copied value of * -1 after the copy has completed since the correct * number of chars cannot be returned as an int. For large streams * use the copyLarge(Reader, Writer) method. * * @param input the Reader to read from * @param output the Appendable to write to * @return the number of characters copied, or -1 if > Integer.MAX_VALUE * @throws NullPointerException if the input or output is null * @throws IOException if an I/O error occurs * @since 2.7 */ public static long copy(final Reader input, final Appendable output) throws IOException { return copy(input, output, CharBuffer.allocate(DEFAULT_BUFFER_SIZE)); } /** * Copies chars from a Reader to an Appendable. *

* This method uses the provided buffer, so there is no need to use a * BufferedReader. *

* * @param input the Reader to read from * @param output the Appendable to write to * @param buffer the buffer to be used for the copy * @return the number of characters copied * @throws NullPointerException if the input or output is null * @throws IOException if an I/O error occurs * @since 2.7 */ public static long copy(final Reader input, final Appendable output, final CharBuffer buffer) throws IOException { long count = 0; int n; while (EOF != (n = input.read(buffer))) { buffer.flip(); output.append(buffer, 0, n); count += n; } return count; } /** * Copies chars from a Reader to bytes on an * OutputStream using the default character encoding of the * platform, and calling flush. *

* This method buffers the input internally, so there is no need to use a * BufferedReader. *

* Due to the implementation of OutputStreamWriter, this method performs a * flush. *

* This method uses {@link OutputStreamWriter}. * * @param input the Reader to read from * @param output the OutputStream to write to * @throws NullPointerException if the input or output is null * @throws IOException if an I/O error occurs * @since 1.1 * @deprecated 2.5 use {@link #copy(Reader, OutputStream, Charset)} instead */ @Deprecated public static void copy(final Reader input, final OutputStream output) throws IOException { copy(input, output, Charset.defaultCharset()); } /** * Copies chars from a Reader to bytes on an * OutputStream using the specified character encoding, and * calling flush. *

* This method buffers the input internally, so there is no need to use a * BufferedReader. *

*

* Due to the implementation of OutputStreamWriter, this method performs a * flush. *

*

* This method uses {@link OutputStreamWriter}. *

* * @param input the Reader to read from * @param output the OutputStream to write to * @param outputCharset the charset to use for the OutputStream, null means platform default * @throws NullPointerException if the input or output is null * @throws IOException if an I/O error occurs * @since 2.3 */ public static void copy(final Reader input, final OutputStream output, final Charset outputCharset) throws IOException { final OutputStreamWriter out = new OutputStreamWriter(output, outputCharset.name()); copy(input, out); // XXX Unless anyone is planning on rewriting OutputStreamWriter, // we have to flush here. out.flush(); } /** * Copies chars from a Reader to bytes on an * OutputStream using the specified character encoding, and * calling flush. *

* This method buffers the input internally, so there is no need to use a * BufferedReader. *

* Character encoding names can be found at * IANA. *

* Due to the implementation of OutputStreamWriter, this method performs a * flush. *

* This method uses {@link OutputStreamWriter}. * * @param input the Reader to read from * @param output the OutputStream to write to * @param outputCharsetName the name of the requested charset for the OutputStream, null means platform default * @throws NullPointerException if the input or output is null * @throws IOException if an I/O error occurs * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io * .UnsupportedEncodingException} in version 2.2 if the * encoding is not supported. * @since 1.1 */ public static void copy(final Reader input, final OutputStream output, final String outputCharsetName) throws IOException { copy(input, output, Charset.forName(outputCharsetName)); } /** * Copies chars from a Reader to a Writer. *

* This method buffers the input internally, so there is no need to use a * BufferedReader. *

* Large streams (over 2GB) will return a chars copied value of * -1 after the copy has completed since the correct * number of chars cannot be returned as an int. For large streams * use the copyLarge(Reader, Writer) method. * * @param input the Reader to read from * @param output the Writer to write to * @return the number of characters copied, or -1 if > Integer.MAX_VALUE * @throws NullPointerException if the input or output is null * @throws IOException if an I/O error occurs * @since 1.1 */ public static int copy(final Reader input, final Writer output) throws IOException { final long count = copyLarge(input, output); if (count > Integer.MAX_VALUE) { return -1; } return (int) count; } /** * Copies bytes from a large (over 2GB) InputStream to an * OutputStream. *

* This method buffers the input internally, so there is no need to use a * BufferedInputStream. *

*

* The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. *

* * @param input the InputStream to read from * @param output the OutputStream to write to * @return the number of bytes copied. or {@code 0} if {@code input is null}. * @throws NullPointerException if the output is null * @throws IOException if an I/O error occurs * @since 1.3 */ public static long copyLarge(final InputStream input, final OutputStream output) throws IOException { return copy(input, output, DEFAULT_BUFFER_SIZE); } /** * Copies bytes from a large (over 2GB) InputStream to an * OutputStream. *

* This method uses the provided buffer, so there is no need to use a * BufferedInputStream. *

* * @param input the InputStream to read from * @param output the OutputStream to write to * @param buffer the buffer to use for the copy * @return the number of bytes copied. or {@code 0} if {@code input is null}. * @throws IOException if an I/O error occurs * @since 2.2 */ public static long copyLarge(final InputStream input, final OutputStream output, final byte[] buffer) throws IOException { long count = 0; if (input != null) { int n; while (EOF != (n = input.read(buffer))) { output.write(buffer, 0, n); count += n; } //input.close(); } return count; } /** * Copies some or all bytes from a large (over 2GB) InputStream to an * OutputStream, optionally skipping input bytes. *

* This method buffers the input internally, so there is no need to use a * BufferedInputStream. *

*

* Note that the implementation uses {@link #skip(InputStream, long)}. * This means that the method may be considerably less efficient than using the actual skip implementation, * this is done to guarantee that the correct number of characters are skipped. *

* The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. * * @param input the InputStream to read from * @param output the OutputStream to write to * @param inputOffset : number of bytes to skip from input before copying * -ve values are ignored * @param length : number of bytes to copy. -ve means all * @return the number of bytes copied * @throws NullPointerException if the input or output is null * @throws IOException if an I/O error occurs * @since 2.2 */ public static long copyLarge(final InputStream input, final OutputStream output, final long inputOffset, final long length) throws IOException { return copyLarge(input, output, inputOffset, length, new byte[DEFAULT_BUFFER_SIZE]); } /** * Copies some or all bytes from a large (over 2GB) InputStream to an * OutputStream, optionally skipping input bytes. *

* This method uses the provided buffer, so there is no need to use a * BufferedInputStream. *

*

* Note that the implementation uses {@link #skip(InputStream, long)}. * This means that the method may be considerably less efficient than using the actual skip implementation, * this is done to guarantee that the correct number of characters are skipped. *

* * @param input the InputStream to read from * @param output the OutputStream to write to * @param inputOffset : number of bytes to skip from input before copying * -ve values are ignored * @param length : number of bytes to copy. -ve means all * @param buffer the buffer to use for the copy * @return the number of bytes copied * @throws NullPointerException if the input or output is null * @throws IOException if an I/O error occurs * @since 2.2 */ public static long copyLarge(final InputStream input, final OutputStream output, final long inputOffset, final long length, final byte[] buffer) throws IOException { if (inputOffset > 0) { skipFully(input, inputOffset); } if (length == 0) { return 0; } final int bufferLength = buffer.length; int bytesToRead = bufferLength; if (length > 0 && length < bufferLength) { bytesToRead = (int) length; } int read; long totalRead = 0; while (bytesToRead > 0 && EOF != (read = input.read(buffer, 0, bytesToRead))) { output.write(buffer, 0, read); totalRead += read; if (length > 0) { // only adjust length if not reading to the end // Note the cast must work because buffer.length is an integer bytesToRead = (int) Math.min(length - totalRead, bufferLength); } } return totalRead; } /** * Copies chars from a large (over 2GB) Reader to a Writer. *

* This method buffers the input internally, so there is no need to use a * BufferedReader. *

* The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. * * @param input the Reader to read from * @param output the Writer to write to * @return the number of characters copied * @throws NullPointerException if the input or output is null * @throws IOException if an I/O error occurs * @since 1.3 */ public static long copyLarge(final Reader input, final Writer output) throws IOException { return copyLarge(input, output, new char[DEFAULT_BUFFER_SIZE]); } /** * Copies chars from a large (over 2GB) Reader to a Writer. *

* This method uses the provided buffer, so there is no need to use a * BufferedReader. *

* * @param input the Reader to read from * @param output the Writer to write to * @param buffer the buffer to be used for the copy * @return the number of characters copied * @throws NullPointerException if the input or output is null * @throws IOException if an I/O error occurs * @since 2.2 */ public static long copyLarge(final Reader input, final Writer output, final char[] buffer) throws IOException { long count = 0; int n; while (EOF != (n = input.read(buffer))) { output.write(buffer, 0, n); count += n; } return count; } /** * Copies some or all chars from a large (over 2GB) InputStream to an * OutputStream, optionally skipping input chars. *

* This method buffers the input internally, so there is no need to use a * BufferedReader. *

* The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. * * @param input the Reader to read from * @param output the Writer to write to * @param inputOffset : number of chars to skip from input before copying * -ve values are ignored * @param length : number of chars to copy. -ve means all * @return the number of chars copied * @throws NullPointerException if the input or output is null * @throws IOException if an I/O error occurs * @since 2.2 */ public static long copyLarge(final Reader input, final Writer output, final long inputOffset, final long length) throws IOException { return copyLarge(input, output, inputOffset, length, new char[DEFAULT_BUFFER_SIZE]); } /** * Copies some or all chars from a large (over 2GB) InputStream to an * OutputStream, optionally skipping input chars. *

* This method uses the provided buffer, so there is no need to use a * BufferedReader. *

* * @param input the Reader to read from * @param output the Writer to write to * @param inputOffset : number of chars to skip from input before copying * -ve values are ignored * @param length : number of chars to copy. -ve means all * @param buffer the buffer to be used for the copy * @return the number of chars copied * @throws NullPointerException if the input or output is null * @throws IOException if an I/O error occurs * @since 2.2 */ public static long copyLarge(final Reader input, final Writer output, final long inputOffset, final long length, final char[] buffer) throws IOException { if (inputOffset > 0) { skipFully(input, inputOffset); } if (length == 0) { return 0; } int bytesToRead = buffer.length; if (length > 0 && length < buffer.length) { bytesToRead = (int) length; } int read; long totalRead = 0; while (bytesToRead > 0 && EOF != (read = input.read(buffer, 0, bytesToRead))) { output.write(buffer, 0, read); totalRead += read; if (length > 0) { // only adjust length if not reading to the end // Note the cast must work because buffer.length is an integer bytesToRead = (int) Math.min(length - totalRead, buffer.length); } } return totalRead; } /** * Skips bytes from an input byte stream. * This implementation guarantees that it will read as many bytes * as possible before giving up; this may not always be the case for * skip() implementations in subclasses of {@link InputStream}. *

* Note that the implementation uses {@link InputStream#read(byte[], int, int)} rather * than delegating to {@link InputStream#skip(long)}. * This means that the method may be considerably less efficient than using the actual skip implementation, * this is done to guarantee that the correct number of bytes are skipped. *

* * @param input byte stream to skip * @param toSkip number of bytes to skip. * @return number of bytes actually skipped. * @throws IOException if there is a problem reading the file * @throws IllegalArgumentException if toSkip is negative * @see InputStream#skip(long) * @see IO-203 - Add skipFully() method for InputStreams * @since 2.0 */ public static long skip(final InputStream input, final long toSkip) throws IOException { if (toSkip < 0) { throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip); } /* * N.B. no need to synchronize access to SKIP_BYTE_BUFFER: - we don't care if the buffer is created multiple * times (the data is ignored) - we always use the same size buffer, so if it it is recreated it will still be * OK (if the buffer size were variable, we would need to synch. to ensure some other thread did not create a * smaller one) */ long remain = toSkip; while (remain > 0) { // See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip() final long n = input.read(SKIP_BYTE_BUFFER, 0, (int) Math.min(remain, SKIP_BYTE_BUFFER.length)); if (n < 0) { // EOF break; } remain -= n; } return toSkip - remain; } /** * Skips bytes from a ReadableByteChannel. * This implementation guarantees that it will read as many bytes * as possible before giving up. * * @param input ReadableByteChannel to skip * @param toSkip number of bytes to skip. * @return number of bytes actually skipped. * @throws IOException if there is a problem reading the ReadableByteChannel * @throws IllegalArgumentException if toSkip is negative * @since 2.5 */ public static long skip(final ReadableByteChannel input, final long toSkip) throws IOException { if (toSkip < 0) { throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip); } final ByteBuffer skipByteBuffer = ByteBuffer.allocate((int) Math.min(toSkip, SKIP_BYTE_BUFFER.length)); long remain = toSkip; while (remain > 0) { skipByteBuffer.position(0); skipByteBuffer.limit((int) Math.min(remain, SKIP_BYTE_BUFFER.length)); final int n = input.read(skipByteBuffer); if (n == EOF) { break; } remain -= n; } return toSkip - remain; } /** * Skips characters from an input character stream. * This implementation guarantees that it will read as many characters * as possible before giving up; this may not always be the case for * skip() implementations in subclasses of {@link Reader}. *

* Note that the implementation uses {@link Reader#read(char[], int, int)} rather * than delegating to {@link Reader#skip(long)}. * This means that the method may be considerably less efficient than using the actual skip implementation, * this is done to guarantee that the correct number of characters are skipped. *

* * @param input character stream to skip * @param toSkip number of characters to skip. * @return number of characters actually skipped. * @throws IOException if there is a problem reading the file * @throws IllegalArgumentException if toSkip is negative * @see Reader#skip(long) * @see IO-203 - Add skipFully() method for InputStreams * @since 2.0 */ public static long skip(final Reader input, final long toSkip) throws IOException { if (toSkip < 0) { throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip); } /* * N.B. no need to synchronize this because: - we don't care if the buffer is created multiple times (the data * is ignored) - we always use the same size buffer, so if it it is recreated it will still be OK (if the buffer * size were variable, we would need to synch. to ensure some other thread did not create a smaller one) */ if (SKIP_CHAR_BUFFER == null) { SKIP_CHAR_BUFFER = new char[SKIP_BYTE_BUFFER.length]; } long remain = toSkip; while (remain > 0) { // See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip() final long n = input.read(SKIP_CHAR_BUFFER, 0, (int) Math.min(remain, SKIP_BYTE_BUFFER.length)); if (n < 0) { // EOF break; } remain -= n; } return toSkip - remain; } /** * Skips the requested number of bytes or fail if there are not enough left. *

* This allows for the possibility that {@link InputStream#skip(long)} may * not skip as many bytes as requested (most likely because of reaching EOF). *

* Note that the implementation uses {@link #skip(InputStream, long)}. * This means that the method may be considerably less efficient than using the actual skip implementation, * this is done to guarantee that the correct number of characters are skipped. *

* * @param input stream to skip * @param toSkip the number of bytes to skip * @throws IOException if there is a problem reading the file * @throws IllegalArgumentException if toSkip is negative * @throws EOFException if the number of bytes skipped was incorrect * @see InputStream#skip(long) * @since 2.0 */ public static void skipFully(final InputStream input, final long toSkip) throws IOException { if (toSkip < 0) { throw new IllegalArgumentException("Bytes to skip must not be negative: " + toSkip); } final long skipped = skip(input, toSkip); if (skipped != toSkip) { throw new EOFException("Bytes to skip: " + toSkip + " actual: " + skipped); } } /** * Skips the requested number of bytes or fail if there are not enough left. * * @param input ReadableByteChannel to skip * @param toSkip the number of bytes to skip * @throws IOException if there is a problem reading the ReadableByteChannel * @throws IllegalArgumentException if toSkip is negative * @throws EOFException if the number of bytes skipped was incorrect * @since 2.5 */ public static void skipFully(final ReadableByteChannel input, final long toSkip) throws IOException { if (toSkip < 0) { throw new IllegalArgumentException("Bytes to skip must not be negative: " + toSkip); } final long skipped = skip(input, toSkip); if (skipped != toSkip) { throw new EOFException("Bytes to skip: " + toSkip + " actual: " + skipped); } } /** * Skips the requested number of characters or fail if there are not enough left. *

* This allows for the possibility that {@link Reader#skip(long)} may * not skip as many characters as requested (most likely because of reaching EOF). *

* Note that the implementation uses {@link #skip(Reader, long)}. * This means that the method may be considerably less efficient than using the actual skip implementation, * this is done to guarantee that the correct number of characters are skipped. *

* * @param input stream to skip * @param toSkip the number of characters to skip * @throws IOException if there is a problem reading the file * @throws IllegalArgumentException if toSkip is negative * @throws EOFException if the number of characters skipped was incorrect * @see Reader#skip(long) * @since 2.0 */ public static void skipFully(final Reader input, final long toSkip) throws IOException { final long skipped = skip(input, toSkip); if (skipped != toSkip) { throw new EOFException("Chars to skip: " + toSkip + " actual: " + skipped); } } /** * Returns the length of the given array in a null-safe manner. * * @param array an array or null * @return the array length -- or 0 if the given array is null. * @since 2.7 */ public static int length(final byte[] array) { return array == null ? 0 : array.length; } /** * Returns the length of the given array in a null-safe manner. * * @param array an array or null * @return the array length -- or 0 if the given array is null. * @since 2.7 */ public static int length(final char[] array) { return array == null ? 0 : array.length; } /** * Returns the length of the given CharSequence in a null-safe manner. * * @param csq a CharSequence or null * @return the CharSequence length -- or 0 if the given CharSequence is null. * @since 2.7 */ public static int length(final CharSequence csq) { return csq == null ? 0 : csq.length(); } /** * Returns the length of the given array in a null-safe manner. * * @param array an array or null * @return the array length -- or 0 if the given array is null. * @since 2.7 */ public static int length(final Object[] array) { return array == null ? 0 : array.length; } /** * Closes the given {@link Closeable} as a null-safe operation. * * @param closeable The resource to close, may be null. * @throws IOException if an I/O error occurs. * @since 2.7 */ public static void close(final Closeable closeable) throws IOException { if (closeable != null) { closeable.close(); } } /** * Closes the given {@link Closeable} as a null-safe operation. * * @param closeables The resource(s) to close, may be null. * @throws IOException if an I/O error occurs. * @since 2.8.0 */ public static void close(final Closeable... closeables) throws IOException { if (closeables != null) { for (final Closeable closeable : closeables) { close(closeable); } } } /** * Closes the given {@link Closeable} as a null-safe operation. * * @param closeable The resource to close, may be null. * @param consumer Consume the IOException thrown by {@link Closeable#close()}. * @throws IOException if an I/O error occurs. * @since 2.7 */ public static void close(final Closeable closeable, final IOConsumer consumer) throws IOException { if (closeable != null) { try { closeable.close(); } catch (final IOException e) { if (consumer != null) { consumer.accept(e); } } } } /** * Closes a URLConnection. * * @param conn the connection to close. * @since 2.4 */ public static void close(final URLConnection conn) { if (conn instanceof HttpURLConnection) { ((HttpURLConnection) conn).disconnect(); } } @SuppressWarnings("unused") public static String Stream2String(InputStream inputStream) { ByteArrayOutputStream result = new ByteArrayOutputStream(); try { byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; int length; while ((length = inputStream.read(buffer)) != -1) { result.write(buffer, 0, length); } return result.toString(); } catch (Exception e) { return e.getLocalizedMessage(); } } } ================================================ FILE: src/main/java/me/ag2s/epublib/util/NoCloseOutputStream.java ================================================ package me.ag2s.epublib.util; import java.io.IOException; import java.io.OutputStream; /** * OutputStream with the close() disabled. * We write multiple documents to a ZipOutputStream. * Some of the formatters call a close() after writing their data. * We don't want them to do that, so we wrap regular OutputStreams in this NoCloseOutputStream. * * @author paul */ @SuppressWarnings("unused") public class NoCloseOutputStream extends OutputStream { private final OutputStream outputStream; public NoCloseOutputStream(OutputStream outputStream) { this.outputStream = outputStream; } @Override public void write(int b) throws IOException { outputStream.write(b); } /** * A close() that does not call it's parent's close() */ public void close() { } } ================================================ FILE: src/main/java/me/ag2s/epublib/util/NoCloseWriter.java ================================================ package me.ag2s.epublib.util; import java.io.IOException; import java.io.Writer; /** * Writer with the close() disabled. * We write multiple documents to a ZipOutputStream. * Some of the formatters call a close() after writing their data. * We don't want them to do that, so we wrap regular Writers in this NoCloseWriter. * * @author paul */ @SuppressWarnings("unused") public class NoCloseWriter extends Writer { private final Writer writer; public NoCloseWriter(Writer writer) { this.writer = writer; } @Override public void close() { } @Override public void flush() throws IOException { writer.flush(); } @Override public void write(char[] cbuf, int off, int len) throws IOException { writer.write(cbuf, off, len); } } ================================================ FILE: src/main/java/me/ag2s/epublib/util/ResourceUtil.java ================================================ package me.ag2s.epublib.util; import org.w3c.dom.Document; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import javax.xml.parsers.DocumentBuilder; import me.ag2s.epublib.Constants; import me.ag2s.epublib.domain.MediaType; import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.epub.EpubProcessorSupport; /** * Various resource utility methods * * @author paul */ public class ResourceUtil { /** * 快速创建HTML类型的Resource * * @param title 章节的标题 * @param txt 章节的正文 * @param model html模板 * @return 返回Resource */ public static Resource createChapterResource(String title, String txt, String model, String href) { if (title.contains("\n")) { title = "" + title.replaceFirst("\\s*\\n\\s*", "
"); } else { title = title.replaceFirst("\\s+", "
"); if (title.contains("")) title = "" + title; } String html = model.replace("{title}", title) .replace("{content}", StringUtil.formatHtml(txt)); return new Resource(html.getBytes(), href); } public static Resource createPublicResource(String name, String author, String intro, String kind, String wordCount, String model, String href) { String html = model.replace("{name}", name) .replace("{author}", author) .replace("{kind}", kind == null ? "" : kind) .replace("{wordCount}", wordCount == null ? "" : wordCount) .replace("{intro}", StringUtil.formatHtml(intro == null ? "" : intro)); return new Resource(html.getBytes(), href); } /** * 快速从File创建Resource * * @param file File * @return Resource * @throws IOException IOException */ @SuppressWarnings("unused") public static Resource createResource(File file) throws IOException { if (file == null) { return null; } MediaType mediaType = MediaTypes.determineMediaType(file.getName()); byte[] data = IOUtil.toByteArray(new FileInputStream(file)); return new Resource(data, mediaType); } /** * 创建一个只带标题的HTMl类型的Resource,常用于封面页,大卷页 * * @param title v * @param href v * @return a resource with as contents a html page with the given title. */ @SuppressWarnings("unused") public static Resource createResource(String title, String href) { String content = "" + title + "

" + title + "

"; return new Resource(null, content.getBytes(), href, MediaTypes.XHTML, Constants.CHARACTER_ENCODING); } /** * Creates a resource out of the given zipEntry and zipInputStream. * * @param zipEntry v * @param zipInputStream v * @return a resource created out of the given zipEntry and zipInputStream. * @throws IOException v */ public static Resource createResource(ZipEntry zipEntry, ZipInputStream zipInputStream) throws IOException { return new Resource(zipInputStream, zipEntry.getName()); } public static Resource createResource(ZipEntry zipEntry, InputStream zipInputStream) throws IOException { return new Resource(zipInputStream, zipEntry.getName()); } /** * Converts a given string from given input character encoding to the requested output character encoding. * * @param inputEncoding v * @param outputEncoding v * @param input v * @return the string from given input character encoding converted to the requested output character encoding. * @throws UnsupportedEncodingException v */ @SuppressWarnings("unused") public static byte[] recode(String inputEncoding, String outputEncoding, byte[] input) throws UnsupportedEncodingException { return new String(input, inputEncoding).getBytes(outputEncoding); } /** * Gets the contents of the Resource as an InputSource in a null-safe manner. */ @SuppressWarnings("unused") public static InputSource getInputSource(Resource resource) throws IOException { if (resource == null) { return null; } Reader reader = resource.getReader(); if (reader == null) { return null; } return new InputSource(reader); } /** * Reads parses the xml therein and returns the result as a Document */ public static Document getAsDocument(Resource resource) throws SAXException, IOException { return getAsDocument(resource, EpubProcessorSupport.createDocumentBuilder()); } /** * Reads the given resources inputstream, parses the xml therein and returns the result as a Document * * @param resource v * @param documentBuilder v * @return the document created from the given resource * @throws UnsupportedEncodingException v * @throws SAXException v * @throws IOException v */ public static Document getAsDocument(Resource resource, DocumentBuilder documentBuilder) throws UnsupportedEncodingException, SAXException, IOException { InputSource inputSource = getInputSource(resource); if (inputSource == null) { return null; } return documentBuilder.parse(inputSource); } } ================================================ FILE: src/main/java/me/ag2s/epublib/util/StringUtil.java ================================================ package me.ag2s.epublib.util; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Various String utility functions. *

* Most of the functions herein are re-implementations of the ones in apache * commons StringUtils. The reason for re-implementing this is that the * functions are fairly simple and using my own implementation saves the * inclusion of a 200Kb jar file. * * @author paul.siegmann */ public class StringUtil { /** * Changes a path containing '..', '.' and empty dirs into a path that * doesn't. X/foo/../Y is changed into 'X/Y', etc. Does not handle invalid * paths like "../". * * @param path path * @return the normalized path */ public static String collapsePathDots(String path) { String[] stringParts = path.split("/"); List parts = new ArrayList<>(Arrays.asList(stringParts)); for (int i = 0; i < parts.size() - 1; i++) { String currentDir = parts.get(i); if (currentDir.length() == 0 || currentDir.equals(".")) { parts.remove(i); i--; } else if (currentDir.equals("..")) { parts.remove(i - 1); parts.remove(i - 1); i -= 2; } } StringBuilder result = new StringBuilder(); if (path.startsWith("/")) { result.append('/'); } for (int i = 0; i < parts.size(); i++) { result.append(parts.get(i)); if (i < (parts.size() - 1)) { result.append('/'); } } return result.toString(); } /** * Whether the String is not null, not zero-length and does not contain of * only whitespace. * * @param text text * @return Whether the String is not null, not zero-length and does not contain of */ public static boolean isNotBlank(String text) { return !isBlank(text); } /** * Whether the String is null, zero-length and does contain only whitespace. * * @return Whether the String is null, zero-length and does contain only whitespace. */ public static boolean isBlank(String text) { if (isEmpty(text)) { return true; } for (int i = 0; i < text.length(); i++) { if (!Character.isWhitespace(text.charAt(i))) { return false; } } return true; } /** * Whether the given string is null or zero-length. * * @param text the input for this method * @return Whether the given string is null or zero-length. */ public static boolean isEmpty(String text) { return (text == null) || (text.length() == 0); } /** * Whether the given source string ends with the given suffix, ignoring * case. * * @param source source * @param suffix suffix * @return Whether the given source string ends with the given suffix, ignoring case. */ public static boolean endsWithIgnoreCase(String source, String suffix) { if (isEmpty(suffix)) { return true; } if (isEmpty(source)) { return false; } if (suffix.length() > source.length()) { return false; } return source.substring(source.length() - suffix.length()) .toLowerCase().endsWith(suffix.toLowerCase()); } /** * If the given text is null return "", the original text otherwise. * * @param text text * @return If the given text is null "", the original text otherwise. */ public static String defaultIfNull(String text) { return defaultIfNull(text, ""); } /** * If the given text is null return "", the given defaultValue otherwise. * * @param text d * @param defaultValue d * @return If the given text is null "", the given defaultValue otherwise. */ public static String defaultIfNull(String text, String defaultValue) { if (text == null) { return defaultValue; } return text; } /** * Null-safe string comparator * * @param text1 d * @param text2 d * @return whether the two strings are equal */ public static boolean equals(String text1, String text2) { if (text1 == null) { return (text2 == null); } return text1.equals(text2); } /** * Pretty toString printer. * * @param keyValues d * @return a string representation of the input values */ public static String toString(Object... keyValues) { StringBuilder result = new StringBuilder(); result.append('['); for (int i = 0; i < keyValues.length; i += 2) { if (i > 0) { result.append(", "); } result.append(keyValues[i]); result.append(": "); Object value = null; if ((i + 1) < keyValues.length) { value = keyValues[i + 1]; } if (value == null) { result.append(""); } else { result.append('\''); result.append(value); result.append('\''); } } result.append(']'); return result.toString(); } public static int hashCode(String... values) { int result = 31; for (String value : values) { result ^= String.valueOf(value).hashCode(); } return result; } /** * Gives the substring of the given text before the given separator. *

* If the text does not contain the given separator then the given text is * returned. * * @param text d * @param separator d * @return the substring of the given text before the given separator. */ public static String substringBefore(String text, char separator) { if (isEmpty(text)) { return text; } int sepPos = text.indexOf(separator); if (sepPos < 0) { return text; } return text.substring(0, sepPos); } /** * Gives the substring of the given text before the last occurrence of the * given separator. *

* If the text does not contain the given separator then the given text is * returned. * * @param text d * @param separator d * @return the substring of the given text before the last occurrence of the given separator. */ public static String substringBeforeLast(String text, char separator) { if (isEmpty(text)) { return text; } int cPos = text.lastIndexOf(separator); if (cPos < 0) { return text; } return text.substring(0, cPos); } /** * Gives the substring of the given text after the last occurrence of the * given separator. *

* If the text does not contain the given separator then "" is returned. * * @param text d * @param separator d * @return the substring of the given text after the last occurrence of the given separator. */ public static String substringAfterLast(String text, char separator) { if (isEmpty(text)) { return text; } int cPos = text.lastIndexOf(separator); if (cPos < 0) { return ""; } return text.substring(cPos + 1); } /** * Gives the substring of the given text after the given separator. *

* If the text does not contain the given separator then "" is returned. * * @param text the input text * @param c the separator char * @return the substring of the given text after the given separator. */ public static String substringAfter(String text, char c) { if (isEmpty(text)) { return text; } int cPos = text.indexOf(c); if (cPos < 0) { return ""; } return text.substring(cPos + 1); } public static String formatHtml(String text) { StringBuilder body = new StringBuilder(); for (String s : text.split("\\r?\\n")) { s = s.replaceAll("^\\s+|\\s+$", ""); if (s.length() > 0) { //段落为一张图片才认定为图片章节/漫画并启用多看单图优化,否则认定为普通文字夹杂着的图片文字。 if (s.matches("(?i)^]+)/?>$")) { body.append(s.replaceAll("(?i)^]+)/?>$", "

")); } else { body.append("

").append(s).append("

"); } } } return body.toString(); } } ================================================ FILE: src/main/java/me/ag2s/epublib/util/commons/io/BOMInputStream.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package me.ag2s.epublib.util.commons.io; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.Comparator; import java.util.List; import me.ag2s.epublib.util.IOUtil; import static me.ag2s.epublib.util.IOUtil.EOF; /** * This class is used to wrap a stream that includes an encoded {@link ByteOrderMark} as its first bytes. * * This class detects these bytes and, if required, can automatically skip them and return the subsequent byte as the * first byte in the stream. * * The {@link ByteOrderMark} implementation has the following pre-defined BOMs: *
    *
  • UTF-8 - {@link ByteOrderMark#UTF_8}
  • *
  • UTF-16BE - {@link ByteOrderMark#UTF_16LE}
  • *
  • UTF-16LE - {@link ByteOrderMark#UTF_16BE}
  • *
  • UTF-32BE - {@link ByteOrderMark#UTF_32LE}
  • *
  • UTF-32LE - {@link ByteOrderMark#UTF_32BE}
  • *
* * *

Example 1 - Detect and exclude a UTF-8 BOM

* *
 * BOMInputStream bomIn = new BOMInputStream(in);
 * if (bomIn.hasBOM()) {
 *     // has a UTF-8 BOM
 * }
 * 
* *

Example 2 - Detect a UTF-8 BOM (but don't exclude it)

* *
 * boolean include = true;
 * BOMInputStream bomIn = new BOMInputStream(in, include);
 * if (bomIn.hasBOM()) {
 *     // has a UTF-8 BOM
 * }
 * 
* *

Example 3 - Detect Multiple BOMs

* *
 * BOMInputStream bomIn = new BOMInputStream(in,
 *   ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE,
 *   ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE
 *   );
 * if (bomIn.hasBOM() == false) {
 *     // No BOM found
 * } else if (bomIn.hasBOM(ByteOrderMark.UTF_16LE)) {
 *     // has a UTF-16LE BOM
 * } else if (bomIn.hasBOM(ByteOrderMark.UTF_16BE)) {
 *     // has a UTF-16BE BOM
 * } else if (bomIn.hasBOM(ByteOrderMark.UTF_32LE)) {
 *     // has a UTF-32LE BOM
 * } else if (bomIn.hasBOM(ByteOrderMark.UTF_32BE)) {
 *     // has a UTF-32BE BOM
 * }
 * 
* * @see ByteOrderMark * @see Wikipedia - Byte Order Mark * @since 2.0 */ public class BOMInputStream extends ProxyInputStream { private final boolean include; /** * BOMs are sorted from longest to shortest. */ private final List boms; private ByteOrderMark byteOrderMark; private int[] firstBytes; private int fbLength; private int fbIndex; private int markFbIndex; private boolean markedAtStart; /** * Constructs a new BOM InputStream that excludes a {@link ByteOrderMark#UTF_8} BOM. * * @param delegate * the InputStream to delegate to */ @SuppressWarnings("unused") public BOMInputStream(final InputStream delegate) { this(delegate, false, ByteOrderMark.UTF_8); } /** * Constructs a new BOM InputStream that detects a a {@link ByteOrderMark#UTF_8} and optionally includes it. * * @param delegate * the InputStream to delegate to * @param include * true to include the UTF-8 BOM or false to exclude it */ @SuppressWarnings("unused") public BOMInputStream(final InputStream delegate, final boolean include) { this(delegate, include, ByteOrderMark.UTF_8); } /** * Constructs a new BOM InputStream that excludes the specified BOMs. * * @param delegate * the InputStream to delegate to * @param boms * The BOMs to detect and exclude */ @SuppressWarnings("unused") public BOMInputStream(final InputStream delegate, final ByteOrderMark... boms) { this(delegate, false, boms); } /** * Compares ByteOrderMark objects in descending length order. */ private static final Comparator ByteOrderMarkLengthComparator = (bom1, bom2) -> { final int len1 = bom1.length(); final int len2 = bom2.length(); return Integer.compare(len2, len1); }; /** * Constructs a new BOM InputStream that detects the specified BOMs and optionally includes them. * * @param delegate * the InputStream to delegate to * @param include * true to include the specified BOMs or false to exclude them * @param boms * The BOMs to detect and optionally exclude */ public BOMInputStream(final InputStream delegate, final boolean include, final ByteOrderMark... boms) { super(delegate); if (IOUtil.length(boms) == 0) { throw new IllegalArgumentException("No BOMs specified"); } this.include = include; final List list = Arrays.asList(boms); // Sort the BOMs to match the longest BOM first because some BOMs have the same starting two bytes. // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // list.sort(ByteOrderMarkLengthComparator); // } this.boms = list; } /** * Indicates whether the stream contains one of the specified BOMs. * * @return true if the stream has one of the specified BOMs, otherwise false if it does not * @throws IOException * if an error reading the first bytes of the stream occurs */ @SuppressWarnings("unused") public boolean hasBOM() throws IOException { return getBOM() != null; } /** * Indicates whether the stream contains the specified BOM. * * @param bom * The BOM to check for * @return true if the stream has the specified BOM, otherwise false if it does not * @throws IllegalArgumentException * if the BOM is not one the stream is configured to detect * @throws IOException * if an error reading the first bytes of the stream occurs */ @SuppressWarnings("unused") public boolean hasBOM(final ByteOrderMark bom) throws IOException { if (!boms.contains(bom)) { throw new IllegalArgumentException("Stream not configure to detect " + bom); } getBOM(); return byteOrderMark != null && byteOrderMark.equals(bom); } /** * Return the BOM (Byte Order Mark). * * @return The BOM or null if none * @throws IOException * if an error reading the first bytes of the stream occurs */ public ByteOrderMark getBOM() throws IOException { if (firstBytes == null) { fbLength = 0; // BOMs are sorted from longest to shortest final int maxBomSize = boms.get(0).length(); firstBytes = new int[maxBomSize]; // Read first maxBomSize bytes for (int i = 0; i < firstBytes.length; i++) { firstBytes[i] = in.read(); fbLength++; if (firstBytes[i] < 0) { break; } } // match BOM in firstBytes byteOrderMark = find(); if (byteOrderMark != null) { if (!include) { if (byteOrderMark.length() < firstBytes.length) { fbIndex = byteOrderMark.length(); } else { fbLength = 0; } } } } return byteOrderMark; } /** * Return the BOM charset Name - {@link ByteOrderMark#getCharsetName()}. * * @return The BOM charset Name or null if no BOM found * @throws IOException * if an error reading the first bytes of the stream occurs * */ public String getBOMCharsetName() throws IOException { getBOM(); return byteOrderMark == null ? null : byteOrderMark.getCharsetName(); } /** * This method reads and either preserves or skips the first bytes in the stream. It behaves like the single-byte * read() method, either returning a valid byte or -1 to indicate that the initial bytes have been * processed already. * * @return the byte read (excluding BOM) or -1 if the end of stream * @throws IOException * if an I/O error occurs */ private int readFirstBytes() throws IOException { getBOM(); return fbIndex < fbLength ? firstBytes[fbIndex++] : EOF; } /** * Find a BOM with the specified bytes. * * @return The matched BOM or null if none matched */ private ByteOrderMark find() { for (final ByteOrderMark bom : boms) { if (matches(bom)) { return bom; } } return null; } /** * Check if the bytes match a BOM. * * @param bom * The BOM * @return true if the bytes match the bom, otherwise false */ private boolean matches(final ByteOrderMark bom) { // if (bom.length() != fbLength) { // return false; // } // firstBytes may be bigger than the BOM bytes for (int i = 0; i < bom.length(); i++) { if (bom.get(i) != firstBytes[i]) { return false; } } return true; } // ---------------------------------------------------------------------------- // Implementation of InputStream // ---------------------------------------------------------------------------- /** * Invokes the delegate's read() method, detecting and optionally skipping BOM. * * @return the byte read (excluding BOM) or -1 if the end of stream * @throws IOException * if an I/O error occurs */ @Override public int read() throws IOException { final int b = readFirstBytes(); return b >= 0 ? b : in.read(); } /** * Invokes the delegate's read(byte[], int, int) method, detecting and optionally skipping BOM. * * @param buf * the buffer to read the bytes into * @param off * The start offset * @param len * The number of bytes to read (excluding BOM) * @return the number of bytes read or -1 if the end of stream * @throws IOException * if an I/O error occurs */ @Override public int read(final byte[] buf, int off, int len) throws IOException { int firstCount = 0; int b = 0; while (len > 0 && b >= 0) { b = readFirstBytes(); if (b >= 0) { buf[off++] = (byte) (b & 0xFF); len--; firstCount++; } } final int secondCount = in.read(buf, off, len); return secondCount < 0 ? firstCount > 0 ? firstCount : EOF : firstCount + secondCount; } /** * Invokes the delegate's read(byte[]) method, detecting and optionally skipping BOM. * * @param buf * the buffer to read the bytes into * @return the number of bytes read (excluding BOM) or -1 if the end of stream * @throws IOException * if an I/O error occurs */ @Override public int read(final byte[] buf) throws IOException { return read(buf, 0, buf.length); } /** * Invokes the delegate's mark(int) method. * * @param readlimit * read ahead limit */ @Override public synchronized void mark(final int readlimit) { markFbIndex = fbIndex; markedAtStart = firstBytes == null; in.mark(readlimit); } /** * Invokes the delegate's reset() method. * * @throws IOException * if an I/O error occurs */ @Override public synchronized void reset() throws IOException { fbIndex = markFbIndex; if (markedAtStart) { firstBytes = null; } in.reset(); } /** * Invokes the delegate's skip(long) method, detecting and optionally skipping BOM. * * @param n * the number of bytes to skip * @return the number of bytes to skipped or -1 if the end of stream * @throws IOException * if an I/O error occurs */ @Override public long skip(final long n) throws IOException { int skipped = 0; while ((n > skipped) && (readFirstBytes() >= 0)) { skipped++; } return in.skip(n - skipped) + skipped; } } ================================================ FILE: src/main/java/me/ag2s/epublib/util/commons/io/ByteOrderMark.java ================================================ package me.ag2s.epublib.util.commons.io; /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.io.Serializable; import java.util.Locale; /** * Byte Order Mark (BOM) representation - see {@link BOMInputStream}. * * @see BOMInputStream * @see Wikipedia: Byte Order Mark * @see W3C: Autodetection of Character Encodings * (Non-Normative) * @since 2.0 */ public class ByteOrderMark implements Serializable { private static final long serialVersionUID = 1L; /** UTF-8 BOM */ public static final ByteOrderMark UTF_8 = new ByteOrderMark("UTF-8", 0xEF, 0xBB, 0xBF); /** UTF-16BE BOM (Big-Endian) */ public static final ByteOrderMark UTF_16BE = new ByteOrderMark("UTF-16BE", 0xFE, 0xFF); /** UTF-16LE BOM (Little-Endian) */ public static final ByteOrderMark UTF_16LE = new ByteOrderMark("UTF-16LE", 0xFF, 0xFE); /** * UTF-32BE BOM (Big-Endian) * @since 2.2 */ public static final ByteOrderMark UTF_32BE = new ByteOrderMark("UTF-32BE", 0x00, 0x00, 0xFE, 0xFF); /** * UTF-32LE BOM (Little-Endian) * @since 2.2 */ public static final ByteOrderMark UTF_32LE = new ByteOrderMark("UTF-32LE", 0xFF, 0xFE, 0x00, 0x00); /** * Unicode BOM character; external form depends on the encoding. * @see Byte Order Mark (BOM) FAQ * @since 2.5 */ @SuppressWarnings("unused") public static final char UTF_BOM = '\uFEFF'; private final String charsetName; private final int[] bytes; /** * Construct a new BOM. * * @param charsetName The name of the charset the BOM represents * @param bytes The BOM's bytes * @throws IllegalArgumentException if the charsetName is null or * zero length * @throws IllegalArgumentException if the bytes are null or zero * length */ public ByteOrderMark(final String charsetName, final int... bytes) { if (charsetName == null || charsetName.isEmpty()) { throw new IllegalArgumentException("No charsetName specified"); } if (bytes == null || bytes.length == 0) { throw new IllegalArgumentException("No bytes specified"); } this.charsetName = charsetName; this.bytes = new int[bytes.length]; System.arraycopy(bytes, 0, this.bytes, 0, bytes.length); } /** * Return the name of the {@link java.nio.charset.Charset} the BOM represents. * * @return the character set name */ public String getCharsetName() { return charsetName; } /** * Return the length of the BOM's bytes. * * @return the length of the BOM's bytes */ public int length() { return bytes.length; } /** * The byte at the specified position. * * @param pos The position * @return The specified byte */ public int get(final int pos) { return bytes[pos]; } /** * Return a copy of the BOM's bytes. * * @return a copy of the BOM's bytes */ public byte[] getBytes() { final byte[] copy = new byte[bytes.length]; for (int i = 0; i < bytes.length; i++) { copy[i] = (byte)bytes[i]; } return copy; } /** * Indicates if this BOM's bytes equals another. * * @param obj The object to compare to * @return true if the bom's bytes are equal, otherwise * false */ @Override public boolean equals(final Object obj) { if (!(obj instanceof ByteOrderMark)) { return false; } final ByteOrderMark bom = (ByteOrderMark)obj; if (bytes.length != bom.length()) { return false; } for (int i = 0; i < bytes.length; i++) { if (bytes[i] != bom.get(i)) { return false; } } return true; } /** * Return the hashcode for this BOM. * * @return the hashcode for this BOM. * @see java.lang.Object#hashCode() */ @Override public int hashCode() { int hashCode = getClass().hashCode(); for (final int b : bytes) { hashCode += b; } return hashCode; } /** * Provide a String representation of the BOM. * * @return the length of the BOM's bytes */ @Override @SuppressWarnings("NullableProblems") public String toString() { final StringBuilder builder = new StringBuilder(); builder.append(getClass().getSimpleName()); builder.append('['); builder.append(charsetName); builder.append(": "); for (int i = 0; i < bytes.length; i++) { if (i > 0) { builder.append(","); } builder.append("0x"); builder.append(Integer.toHexString(0xFF & bytes[i]).toUpperCase(Locale.ROOT)); } builder.append(']'); return builder.toString(); } } ================================================ FILE: src/main/java/me/ag2s/epublib/util/commons/io/IOConsumer.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package me.ag2s.epublib.util.commons.io; import java.io.IOException; import java.util.Objects; import java.util.function.Consumer; /** * Like {@link Consumer} but throws {@link IOException}. * * @param the type of the input to the operations. * @since 2.7 */ @FunctionalInterface public interface IOConsumer { /** * Performs this operation on the given argument. * * @param t the input argument * @throws IOException if an I/O error occurs. */ void accept(T t) throws IOException; /** * Returns a composed {@code IoConsumer} that performs, in sequence, this operation followed by the {@code after} * operation. If performing either operation throws an exception, it is relayed to the caller of the composed * operation. If performing this operation throws an exception, the {@code after} operation will not be performed. * * @param after the operation to perform after this operation * @return a composed {@code Consumer} that performs in sequence this operation followed by the {@code after} * operation * @throws NullPointerException if {@code after} is null */ @SuppressWarnings("unused") default IOConsumer andThen(final IOConsumer after) { Objects.requireNonNull(after); return (final T t) -> { accept(t); after.accept(t); }; } } ================================================ FILE: src/main/java/me/ag2s/epublib/util/commons/io/ProxyInputStream.java ================================================ package me.ag2s.epublib.util.commons.io; /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import me.ag2s.epublib.util.IOUtil; import static me.ag2s.epublib.util.IOUtil.EOF; /** * A Proxy stream which acts as expected, that is it passes the method * calls on to the proxied stream and doesn't change which methods are * being called. *

* It is an alternative base class to FilterInputStream * to increase reusability, because FilterInputStream changes the * methods being called, such as read(byte[]) to read(byte[], int, int). *

*

* See the protected methods for ways in which a subclass can easily decorate * a stream with custom pre-, post- or error processing functionality. *

*/ public abstract class ProxyInputStream extends FilterInputStream { /** * Constructs a new ProxyInputStream. * * @param proxy the InputStream to delegate to */ public ProxyInputStream(final InputStream proxy) { super(proxy); // the proxy is stored in a protected superclass variable named 'in' } /** * Invokes the delegate's read() method. * * @return the byte read or -1 if the end of stream * @throws IOException if an I/O error occurs */ @Override public int read() throws IOException { try { beforeRead(1); final int b = in.read(); afterRead(b != EOF ? 1 : EOF); return b; } catch (final IOException e) { handleIOException(e); return EOF; } } /** * Invokes the delegate's read(byte[]) method. * * @param bts the buffer to read the bytes into * @return the number of bytes read or EOF if the end of stream * @throws IOException if an I/O error occurs */ @Override public int read(final byte[] bts) throws IOException { try { beforeRead(IOUtil.length(bts)); final int n = in.read(bts); afterRead(n); return n; } catch (final IOException e) { handleIOException(e); return EOF; } } /** * Invokes the delegate's read(byte[], int, int) method. * * @param bts the buffer to read the bytes into * @param off The start offset * @param len The number of bytes to read * @return the number of bytes read or -1 if the end of stream * @throws IOException if an I/O error occurs */ @Override public int read(final byte[] bts, final int off, final int len) throws IOException { try { beforeRead(len); final int n = in.read(bts, off, len); afterRead(n); return n; } catch (final IOException e) { handleIOException(e); return EOF; } } /** * Invokes the delegate's skip(long) method. * * @param ln the number of bytes to skip * @return the actual number of bytes skipped * @throws IOException if an I/O error occurs */ @Override public long skip(final long ln) throws IOException { try { return in.skip(ln); } catch (final IOException e) { handleIOException(e); return 0; } } /** * Invokes the delegate's available() method. * * @return the number of available bytes * @throws IOException if an I/O error occurs */ @Override public int available() throws IOException { try { return super.available(); } catch (final IOException e) { handleIOException(e); return 0; } } /** * Invokes the delegate's close() method. * * @throws IOException if an I/O error occurs */ @Override public void close() throws IOException { IOUtil.close(in, this::handleIOException); } /** * Invokes the delegate's mark(int) method. * * @param readlimit read ahead limit */ @Override public synchronized void mark(final int readlimit) { in.mark(readlimit); } /** * Invokes the delegate's reset() method. * * @throws IOException if an I/O error occurs */ @Override public synchronized void reset() throws IOException { try { in.reset(); } catch (final IOException e) { handleIOException(e); } } /** * Invokes the delegate's markSupported() method. * * @return true if mark is supported, otherwise false */ @Override public boolean markSupported() { return in.markSupported(); } /** * Invoked by the read methods before the call is proxied. The number * of bytes that the caller wanted to read (1 for the {@link #read()} * method, buffer length for {@link #read(byte[])}, etc.) is given as * an argument. *

* Subclasses can override this method to add common pre-processing * functionality without having to override all the read methods. * The default implementation does nothing. *

* Note this method is not called from {@link #skip(long)} or * {@link #reset()}. You need to explicitly override those methods if * you want to add pre-processing steps also to them. * * @param n number of bytes that the caller asked to be read * @since 2.0 */ @SuppressWarnings("unused") protected void beforeRead(final int n) { // no-op } /** * Invoked by the read methods after the proxied call has returned * successfully. The number of bytes returned to the caller (or -1 if * the end of stream was reached) is given as an argument. *

* Subclasses can override this method to add common post-processing * functionality without having to override all the read methods. * The default implementation does nothing. *

* Note this method is not called from {@link #skip(long)} or * {@link #reset()}. You need to explicitly override those methods if * you want to add post-processing steps also to them. * * @param n number of bytes read, or -1 if the end of stream was reached * @since 2.0 */ @SuppressWarnings("unused") protected void afterRead(final int n) { // no-op } /** * Handle any IOExceptions thrown. *

* This method provides a point to implement custom exception * handling. The default behavior is to re-throw the exception. * * @param e The IOException thrown * @throws IOException if an I/O error occurs * @since 2.0 */ protected void handleIOException(final IOException e) throws IOException { throw e; } } ================================================ FILE: src/main/java/me/ag2s/epublib/util/commons/io/XmlStreamReader.java ================================================ package me.ag2s.epublib.util.commons.io; /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.text.MessageFormat; import java.util.Locale; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import me.ag2s.epublib.util.IOUtil; /** * Character stream that handles all the necessary Voodoo to figure out the * charset encoding of the XML document within the stream. *

* IMPORTANT: This class is not related in any way to the org.xml.sax.XMLReader. * This one IS a character stream. *

*

* All this has to be done without consuming characters from the stream, if not * the XML parser will not recognized the document as a valid XML. This is not * 100% true, but it's close enough (UTF-8 BOM is not handled by all parsers * right now, XmlStreamReader handles it and things work in all parsers). *

*

* The XmlStreamReader class handles the charset encoding of XML documents in * Files, raw streams and HTTP streams by offering a wide set of constructors. *

*

* By default the charset encoding detection is lenient, the constructor with * the lenient flag can be used for a script (following HTTP MIME and XML * specifications). All this is nicely explained by Mark Pilgrim in his blog, * Determining the character encoding of a feed. *

*

* Originally developed for ROME under * Apache License 2.0. *

* * //@seerr XmlStreamWriter * @since 2.0 */ public class XmlStreamReader extends Reader { private static final int BUFFER_SIZE = IOUtil.DEFAULT_BUFFER_SIZE; private static final String UTF_8 = "UTF-8"; private static final String US_ASCII = "US-ASCII"; private static final String UTF_16BE = "UTF-16BE"; private static final String UTF_16LE = "UTF-16LE"; private static final String UTF_32BE = "UTF-32BE"; private static final String UTF_32LE = "UTF-32LE"; private static final String UTF_16 = "UTF-16"; private static final String UTF_32 = "UTF-32"; private static final String EBCDIC = "CP1047"; private static final ByteOrderMark[] BOMS = new ByteOrderMark[] { ByteOrderMark.UTF_8, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_32BE, ByteOrderMark.UTF_32LE }; // UTF_16LE and UTF_32LE have the same two starting BOM bytes. private static final ByteOrderMark[] XML_GUESS_BYTES = new ByteOrderMark[] { new ByteOrderMark(UTF_8, 0x3C, 0x3F, 0x78, 0x6D), new ByteOrderMark(UTF_16BE, 0x00, 0x3C, 0x00, 0x3F), new ByteOrderMark(UTF_16LE, 0x3C, 0x00, 0x3F, 0x00), new ByteOrderMark(UTF_32BE, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x6D), new ByteOrderMark(UTF_32LE, 0x3C, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x6D, 0x00, 0x00, 0x00), new ByteOrderMark(EBCDIC, 0x4C, 0x6F, 0xA7, 0x94) }; private final Reader reader; private final String encoding; private final String defaultEncoding; /** * Returns the default encoding to use if none is set in HTTP content-type, * XML prolog and the rules based on content-type are not adequate. *

* If it is NULL the content-type based rules are used. * * @return the default encoding to use. */ public String getDefaultEncoding() { return defaultEncoding; } /** * Creates a Reader for a File. *

* It looks for the UTF-8 BOM first, if none sniffs the XML prolog charset, * if this is also missing defaults to UTF-8. *

* It does a lenient charset encoding detection, check the constructor with * the lenient parameter for details. * * @param file File to create a Reader from. * @throws IOException thrown if there is a problem reading the file. */ @SuppressWarnings("unused") public XmlStreamReader(final File file) throws IOException { this(new FileInputStream(Objects.requireNonNull(file))); } /** * Creates a Reader for a raw InputStream. *

* It follows the same logic used for files. *

* It does a lenient charset encoding detection, check the constructor with * the lenient parameter for details. * * @param inputStream InputStream to create a Reader from. * @throws IOException thrown if there is a problem reading the stream. */ public XmlStreamReader(final InputStream inputStream) throws IOException { this(inputStream, true); } /** * Creates a Reader for a raw InputStream. *

* It follows the same logic used for files. *

* If lenient detection is indicated and the detection above fails as per * specifications it then attempts the following: *

* If the content type was 'text/html' it replaces it with 'text/xml' and * tries the detection again. *

* Else if the XML prolog had a charset encoding that encoding is used. *

* Else if the content type had a charset encoding that encoding is used. *

* Else 'UTF-8' is used. *

* If lenient detection is indicated an XmlStreamReaderException is never * thrown. * * @param inputStream InputStream to create a Reader from. * @param lenient indicates if the charset encoding detection should be * relaxed. * @throws IOException thrown if there is a problem reading the stream. * @throws XmlStreamReaderException thrown if the charset encoding could not * be determined according to the specs. */ public XmlStreamReader(final InputStream inputStream, final boolean lenient) throws IOException { this(inputStream, lenient, null); } /** * Creates a Reader for a raw InputStream. *

* It follows the same logic used for files. *

* If lenient detection is indicated and the detection above fails as per * specifications it then attempts the following: *

* If the content type was 'text/html' it replaces it with 'text/xml' and * tries the detection again. *

* Else if the XML prolog had a charset encoding that encoding is used. *

* Else if the content type had a charset encoding that encoding is used. *

* Else 'UTF-8' is used. *

* If lenient detection is indicated an XmlStreamReaderException is never * thrown. * * @param inputStream InputStream to create a Reader from. * @param lenient indicates if the charset encoding detection should be * relaxed. * @param defaultEncoding The default encoding * @throws IOException thrown if there is a problem reading the stream. * @throws XmlStreamReaderException thrown if the charset encoding could not * be determined according to the specs. */ public XmlStreamReader(final InputStream inputStream, final boolean lenient, final String defaultEncoding) throws IOException { Objects.requireNonNull(inputStream, "inputStream"); this.defaultEncoding = defaultEncoding; final BOMInputStream bom = new BOMInputStream(new BufferedInputStream(inputStream, BUFFER_SIZE), false, BOMS); final BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES); this.encoding = doRawStream(bom, pis, lenient); this.reader = new InputStreamReader(pis, encoding); } /** * Creates a Reader using the InputStream of a URL. *

* If the URL is not of type HTTP and there is not 'content-type' header in * the fetched data it uses the same logic used for Files. *

* If the URL is a HTTP Url or there is a 'content-type' header in the * fetched data it uses the same logic used for an InputStream with * content-type. *

* It does a lenient charset encoding detection, check the constructor with * the lenient parameter for details. * * @param url URL to create a Reader from. * @throws IOException thrown if there is a problem reading the stream of * the URL. */ @SuppressWarnings("unused") public XmlStreamReader(final URL url) throws IOException { this(Objects.requireNonNull(url, "url").openConnection(), null); } /** * Creates a Reader using the InputStream of a URLConnection. *

* If the URLConnection is not of type HttpURLConnection and there is not * 'content-type' header in the fetched data it uses the same logic used for * files. *

* If the URLConnection is a HTTP Url or there is a 'content-type' header in * the fetched data it uses the same logic used for an InputStream with * content-type. *

* It does a lenient charset encoding detection, check the constructor with * the lenient parameter for details. * * @param conn URLConnection to create a Reader from. * @param defaultEncoding The default encoding * @throws IOException thrown if there is a problem reading the stream of * the URLConnection. */ public XmlStreamReader(final URLConnection conn, final String defaultEncoding) throws IOException { Objects.requireNonNull(conn, "conm"); this.defaultEncoding = defaultEncoding; final boolean lenient = true; final String contentType = conn.getContentType(); final InputStream inputStream = conn.getInputStream(); final BOMInputStream bom = new BOMInputStream(new BufferedInputStream(inputStream, BUFFER_SIZE), false, BOMS); final BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES); if (conn instanceof HttpURLConnection || contentType != null) { this.encoding = processHttpStream(bom, pis, contentType, lenient); } else { this.encoding = doRawStream(bom, pis, lenient); } this.reader = new InputStreamReader(pis, encoding); } /** * Creates a Reader using an InputStream and the associated content-type * header. *

* First it checks if the stream has BOM. If there is not BOM checks the * content-type encoding. If there is not content-type encoding checks the * XML prolog encoding. If there is not XML prolog encoding uses the default * encoding mandated by the content-type MIME type. *

* It does a lenient charset encoding detection, check the constructor with * the lenient parameter for details. * * @param inputStream InputStream to create the reader from. * @param httpContentType content-type header to use for the resolution of * the charset encoding. * @throws IOException thrown if there is a problem reading the file. */ public XmlStreamReader(final InputStream inputStream, final String httpContentType) throws IOException { this(inputStream, httpContentType, true); } /** * Creates a Reader using an InputStream and the associated content-type * header. This constructor is lenient regarding the encoding detection. *

* First it checks if the stream has BOM. If there is not BOM checks the * content-type encoding. If there is not content-type encoding checks the * XML prolog encoding. If there is not XML prolog encoding uses the default * encoding mandated by the content-type MIME type. *

* If lenient detection is indicated and the detection above fails as per * specifications it then attempts the following: *

* If the content type was 'text/html' it replaces it with 'text/xml' and * tries the detection again. *

* Else if the XML prolog had a charset encoding that encoding is used. *

* Else if the content type had a charset encoding that encoding is used. *

* Else 'UTF-8' is used. *

* If lenient detection is indicated an XmlStreamReaderException is never * thrown. * * @param inputStream InputStream to create the reader from. * @param httpContentType content-type header to use for the resolution of * the charset encoding. * @param lenient indicates if the charset encoding detection should be * relaxed. * @param defaultEncoding The default encoding * @throws IOException thrown if there is a problem reading the file. * @throws XmlStreamReaderException thrown if the charset encoding could not * be determined according to the specs. */ public XmlStreamReader(final InputStream inputStream, final String httpContentType, final boolean lenient, final String defaultEncoding) throws IOException { Objects.requireNonNull(inputStream, "inputStream"); this.defaultEncoding = defaultEncoding; final BOMInputStream bom = new BOMInputStream(new BufferedInputStream(inputStream, BUFFER_SIZE), false, BOMS); final BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES); this.encoding = processHttpStream(bom, pis, httpContentType, lenient); this.reader = new InputStreamReader(pis, encoding); } /** * Creates a Reader using an InputStream and the associated content-type * header. This constructor is lenient regarding the encoding detection. *

* First it checks if the stream has BOM. If there is not BOM checks the * content-type encoding. If there is not content-type encoding checks the * XML prolog encoding. If there is not XML prolog encoding uses the default * encoding mandated by the content-type MIME type. *

* If lenient detection is indicated and the detection above fails as per * specifications it then attempts the following: *

* If the content type was 'text/html' it replaces it with 'text/xml' and * tries the detection again. *

* Else if the XML prolog had a charset encoding that encoding is used. *

* Else if the content type had a charset encoding that encoding is used. *

* Else 'UTF-8' is used. *

* If lenient detection is indicated an XmlStreamReaderException is never * thrown. * * @param inputStream InputStream to create the reader from. * @param httpContentType content-type header to use for the resolution of * the charset encoding. * @param lenient indicates if the charset encoding detection should be * relaxed. * @throws IOException thrown if there is a problem reading the file. * @throws XmlStreamReaderException thrown if the charset encoding could not * be determined according to the specs. */ public XmlStreamReader(final InputStream inputStream, final String httpContentType, final boolean lenient) throws IOException { this(inputStream, httpContentType, lenient, null); } /** * Returns the charset encoding of the XmlStreamReader. * * @return charset encoding. */ public String getEncoding() { return encoding; } /** * Invokes the underlying reader's read(char[], int, int) method. * @param buf the buffer to read the characters into * @param offset The start offset * @param len The number of bytes to read * @return the number of characters read or -1 if the end of stream * @throws IOException if an I/O error occurs */ @Override public int read(final char[] buf, final int offset, final int len) throws IOException { return reader.read(buf, offset, len); } /** * Closes the XmlStreamReader stream. * * @throws IOException thrown if there was a problem closing the stream. */ @Override public void close() throws IOException { reader.close(); } /** * Process the raw stream. * * @param bom BOMInputStream to detect byte order marks * @param pis BOMInputStream to guess XML encoding * @param lenient indicates if the charset encoding detection should be * relaxed. * @return the encoding to be used * @throws IOException thrown if there is a problem reading the stream. */ private String doRawStream(final BOMInputStream bom, final BOMInputStream pis, final boolean lenient) throws IOException { final String bomEnc = bom.getBOMCharsetName(); final String xmlGuessEnc = pis.getBOMCharsetName(); final String xmlEnc = getXmlProlog(pis, xmlGuessEnc); try { return calculateRawEncoding(bomEnc, xmlGuessEnc, xmlEnc); } catch (final XmlStreamReaderException ex) { if (lenient) { return doLenientDetection(null, ex); } throw ex; } } /** * Process a HTTP stream. * * @param bom BOMInputStream to detect byte order marks * @param pis BOMInputStream to guess XML encoding * @param httpContentType The HTTP content type * @param lenient indicates if the charset encoding detection should be * relaxed. * @return the encoding to be used * @throws IOException thrown if there is a problem reading the stream. */ private String processHttpStream(final BOMInputStream bom, final BOMInputStream pis, final String httpContentType, final boolean lenient) throws IOException { final String bomEnc = bom.getBOMCharsetName(); final String xmlGuessEnc = pis.getBOMCharsetName(); final String xmlEnc = getXmlProlog(pis, xmlGuessEnc); try { return calculateHttpEncoding(httpContentType, bomEnc, xmlGuessEnc, xmlEnc, lenient); } catch (final XmlStreamReaderException ex) { if (lenient) { return doLenientDetection(httpContentType, ex); } throw ex; } } /** * Do lenient detection. * * @param httpContentType content-type header to use for the resolution of * the charset encoding. * @param ex The thrown exception * @return the encoding * @throws IOException thrown if there is a problem reading the stream. */ private String doLenientDetection(String httpContentType, XmlStreamReaderException ex) throws IOException { if (httpContentType != null && httpContentType.startsWith("text/html")) { httpContentType = httpContentType.substring("text/html".length()); httpContentType = "text/xml" + httpContentType; try { return calculateHttpEncoding(httpContentType, ex.getBomEncoding(), ex.getXmlGuessEncoding(), ex.getXmlEncoding(), true); } catch (final XmlStreamReaderException ex2) { ex = ex2; } } String encoding = ex.getXmlEncoding(); if (encoding == null) { encoding = ex.getContentTypeEncoding(); } if (encoding == null) { encoding = defaultEncoding == null ? UTF_8 : defaultEncoding; } return encoding; } /** * Calculate the raw encoding. * * @param bomEnc BOM encoding * @param xmlGuessEnc XML Guess encoding * @param xmlEnc XML encoding * @return the raw encoding * @throws IOException thrown if there is a problem reading the stream. */ String calculateRawEncoding(final String bomEnc, final String xmlGuessEnc, final String xmlEnc) throws IOException { // BOM is Null if (bomEnc == null) { if (xmlGuessEnc == null || xmlEnc == null) { return defaultEncoding == null ? UTF_8 : defaultEncoding; } if (xmlEnc.equals(UTF_16) && (xmlGuessEnc.equals(UTF_16BE) || xmlGuessEnc.equals(UTF_16LE))) { return xmlGuessEnc; } return xmlEnc; } // BOM is UTF-8 if (bomEnc.equals(UTF_8)) { if (xmlGuessEnc != null && !xmlGuessEnc.equals(UTF_8)) { final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); } if (xmlEnc != null && !xmlEnc.equals(UTF_8)) { final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); } return bomEnc; } // BOM is UTF-16BE or UTF-16LE if (bomEnc.equals(UTF_16BE) || bomEnc.equals(UTF_16LE)) { if (xmlGuessEnc != null && !xmlGuessEnc.equals(bomEnc)) { final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); } if (xmlEnc != null && !xmlEnc.equals(UTF_16) && !xmlEnc.equals(bomEnc)) { final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); } return bomEnc; } // BOM is UTF-32BE or UTF-32LE if (bomEnc.equals(UTF_32BE) || bomEnc.equals(UTF_32LE)) { if (xmlGuessEnc != null && !xmlGuessEnc.equals(bomEnc)) { final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); } if (xmlEnc != null && !xmlEnc.equals(UTF_32) && !xmlEnc.equals(bomEnc)) { final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); } return bomEnc; } // BOM is something else final String msg = MessageFormat.format(RAW_EX_2, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); } /** * Calculate the HTTP encoding. * * @param httpContentType The HTTP content type * @param bomEnc BOM encoding * @param xmlGuessEnc XML Guess encoding * @param xmlEnc XML encoding * @param lenient indicates if the charset encoding detection should be * relaxed. * @return the HTTP encoding * @throws IOException thrown if there is a problem reading the stream. */ String calculateHttpEncoding(final String httpContentType, final String bomEnc, final String xmlGuessEnc, final String xmlEnc, final boolean lenient) throws IOException { // Lenient and has XML encoding if (lenient && xmlEnc != null) { return xmlEnc; } // Determine mime/encoding content types from HTTP Content Type final String cTMime = getContentTypeMime(httpContentType); final String cTEnc = getContentTypeEncoding(httpContentType); final boolean appXml = isAppXml(cTMime); final boolean textXml = isTextXml(cTMime); // Mime type NOT "application/xml" or "text/xml" if (!appXml && !textXml) { final String msg = MessageFormat.format(HTTP_EX_3, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); } // No content type encoding if (cTEnc == null) { if (appXml) { return calculateRawEncoding(bomEnc, xmlGuessEnc, xmlEnc); } return defaultEncoding == null ? US_ASCII : defaultEncoding; } // UTF-16BE or UTF-16LE content type encoding if (cTEnc.equals(UTF_16BE) || cTEnc.equals(UTF_16LE)) { if (bomEnc != null) { final String msg = MessageFormat.format(HTTP_EX_1, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); } return cTEnc; } // UTF-16 content type encoding if (cTEnc.equals(UTF_16)) { if (bomEnc != null && bomEnc.startsWith(UTF_16)) { return bomEnc; } final String msg = MessageFormat.format(HTTP_EX_2, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); } // UTF-32BE or UTF-132E content type encoding if (cTEnc.equals(UTF_32BE) || cTEnc.equals(UTF_32LE)) { if (bomEnc != null) { final String msg = MessageFormat.format(HTTP_EX_1, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); } return cTEnc; } // UTF-32 content type encoding if (cTEnc.equals(UTF_32)) { if (bomEnc != null && bomEnc.startsWith(UTF_32)) { return bomEnc; } final String msg = MessageFormat.format(HTTP_EX_2, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); } return cTEnc; } /** * Returns MIME type or NULL if httpContentType is NULL. * * @param httpContentType the HTTP content type * @return The mime content type */ static String getContentTypeMime(final String httpContentType) { String mime = null; if (httpContentType != null) { final int i = httpContentType.indexOf(";"); if (i >= 0) { mime = httpContentType.substring(0, i); } else { mime = httpContentType; } mime = mime.trim(); } return mime; } private static final Pattern CHARSET_PATTERN = Pattern .compile("charset=[\"']?([.[^; \"']]*)[\"']?"); /** * Returns charset parameter value, NULL if not present, NULL if * httpContentType is NULL. * * @param httpContentType the HTTP content type * @return The content type encoding (upcased) */ static String getContentTypeEncoding(final String httpContentType) { String encoding = null; if (httpContentType != null) { final int i = httpContentType.indexOf(";"); if (i > -1) { final String postMime = httpContentType.substring(i + 1); final Matcher m = CHARSET_PATTERN.matcher(postMime); encoding = m.find() ? m.group(1) : null; encoding = encoding != null ? encoding.toUpperCase(Locale.ROOT) : null; } } return encoding; } /** * Pattern capturing the encoding of the "xml" processing instruction. */ public static final Pattern ENCODING_PATTERN = Pattern.compile( "<\\?xml.*encoding[\\s]*=[\\s]*((?:\".[^\"]*\")|(?:'.[^']*'))", Pattern.MULTILINE); /** * Returns the encoding declared in the , NULL if none. * * @param inputStream InputStream to create the reader from. * @param guessedEnc guessed encoding * @return the encoding declared in the * @throws IOException thrown if there is a problem reading the stream. */ private static String getXmlProlog(final InputStream inputStream, final String guessedEnc) throws IOException { String encoding = null; if (guessedEnc != null) { final byte[] bytes = new byte[BUFFER_SIZE]; inputStream.mark(BUFFER_SIZE); int offset = 0; int max = BUFFER_SIZE; int c = inputStream.read(bytes, offset, max); int firstGT = -1; String xmlProlog = ""; // avoid possible NPE warning (cannot happen; this just silences the warning) while (c != -1 && firstGT == -1 && offset < BUFFER_SIZE) { offset += c; max -= c; c = inputStream.read(bytes, offset, max); xmlProlog = new String(bytes, 0, offset, guessedEnc); firstGT = xmlProlog.indexOf('>'); } if (firstGT == -1) { if (c == -1) { throw new IOException("Unexpected end of XML stream"); } throw new IOException( "XML prolog or ROOT element not found on first " + offset + " bytes"); } final int bytesRead = offset; if (bytesRead > 0) { inputStream.reset(); final BufferedReader bReader = new BufferedReader(new StringReader( xmlProlog.substring(0, firstGT + 1))); final StringBuffer prolog = new StringBuffer(); String line; while ((line = bReader.readLine()) != null) { prolog.append(line); } final Matcher m = ENCODING_PATTERN.matcher(prolog); if (m.find()) { encoding = Objects.requireNonNull(m.group(1)).toUpperCase(Locale.ROOT); encoding = encoding.substring(1, encoding.length() - 1); } } } return encoding; } /** * Indicates if the MIME type belongs to the APPLICATION XML family. * * @param mime The mime type * @return true if the mime type belongs to the APPLICATION XML family, * otherwise false */ static boolean isAppXml(final String mime) { return mime != null && (mime.equals("application/xml") || mime.equals("application/xml-dtd") || mime.equals("application/xml-external-parsed-entity") || mime.startsWith("application/") && mime.endsWith("+xml")); } /** * Indicates if the MIME type belongs to the TEXT XML family. * * @param mime The mime type * @return true if the mime type belongs to the TEXT XML family, * otherwise false */ static boolean isTextXml(final String mime) { return mime != null && (mime.equals("text/xml") || mime.equals("text/xml-external-parsed-entity") || mime.startsWith("text/") && mime.endsWith("+xml")); } private static final String RAW_EX_1 = "Invalid encoding, BOM [{0}] XML guess [{1}] XML prolog [{2}] encoding mismatch"; private static final String RAW_EX_2 = "Invalid encoding, BOM [{0}] XML guess [{1}] XML prolog [{2}] unknown BOM"; private static final String HTTP_EX_1 = "Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], BOM must be NULL"; private static final String HTTP_EX_2 = "Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], encoding mismatch"; private static final String HTTP_EX_3 = "Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], Invalid MIME"; } ================================================ FILE: src/main/java/me/ag2s/epublib/util/commons/io/XmlStreamReaderException.java ================================================ package me.ag2s.epublib.util.commons.io; /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.io.IOException; /** * The XmlStreamReaderException is thrown by the XmlStreamReader constructors if * the charset encoding can not be determined according to the XML 1.0 * specification and RFC 3023. *

* The exception returns the unconsumed InputStream to allow the application to * do an alternate processing with the stream. Note that the original * InputStream given to the XmlStreamReader cannot be used as that one has been * already read. *

* * @since 2.0 */ public class XmlStreamReaderException extends IOException { private static final long serialVersionUID = 1L; private final String bomEncoding; private final String xmlGuessEncoding; private final String xmlEncoding; private final String contentTypeMime; private final String contentTypeEncoding; /** * Creates an exception instance if the charset encoding could not be * determined. *

* Instances of this exception are thrown by the XmlStreamReader. *

* * @param msg message describing the reason for the exception. * @param bomEnc BOM encoding. * @param xmlGuessEnc XML guess encoding. * @param xmlEnc XML prolog encoding. */ public XmlStreamReaderException(final String msg, final String bomEnc, final String xmlGuessEnc, final String xmlEnc) { this(msg, null, null, bomEnc, xmlGuessEnc, xmlEnc); } /** * Creates an exception instance if the charset encoding could not be * determined. *

* Instances of this exception are thrown by the XmlStreamReader. *

* * @param msg message describing the reason for the exception. * @param ctMime MIME type in the content-type. * @param ctEnc encoding in the content-type. * @param bomEnc BOM encoding. * @param xmlGuessEnc XML guess encoding. * @param xmlEnc XML prolog encoding. */ public XmlStreamReaderException(final String msg, final String ctMime, final String ctEnc, final String bomEnc, final String xmlGuessEnc, final String xmlEnc) { super(msg); contentTypeMime = ctMime; contentTypeEncoding = ctEnc; bomEncoding = bomEnc; xmlGuessEncoding = xmlGuessEnc; xmlEncoding = xmlEnc; } /** * Returns the BOM encoding found in the InputStream. * * @return the BOM encoding, null if none. */ public String getBomEncoding() { return bomEncoding; } /** * Returns the encoding guess based on the first bytes of the InputStream. * * @return the encoding guess, null if it couldn't be guessed. */ public String getXmlGuessEncoding() { return xmlGuessEncoding; } /** * Returns the encoding found in the XML prolog of the InputStream. * * @return the encoding of the XML prolog, null if none. */ public String getXmlEncoding() { return xmlEncoding; } /** * Returns the MIME type in the content-type used to attempt determining the * encoding. * * @return the MIME type in the content-type, null if there was not * content-type or the encoding detection did not involve HTTP. */ public String getContentTypeMime() { return contentTypeMime; } /** * Returns the encoding in the content-type used to attempt determining the * encoding. * * @return the encoding in the content-type, null if there was not * content-type, no encoding in it or the encoding detection did not * involve HTTP. */ public String getContentTypeEncoding() { return contentTypeEncoding; } } ================================================ FILE: src/main/java/me/ag2s/umdlib/domain/UmdBook.java ================================================ package me.ag2s.umdlib.domain; import java.io.IOException; import java.io.OutputStream; import me.ag2s.umdlib.tool.WrapOutputStream; public class UmdBook { public int getNum() { return num; } public void setNum(int num) { this.num = num; } private int num; /** Header Part of UMD book */ private UmdHeader header = new UmdHeader(); /** * Detail chapters Part of UMD book * (include Titles & Contents of each chapter) */ private UmdChapters chapters = new UmdChapters(); /** Cover Part of UMD book (for example, and JPEG file) */ private UmdCover cover = new UmdCover(); /** End Part of UMD book */ private UmdEnd end = new UmdEnd(); /** * Build the UMD file. * @param os * @throws IOException */ public void buildUmd(OutputStream os) throws IOException { WrapOutputStream wos = new WrapOutputStream(os); header.buildHeader(wos); chapters.buildChapters(wos); cover.buildCover(wos); end.buildEnd(wos); } public UmdHeader getHeader() { return header; } public void setHeader(UmdHeader header) { this.header = header; } public UmdChapters getChapters() { return chapters; } public void setChapters(UmdChapters chapters) { this.chapters = chapters; } public UmdCover getCover() { return cover; } public void setCover(UmdCover cover) { this.cover = cover; } public UmdEnd getEnd() { return end; } public void setEnd(UmdEnd end) { this.end = end; } } ================================================ FILE: src/main/java/me/ag2s/umdlib/domain/UmdChapters.java ================================================ package me.ag2s.umdlib.domain; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.zip.DeflaterOutputStream; import me.ag2s.umdlib.tool.UmdUtils; import me.ag2s.umdlib.tool.WrapOutputStream; /** * It includes all titles and contents of each chapter in the UMD file. * And the content has been compressed by zlib. * * @author Ray Liang (liangguanhui@qq.com) * 2009-12-20 */ public class UmdChapters { private static final int DEFAULT_CHUNK_INIT_SIZE = 32768; private int TotalContentLen; public List getTitles() { return titles; } private List titles = new ArrayList<>(); public List contentLengths = new ArrayList<>(); public ByteArrayOutputStream contents = new ByteArrayOutputStream(); public void addTitle(String s){ titles.add(UmdUtils.stringToUnicodeBytes(s)); } public void addTitle(byte[] s){ titles.add(s); } public void addContentLength(Integer integer){ contentLengths.add(integer); } public int getContentLength(int index){ return contentLengths.get(index); } public byte[] getContent(int index) { int st=contentLengths.get(index); byte[] b=contents.toByteArray(); int end=index+1 chunkRbList = new ArrayList(); while(startPos < allContents.length) { left = allContents.length - startPos; len = DEFAULT_CHUNK_INIT_SIZE < left ? DEFAULT_CHUNK_INIT_SIZE : left; bos.reset(); DeflaterOutputStream zos = new DeflaterOutputStream(bos); zos.write(allContents, startPos, len); zos.close(); byte[] chunk = bos.toByteArray(); byte[] rb = UmdUtils.genRandomBytes(4); wos.writeByte('$'); wos.writeBytes(rb); // 4 random chunkRbList.add(rb); wos.writeInt(chunk.length + 9); wos.write(chunk); // end of each chunk wos.writeBytes('#', 0xF1, 0, 0, 0x15); wos.write(zero16); startPos += len; chunkCnt++; } // end of all chunks wos.writeBytes('#', 0x81, 0, 0x01, 0x09); wos.writeBytes(0, 0, 0, 0); //random numbers wos.write('$'); wos.writeBytes(0, 0, 0, 0); //random numbers wos.writeInt(chunkCnt * 4 + 9); for (int i = chunkCnt - 1; i >= 0; i--) { // random. They are as the same as random numbers in the begin of each chunk // use desc order to output these random wos.writeBytes(chunkRbList.get(i)); } } public void addChapter(String title, String content) { titles.add(UmdUtils.stringToUnicodeBytes(title)); byte[] b = UmdUtils.stringToUnicodeBytes(content); contentLengths.add(b.length); try { contents.write(b); } catch (IOException e) { throw new RuntimeException(e); } } public void addFile(File f, String title) throws IOException { byte[] temp = UmdUtils.readFile(f); String s = new String(temp); addChapter(title, s); } public void addFile(File f) throws IOException { String s = f.getName(); int idx = s.lastIndexOf('.'); if (idx >= 0) { s = s.substring(0, idx); } addFile(f, s); } public void clearChapters() { titles.clear(); contentLengths.clear(); contents.reset(); } public int getTotalContentLen() { return TotalContentLen; } public void setTotalContentLen(int totalContentLen) { TotalContentLen = totalContentLen; } } ================================================ FILE: src/main/java/me/ag2s/umdlib/domain/UmdCover.java ================================================ package me.ag2s.umdlib.domain; import java.io.File; import java.io.IOException; import me.ag2s.umdlib.tool.UmdUtils; import me.ag2s.umdlib.tool.WrapOutputStream; /** * This is the cover part of the UMD file. *

* NOTICE: if the "coverData" is empty, it will be skipped when building UMD file. *

* There are 3 ways to load the image data: *
    *
  1. new constructor function of UmdCover.
  2. *
  3. use UmdCover.load function.
  4. *
  5. use UmdCover.initDefaultCover, it will generate a simple image with text.
  6. *
* @author Ray Liang (liangguanhui@qq.com) * 2009-12-20 */ public class UmdCover { private static int DEFAULT_COVER_WIDTH = 120; private static int DEFAULT_COVER_HEIGHT = 160; private byte[] coverData; public UmdCover() { } public UmdCover(byte[] coverData) { this.coverData = coverData; } public void load(File f) throws IOException { this.coverData = UmdUtils.readFile(f); } public void load(String fileName) throws IOException { load(new File(fileName)); } public void initDefaultCover(String title) throws IOException { // BufferedImage img = new BufferedImage(DEFAULT_COVER_WIDTH, DEFAULT_COVER_HEIGHT, BufferedImage.TYPE_INT_RGB); // Graphics g = img.getGraphics(); // g.setColor(Color.BLACK); // g.fillRect(0, 0, img.getWidth(), img.getHeight()); // g.setColor(Color.WHITE); // g.setFont(new Font("����", Font.PLAIN, 12)); // // FontMetrics fm = g.getFontMetrics(); // int ascent = fm.getAscent(); // int descent = fm.getDescent(); // int strWidth = fm.stringWidth(title); // int x = (img.getWidth() - strWidth) / 2; // int y = (img.getHeight() - ascent - descent) / 2; // g.drawString(title, x, y); // g.dispose(); // // ByteArrayOutputStream baos = new ByteArrayOutputStream(); // // JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(baos); // JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(img); // param.setQuality(0.5f, false); // encoder.setJPEGEncodeParam(param); // encoder.encode(img); // // coverData = baos.toByteArray(); } public void buildCover(WrapOutputStream wos) throws IOException { if (coverData == null || coverData.length == 0) { return; } wos.writeBytes('#', 0x82, 0, 0x01, 0x0A, 0x01); byte[] rb = UmdUtils.genRandomBytes(4); wos.writeBytes(rb); //random numbers wos.write('$'); wos.writeBytes(rb); //random numbers wos.writeInt(coverData.length + 9); wos.write(coverData); } public byte[] getCoverData() { return coverData; } public void setCoverData(byte[] coverData) { this.coverData = coverData; } } ================================================ FILE: src/main/java/me/ag2s/umdlib/domain/UmdEnd.java ================================================ package me.ag2s.umdlib.domain; import java.io.IOException; import me.ag2s.umdlib.tool.WrapOutputStream; /** * End part of UMD book, nothing to be special * * @author Ray Liang (liangguanhui@qq.com) * 2009-12-20 */ public class UmdEnd { public void buildEnd(WrapOutputStream wos) throws IOException { wos.writeBytes('#', 0x0C, 0, 0x01, 0x09); wos.writeInt(wos.getWritten() + 4); } } ================================================ FILE: src/main/java/me/ag2s/umdlib/domain/UmdHeader.java ================================================ package me.ag2s.umdlib.domain; import java.io.IOException; import me.ag2s.umdlib.tool.UmdUtils; import me.ag2s.umdlib.tool.WrapOutputStream; /** * Header of UMD file. * It includes a lot of properties of header. * All the properties are String type. * * @author Ray Liang (liangguanhui@qq.com) * 2009-12-20 */ public class UmdHeader { public byte getUmdType() { return umdType; } public void setUmdType(byte umdType) { this.umdType = umdType; } private byte umdType; private String title; private String author; private String year; private String month; private String day; private String bookType; private String bookMan; private String shopKeeper; private final static byte B_type_umd = (byte) 0x01; private final static byte B_type_title = (byte) 0x02; private final static byte B_type_author = (byte) 0x03; private final static byte B_type_year = (byte) 0x04; private final static byte B_type_month = (byte) 0x05; private final static byte B_type_day = (byte) 0x06; private final static byte B_type_bookType = (byte) 0x07; private final static byte B_type_bookMan = (byte) 0x08; private final static byte B_type_shopKeeper = (byte) 0x09; public void buildHeader(WrapOutputStream wos) throws IOException { wos.writeBytes(0x89, 0x9b, 0x9a, 0xde); // UMD file type flags wos.writeByte('#'); wos.writeBytes(0x01, 0x00, 0x00, 0x08); // Unknown wos.writeByte(0x01); //0x01 is text type; while 0x02 is Image type. wos.writeBytes(UmdUtils.genRandomBytes(2)); //random number // start properties output buildType(wos, B_type_title, getTitle()); buildType(wos, B_type_author, getAuthor()); buildType(wos, B_type_year, getYear()); buildType(wos, B_type_month, getMonth()); buildType(wos, B_type_day, getDay()); buildType(wos, B_type_bookType, getBookType()); buildType(wos, B_type_bookMan, getBookMan()); buildType(wos, B_type_shopKeeper, getShopKeeper()); } public void buildType(WrapOutputStream wos, byte type, String content) throws IOException { if (content == null || content.length() == 0) { return; } wos.writeBytes('#', type, 0, 0); byte[] temp = UmdUtils.stringToUnicodeBytes(content); wos.writeByte(temp.length + 5); wos.write(temp); } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public String getBookMan() { return bookMan; } public void setBookMan(String bookMan) { this.bookMan = bookMan; } public String getShopKeeper() { return shopKeeper; } public void setShopKeeper(String shopKeeper) { this.shopKeeper = shopKeeper; } public String getYear() { return year; } public void setYear(String year) { this.year = year; } public String getMonth() { return month; } public void setMonth(String month) { this.month = month; } public String getDay() { return day; } public void setDay(String day) { this.day = day; } public String getBookType() { return bookType; } public void setBookType(String bookType) { this.bookType = bookType; } @Override public String toString() { return "UmdHeader{" + "umdType=" + umdType + ", title='" + title + '\'' + ", author='" + author + '\'' + ", year='" + year + '\'' + ", month='" + month + '\'' + ", day='" + day + '\'' + ", bookType='" + bookType + '\'' + ", bookMan='" + bookMan + '\'' + ", shopKeeper='" + shopKeeper + '\'' + '}'; } } ================================================ FILE: src/main/java/me/ag2s/umdlib/tool/StreamReader.java ================================================ package me.ag2s.umdlib.tool; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class StreamReader { private InputStream is; public long getOffset() { return offset; } public void setOffset(long offset) { this.offset = offset; } public long getSize() { return size; } public void setSize(long size) { this.size = size; } private long offset; private long size; private void incCount(int value) { int temp = (int) (offset + value); if (temp < 0) { temp = Integer.MAX_VALUE; } offset = temp; } public StreamReader(InputStream inputStream) throws IOException { this.is=inputStream; //this.size=inputStream.getChannel().size(); } public short readUint8() throws IOException { byte[] b=new byte[1]; is.read(b); incCount(1); return (short) ((b[0] & 0xFF)); } public byte readByte() throws IOException { byte[] b=new byte[1]; is.read(b); incCount(1); return b[0]; } public byte[] readBytes(int len) throws IOException { if (len<1){ System.out.println(len); throw new IllegalArgumentException("Length must > 0: " + len); } byte[] b=new byte[len]; is.read(b); incCount(len); return b; } public String readHex(int len) throws IOException { if (len<1){ System.out.println(len); throw new IllegalArgumentException("Length must > 0: " + len); } byte[] b=new byte[len]; is.read(b); incCount(len); return UmdUtils.toHex(b); } public short readShort() throws IOException { byte[] b=new byte[2]; is.read(b); incCount(2); short x = (short) (((b[0] & 0xFF) << 8) | ((b[1] & 0xFF) << 0)); return x; } public short readShortLe() throws IOException { byte[] b=new byte[2]; is.read(b); incCount(2); short x = (short) (((b[1] & 0xFF) << 8) | ((b[0] & 0xFF) << 0)); return x; } public int readInt() throws IOException { byte[] b=new byte[4]; is.read(b); incCount(4); int x = ((b[0] & 0xFF) << 24) | ((b[1] & 0xFF) << 16) | ((b[2] & 0xFF) << 8) | ((b[3] & 0xFF) << 0); return x; } public int readIntLe() throws IOException { byte[] b=new byte[4]; is.read(b); incCount(4); int x = ((b[3] & 0xFF) << 24) | ((b[2] & 0xFF) << 16) | ((b[1] & 0xFF) << 8) | ((b[0] & 0xFF) << 0); return x; } public void skip(int len) throws IOException { readBytes(len); } public byte[] read(byte[] b) throws IOException { is.read(b); incCount(b.length); return b; } public byte[] read(byte[] b, int off, int len) throws IOException { is.read(b, off, len); incCount(len); return b; } } ================================================ FILE: src/main/java/me/ag2s/umdlib/tool/UmdUtils.java ================================================ package me.ag2s.umdlib.tool; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.Random; import java.util.zip.InflaterInputStream; public class UmdUtils { private static final int EOF = -1; private static final int BUFFER_SIZE = 8 * 1024; /** * 将字符串编码成Unicode形式的byte[] * @param s 要编码的字符串 * @return 编码好的byte[] */ public static byte[] stringToUnicodeBytes(String s) { if (s == null) { throw new NullPointerException(); } int len = s.length(); byte[] ret = new byte[len * 2]; int a, b, c; for (int i = 0; i < len; i++) { c = s.charAt(i); a = c >> 8; b = c & 0xFF; if (a < 0) { a += 0xFF; } if (b < 0) { b += 0xFF; } ret[i * 2] = (byte) b; ret[i * 2 + 1] = (byte) a; } return ret; } /** * 将编码成Unicode形式的byte[]解码成原始字符串 * @param bytes 编码成Unicode形式的byte[] * @return 原始字符串 */ public static String unicodeBytesToString(byte[] bytes){ char[] s=new char[bytes.length/2]; StringBuilder sb=new StringBuilder(); int a,b,c; for(int i=0;i= 0) { baos.write(ch); } baos.flush(); return baos.toByteArray(); } finally { fis.close(); } } private static Random random = new Random(); public static byte[] genRandomBytes(int len) { if (len <= 0) { throw new IllegalArgumentException("Length must > 0: " + len); } byte[] ret = new byte[len]; for (int i = 0; i < ret.length; i++) { ret[i] = (byte) random.nextInt(256); } return ret; } } ================================================ FILE: src/main/java/me/ag2s/umdlib/tool/WrapOutputStream.java ================================================ package me.ag2s.umdlib.tool; import java.io.IOException; import java.io.OutputStream; public class WrapOutputStream extends OutputStream { private OutputStream os; private int written; public WrapOutputStream(OutputStream os) { this.os = os; } private void incCount(int value) { int temp = written + value; if (temp < 0) { temp = Integer.MAX_VALUE; } written = temp; } // it is different from the writeInt of DataOutputStream public void writeInt(int v) throws IOException { os.write((v >>> 0) & 0xFF); os.write((v >>> 8) & 0xFF); os.write((v >>> 16) & 0xFF); os.write((v >>> 24) & 0xFF); incCount(4); } public void writeByte(byte b) throws IOException { write(b); } public void writeByte(int n) throws IOException { write(n); } public void writeBytes(byte ... bytes) throws IOException { write(bytes); } public void writeBytes(int ... vals) throws IOException { for (int v : vals) { write(v); } } public void write(byte[] b, int off, int len) throws IOException { os.write(b, off, len); incCount(len); } public void write(byte[] b) throws IOException { os.write(b); incCount(b.length); } public void write(int b) throws IOException { os.write(b); incCount(1); } ///////////////////////////////////////////////// public void close() throws IOException { os.close(); } public void flush() throws IOException { os.flush(); } public boolean equals(Object obj) { return os.equals(obj); } public int hashCode() { return os.hashCode(); } public String toString() { return os.toString(); } public int getWritten() { return written; } } ================================================ FILE: src/main/java/me/ag2s/umdlib/umd/UmdReader.java ================================================ package me.ag2s.umdlib.umd; import java.io.IOException; import java.io.InputStream; import me.ag2s.umdlib.domain.UmdBook; import me.ag2s.umdlib.domain.UmdCover; import me.ag2s.umdlib.domain.UmdHeader; import me.ag2s.umdlib.tool.StreamReader; import me.ag2s.umdlib.tool.UmdUtils; /** * UMD格式的电子书解析 * 格式规范参考: * http://blog.sina.com.cn/s/blog_7c8dc2d501018o5d.html * http://blog.sina.com.cn/s/blog_7c8dc2d501018o5l.html * */ public class UmdReader { UmdBook book; InputStream inputStream; int _AdditionalCheckNumber; int _TotalContentLen; boolean end = false; public synchronized UmdBook read(InputStream inputStream) throws Exception { book = new UmdBook(); this.inputStream=inputStream; StreamReader reader = new StreamReader(inputStream); UmdHeader umdHeader = new UmdHeader(); book.setHeader(umdHeader); if (reader.readIntLe() != 0xde9a9b89) { throw new IOException("Wrong header"); } short num1 = -1; byte ch = reader.readByte(); while (ch == 35) { //int num2=reader.readByte(); short segType = reader.readShortLe(); byte segFlag = reader.readByte(); short len = (short) (reader.readUint8() - 5); System.out.println("块标识:" + segType); //short length1 = reader.readByte(); ReadSection(segType, segFlag, len, reader, umdHeader); if ((int) segType == 241 || (int) segType == 10) { segType = num1; } for (ch = reader.readByte(); ch == 36; ch = reader.readByte()) { //int num3 = reader.readByte(); System.out.println(ch); int additionalCheckNumber = reader.readIntLe(); int length2 = (reader.readIntLe() - 9); ReadAdditionalSection(segType, additionalCheckNumber, length2, reader); } num1 = segType; } System.out.println(book.getHeader().toString()); return book; } private void ReadAdditionalSection(short segType, int additionalCheckNumber, int length, StreamReader reader) throws Exception { switch (segType) { case 14: //this._TotalImageList.Add((object) Image.FromStream((Stream) new MemoryStream(reader.ReadBytes((int) length)))); break; case 15: //this._TotalImageList.Add((object) Image.FromStream((Stream) new MemoryStream(reader.ReadBytes((int) length)))); break; case 129: reader.readBytes(length); break; case 130: //byte[] covers = reader.readBytes(length); book.setCover(new UmdCover(reader.readBytes(length))); //this._Book.Cover = BitmapImage.FromStream((Stream) new MemoryStream(reader.ReadBytes((int) length))); break; case 131: System.out.println(length / 4); book.setNum(length / 4); for (int i = 0; i < length / 4; ++i) { book.getChapters().addContentLength(reader.readIntLe()); } break; case 132: //System.out.println(length/4); System.out.println(_AdditionalCheckNumber); System.out.println(additionalCheckNumber); if (this._AdditionalCheckNumber != additionalCheckNumber) { System.out.println(length); book.getChapters().contents.write(UmdUtils.decompress(reader.readBytes(length))); book.getChapters().contents.flush(); break; } else { for (int i = 0; i < book.getNum(); i++) { short len = reader.readUint8(); byte[] title = reader.readBytes(len); //System.out.println(UmdUtils.unicodeBytesToString(title)); book.getChapters().addTitle(title); } } break; default: /*Console.WriteLine("未知内容"); Console.WriteLine("Seg Type = " + (object) segType); Console.WriteLine("Seg Len = " + (object) length); Console.WriteLine("content = " + (object) reader.ReadBytes((int) length));*/ break; } } public void ReadSection(short segType, byte segFlag, short length, StreamReader reader, UmdHeader header) throws IOException { switch (segType) { case 1://umd文件头 DCTS_CMD_ID_VERSION header.setUmdType(reader.readByte()); reader.readBytes(2);//Random 2 System.out.println("UMD文件类型:" + header.getUmdType()); break; case 2://文件标题 DCTS_CMD_ID_TITLE header.setTitle(UmdUtils.unicodeBytesToString(reader.readBytes(length))); System.out.println("文件标题:" + header.getTitle()); break; case 3://作者 header.setAuthor(UmdUtils.unicodeBytesToString(reader.readBytes(length))); System.out.println("作者:" + header.getAuthor()); break; case 4://年 header.setYear(UmdUtils.unicodeBytesToString(reader.readBytes(length))); System.out.println("年:" + header.getYear()); break; case 5://月 header.setMonth(UmdUtils.unicodeBytesToString(reader.readBytes(length))); System.out.println("月:" + header.getMonth()); break; case 6://日 header.setDay(UmdUtils.unicodeBytesToString(reader.readBytes(length))); System.out.println("日:" + header.getDay()); break; case 7://小说类型 header.setBookType(UmdUtils.unicodeBytesToString(reader.readBytes(length))); System.out.println("小说类型:" + header.getBookType()); break; case 8://出版商 header.setBookMan(UmdUtils.unicodeBytesToString(reader.readBytes(length))); System.out.println("出版商:" + header.getBookMan()); break; case 9:// 零售商 header.setShopKeeper(UmdUtils.unicodeBytesToString(reader.readBytes(length))); System.out.println("零售商:" + header.getShopKeeper()); break; case 10://CONTENT ID System.out.println("CONTENT ID:" + reader.readHex(length)); break; case 11: //内容长度 DCTS_CMD_ID_FILE_LENGTH _TotalContentLen = reader.readIntLe(); book.getChapters().setTotalContentLen(_TotalContentLen); System.out.println("内容长度:" + _TotalContentLen); break; case 12://UMD文件结束 end = true; int num2 = reader.readIntLe(); System.out.println("整个文件长度" + num2); break; case 13: break; case 14: int num3 = (int) reader.readByte(); break; case 15: reader.readBytes(length); break; case 129://正文 case 131://章节偏移 _AdditionalCheckNumber = reader.readIntLe(); System.out.println("章节偏移:" + _AdditionalCheckNumber); break; case 132://章节标题,正文 _AdditionalCheckNumber = reader.readIntLe(); System.out.println("章节标题,正文:" + _AdditionalCheckNumber); break; case 130://封面(jpg) int num4 = (int) reader.readByte(); _AdditionalCheckNumber = reader.readIntLe(); break; case 135://页面偏移(Page Offset) reader.readUint8();//fontSize 一字节 字体大小 reader.readUint8();//screenWidth 屏幕宽度 reader.readBytes(4);//BlockRandom 指向一个页面偏移数据块 break; case 240://CDS KEY break; case 241://许可证(LICENCE KEY) //System.out.println("整个文件长度" + length); System.out.println("许可证(LICENCE KEY):" + reader.readHex(16)); break; default: if (length > 0) { byte[] numArray = reader.readBytes(length); } } } @Override public String toString() { return "UmdReader{" + "book=" + book + '}'; } } ================================================ FILE: src/main/java/org/kxml2/io/KXmlParser.java ================================================ /* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ // Contributors: Paul Hackenberger (unterminated entity handling in relaxed mode) package org.kxml2.io; import java.io.*; import java.util.*; import org.xmlpull.v1.*; /** A simple, pull based XML parser. This classe replaces the kXML 1 XmlParser class and the corresponding event classes. */ public class KXmlParser implements XmlPullParser { private Object location; static final private String UNEXPECTED_EOF = "Unexpected EOF"; static final private String ILLEGAL_TYPE = "Wrong event type"; static final private int LEGACY = 999; static final private int XML_DECL = 998; // general private String version; private Boolean standalone; private boolean processNsp; private boolean relaxed; private Hashtable entityMap; private int depth; private String[] elementStack = new String[16]; private String[] nspStack = new String[8]; private int[] nspCounts = new int[4]; // source private Reader reader; private String encoding; private char[] srcBuf; private int srcPos; private int srcCount; private int line; private int column; // txtbuffer /** Target buffer for storing incoming text (including aggregated resolved entities) */ private char[] txtBuf = new char[128]; /** Write position */ private int txtPos; // Event-related private int type; private boolean isWhitespace; private String namespace; private String prefix; private String name; private boolean degenerated; private int attributeCount; private String[] attributes = new String[16]; // private int stackMismatch = 0; private String error; /** * A separate peek buffer seems simpler than managing * wrap around in the first level read buffer */ private int[] peek = new int[2]; private int peekCount; private boolean wasCR; private boolean unresolved; private boolean token; public KXmlParser() { srcBuf = new char[Runtime.getRuntime().freeMemory() >= 1048576 ? 8192 : 128]; } private final boolean isProp(String n1, boolean prop, String n2) { if (!n1.startsWith("http://xmlpull.org/v1/doc/")) return false; if (prop) return n1.substring(42).equals(n2); else return n1.substring(40).equals(n2); } private final boolean adjustNsp() throws XmlPullParserException { boolean any = false; for (int i = 0; i < attributeCount << 2; i += 4) { // * 4 - 4; i >= 0; i -= 4) { String attrName = attributes[i + 2]; int cut = attrName.indexOf(':'); String prefix; if (cut != -1) { prefix = attrName.substring(0, cut); attrName = attrName.substring(cut + 1); } else if (attrName.equals("xmlns")) { prefix = attrName; attrName = null; } else continue; if (!prefix.equals("xmlns")) { any = true; } else { int j = (nspCounts[depth]++) << 1; nspStack = ensureCapacity(nspStack, j + 2); nspStack[j] = attrName; nspStack[j + 1] = attributes[i + 3]; if (attrName != null && attributes[i + 3].equals("")) error("illegal empty namespace"); // prefixMap = new PrefixMap (prefixMap, attrName, attr.getValue ()); //System.out.println (prefixMap); System.arraycopy( attributes, i + 4, attributes, i, ((--attributeCount) << 2) - i); i -= 4; } } if (any) { for (int i = (attributeCount << 2) - 4; i >= 0; i -= 4) { String attrName = attributes[i + 2]; int cut = attrName.indexOf(':'); if (cut == 0 && !relaxed) throw new RuntimeException( "illegal attribute name: " + attrName + " at " + this); else if (cut != -1) { String attrPrefix = attrName.substring(0, cut); attrName = attrName.substring(cut + 1); String attrNs = getNamespace(attrPrefix); if (attrNs == null && !relaxed) throw new RuntimeException( "Undefined Prefix: " + attrPrefix + " in " + this); attributes[i] = attrNs; attributes[i + 1] = attrPrefix; attributes[i + 2] = attrName; /* if (!relaxed) { for (int j = (attributeCount << 2) - 4; j > i; j -= 4) if (attrName.equals(attributes[j + 2]) && attrNs.equals(attributes[j])) exception( "Duplicate Attribute: {" + attrNs + "}" + attrName); } */ } } } int cut = name.indexOf(':'); if (cut == 0) error("illegal tag name: " + name); if (cut != -1) { prefix = name.substring(0, cut); name = name.substring(cut + 1); } this.namespace = getNamespace(prefix); if (this.namespace == null) { if (prefix != null) error("undefined prefix: " + prefix); this.namespace = NO_NAMESPACE; } return any; } private final String[] ensureCapacity(String[] arr, int required) { if (arr.length >= required) return arr; String[] bigger = new String[required + 16]; System.arraycopy(arr, 0, bigger, 0, arr.length); return bigger; } private final void error(String desc) throws XmlPullParserException { if (relaxed) { if (error == null) error = "ERR: " + desc; } else exception(desc); } private final void exception(String desc) throws XmlPullParserException { throw new XmlPullParserException( desc.length() < 100 ? desc : desc.substring(0, 100) + "\n", this, null); } /** * common base for next and nextToken. Clears the state, except from * txtPos and whitespace. Does not set the type variable */ private final void nextImpl() throws IOException, XmlPullParserException { if (reader == null) exception("No Input specified"); if (type == END_TAG) depth--; while (true) { attributeCount = -1; // degenerated needs to be handled before error because of possible // processor expectations(!) if (degenerated) { degenerated = false; type = END_TAG; return; } if (error != null) { for (int i = 0; i < error.length(); i++) push(error.charAt(i)); // text = error; error = null; type = COMMENT; return; } // if (relaxed // && (stackMismatch > 0 || (peek(0) == -1 && depth > 0))) { // int sp = (depth - 1) << 2; // type = END_TAG; // namespace = elementStack[sp]; // prefix = elementStack[sp + 1]; // name = elementStack[sp + 2]; // if (stackMismatch != 1) // error = "missing end tag /" + name + " inserted"; // if (stackMismatch > 0) // stackMismatch--; // return; // } prefix = null; name = null; namespace = null; // text = null; type = peekType(); switch (type) { case ENTITY_REF : pushEntity(); return; case START_TAG : parseStartTag(false); return; case END_TAG : parseEndTag(); return; case END_DOCUMENT : return; case TEXT : pushText('<', !token); if (depth == 0) { if (isWhitespace) type = IGNORABLE_WHITESPACE; // make exception switchable for instances.chg... !!!! // else // exception ("text '"+getText ()+"' not allowed outside root element"); } return; default : type = parseLegacy(token); if (type != XML_DECL) return; } } } private final int parseLegacy(boolean push) throws IOException, XmlPullParserException { String req = ""; int term; int result; int prev = 0; read(); // < int c = read(); if (c == '?') { if ((peek(0) == 'x' || peek(0) == 'X') && (peek(1) == 'm' || peek(1) == 'M')) { if (push) { push(peek(0)); push(peek(1)); } read(); read(); if ((peek(0) == 'l' || peek(0) == 'L') && peek(1) <= ' ') { if (line != 1 || column > 4) error("PI must not start with xml"); parseStartTag(true); if (attributeCount < 1 || !"version".equals(attributes[2])) error("version expected"); version = attributes[3]; int pos = 1; if (pos < attributeCount && "encoding".equals(attributes[2 + 4])) { encoding = attributes[3 + 4]; pos++; } if (pos < attributeCount && "standalone".equals(attributes[4 * pos + 2])) { String st = attributes[3 + 4 * pos]; if ("yes".equals(st)) standalone = new Boolean(true); else if ("no".equals(st)) standalone = new Boolean(false); else error("illegal standalone value: " + st); pos++; } if (pos != attributeCount) error("illegal xmldecl"); isWhitespace = true; txtPos = 0; return XML_DECL; } } /* int c0 = read (); int c1 = read (); int */ term = '?'; result = PROCESSING_INSTRUCTION; } else if (c == '!') { if (peek(0) == '-') { result = COMMENT; req = "--"; term = '-'; } else if (peek(0) == '[') { result = CDSECT; req = "[CDATA["; term = ']'; push = true; } else { result = DOCDECL; req = "DOCTYPE"; term = -1; } } else { error("illegal: <" + c); return COMMENT; } for (int i = 0; i < req.length(); i++) read(req.charAt(i)); if (result == DOCDECL) parseDoctype(push); else { while (true) { c = read(); if (c == -1){ error(UNEXPECTED_EOF); return COMMENT; } if (push) push(c); if ((term == '?' || c == term) && peek(0) == term && peek(1) == '>') break; prev = c; } if (term == '-' && prev == '-' && !relaxed) error("illegal comment delimiter: --->"); read(); read(); if (push && term != '?') txtPos--; } return result; } /** precondition: <! consumed */ private final void parseDoctype(boolean push) throws IOException, XmlPullParserException { int nesting = 1; boolean quoted = false; // read(); while (true) { int i = read(); switch (i) { case -1 : error(UNEXPECTED_EOF); return; case '\'' : quoted = !quoted; break; case '<' : if (!quoted) nesting++; break; case '>' : if (!quoted) { if ((--nesting) == 0) return; } break; } if (push) push(i); } } /* precondition: </ consumed */ private final void parseEndTag() throws IOException, XmlPullParserException { read(); // '<' read(); // '/' name = readName(); skip(); read('>'); int sp = (depth - 1) << 2; if (depth == 0) { error("element stack empty"); type = COMMENT; return; } if (!relaxed) { if (!name.equals(elementStack[sp + 3])) { error("expected: /" + elementStack[sp + 3] + " read: " + name); // become case insensitive in relaxed mode // int probe = sp; // while (probe >= 0 && !name.toLowerCase().equals(elementStack[probe + 3].toLowerCase())) { // stackMismatch++; // probe -= 4; // } // // if (probe < 0) { // stackMismatch = 0; // // text = "unexpected end tag ignored"; // type = COMMENT; // return; // } } namespace = elementStack[sp]; prefix = elementStack[sp + 1]; name = elementStack[sp + 2]; } } private final int peekType() throws IOException { switch (peek(0)) { case -1 : return END_DOCUMENT; case '&' : return ENTITY_REF; case '<' : switch (peek(1)) { case '/' : return END_TAG; case '?' : case '!' : return LEGACY; default : return START_TAG; } default : return TEXT; } } private final String get(int pos) { return new String(txtBuf, pos, txtPos - pos); } /* private final String pop (int pos) { String result = new String (txtBuf, pos, txtPos - pos); txtPos = pos; return result; } */ private final void push(int c) { isWhitespace &= c <= ' '; if (txtPos + 1 >= txtBuf.length) { // +1 to have enough space for 2 surrogates, if needed char[] bigger = new char[txtPos * 4 / 3 + 4]; System.arraycopy(txtBuf, 0, bigger, 0, txtPos); txtBuf = bigger; } if (c > 0xffff) { // write high Unicode value as surrogate pair int offset = c - 0x010000; txtBuf[txtPos++] = (char)((offset >>> 10) + 0xd800); // high surrogate txtBuf[txtPos++] = (char)((offset & 0x3ff) + 0xdc00); // low surrogate } else { txtBuf[txtPos++] = (char) c; } } /** Sets name and attributes */ private final void parseStartTag(boolean xmldecl) throws IOException, XmlPullParserException { if (!xmldecl) read(); name = readName(); attributeCount = 0; while (true) { skip(); int c = peek(0); if (xmldecl) { if (c == '?') { read(); read('>'); return; } } else { if (c == '/') { degenerated = true; read(); skip(); read('>'); break; } if (c == '>' && !xmldecl) { read(); break; } } if (c == -1) { error(UNEXPECTED_EOF); //type = COMMENT; return; } String attrName = readName(); if (attrName.length() == 0) { error("attr name expected"); //type = COMMENT; break; } int i = (attributeCount++) << 2; attributes = ensureCapacity(attributes, i + 4); attributes[i++] = ""; attributes[i++] = null; attributes[i++] = attrName; skip(); if (peek(0) != '=') { if(!relaxed){ error("Attr.value missing f. "+attrName); } attributes[i] = attrName; } else { read('='); skip(); int delimiter = peek(0); if (delimiter != '\'' && delimiter != '"') { if(!relaxed){ error("attr value delimiter missing!"); } delimiter = ' '; } else read(); int p = txtPos; pushText(delimiter, true); attributes[i] = get(p); txtPos = p; if (delimiter != ' ') read(); // skip endquote } } int sp = depth++ << 2; elementStack = ensureCapacity(elementStack, sp + 4); elementStack[sp + 3] = name; if (depth >= nspCounts.length) { int[] bigger = new int[depth + 4]; System.arraycopy(nspCounts, 0, bigger, 0, nspCounts.length); nspCounts = bigger; } nspCounts[depth] = nspCounts[depth - 1]; /* if(!relaxed){ for (int i = attributeCount - 1; i > 0; i--) { for (int j = 0; j < i; j++) { if (getAttributeName(i).equals(getAttributeName(j))) exception("Duplicate Attribute: " + getAttributeName(i)); } } } */ if (processNsp) adjustNsp(); else namespace = ""; elementStack[sp] = namespace; elementStack[sp + 1] = prefix; elementStack[sp + 2] = name; } /** * result: isWhitespace; if the setName parameter is set, * the name of the entity is stored in "name" */ private final void pushEntity() throws IOException, XmlPullParserException { push(read()); // & int pos = txtPos; while (true) { int c = peek(0); if (c == ';') { read(); break; } if (c < 128 && (c < '0' || c > '9') && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && c != '_' && c != '-' && c != '#') { if(!relaxed){ error("unterminated entity ref"); } System.out.println("broken entitiy: "+get(pos-1)); //; ends with:"+(char)c); // if (c != -1) // push(c); return; } push(read()); } String code = get(pos); txtPos = pos - 1; if (token && type == ENTITY_REF){ name = code; } if (code.charAt(0) == '#') { int c = (code.charAt(1) == 'x' ? Integer.parseInt(code.substring(2), 16) : Integer.parseInt(code.substring(1))); push(c); return; } String result = (String) entityMap.get(code); unresolved = result == null; if (unresolved) { if (!token) error("unresolved: &" + code + ";"); } else { for (int i = 0; i < result.length(); i++) push(result.charAt(i)); } } /** types: '<': parse to any token (for nextToken ()) '"': parse to quote ' ': parse to whitespace or '>' */ private final void pushText(int delimiter, boolean resolveEntities) throws IOException, XmlPullParserException { int next = peek(0); int cbrCount = 0; while (next != -1 && next != delimiter) { // covers eof, '<', '"' if (delimiter == ' ') if (next <= ' ' || next == '>') break; if (next == '&') { if (!resolveEntities) break; pushEntity(); } else if (next == '\n' && type == START_TAG) { read(); push(' '); } else push(read()); if (next == '>' && cbrCount >= 2 && delimiter != ']') error("Illegal: ]]>"); if (next == ']') cbrCount++; else cbrCount = 0; next = peek(0); } } private final void read(char c) throws IOException, XmlPullParserException { int a = read(); if (a != c) error("expected: '" + c + "' actual: '" + ((char) a) + "'"); } private final int read() throws IOException { int result; if (peekCount == 0) result = peek(0); else { result = peek[0]; peek[0] = peek[1]; } // else { // result = peek[0]; // System.arraycopy (peek, 1, peek, 0, peekCount-1); // } peekCount--; column++; if (result == '\n') { line++; column = 1; } return result; } /** Does never read more than needed */ private final int peek(int pos) throws IOException { while (pos >= peekCount) { int nw; if (srcBuf.length <= 1) nw = reader.read(); else if (srcPos < srcCount) nw = srcBuf[srcPos++]; else { srcCount = reader.read(srcBuf, 0, srcBuf.length); if (srcCount <= 0) nw = -1; else nw = srcBuf[0]; srcPos = 1; } if (nw == '\r') { wasCR = true; peek[peekCount++] = '\n'; } else { if (nw == '\n') { if (!wasCR) peek[peekCount++] = '\n'; } else peek[peekCount++] = nw; wasCR = false; } } return peek[pos]; } private final String readName() throws IOException, XmlPullParserException { int pos = txtPos; int c = peek(0); if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && c != '_' && c != ':' && c < 0x0c0 && !relaxed) error("name expected"); do { push(read()); c = peek(0); } while ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == ':' || c == '.' || c >= 0x0b7); String result = get(pos); txtPos = pos; return result; } private final void skip() throws IOException { while (true) { int c = peek(0); if (c > ' ' || c == -1) break; read(); } } // public part starts here... public void setInput(Reader reader) throws XmlPullParserException { this.reader = reader; line = 1; column = 0; type = START_DOCUMENT; name = null; namespace = null; degenerated = false; attributeCount = -1; encoding = null; version = null; standalone = null; if (reader == null) return; srcPos = 0; srcCount = 0; peekCount = 0; depth = 0; entityMap = new Hashtable(); entityMap.put("amp", "&"); entityMap.put("apos", "'"); entityMap.put("gt", ">"); entityMap.put("lt", "<"); entityMap.put("quot", "\""); } public void setInput(InputStream is, String _enc) throws XmlPullParserException { srcPos = 0; srcCount = 0; String enc = _enc; if (is == null) throw new IllegalArgumentException(); try { if (enc == null) { // read four bytes int chk = 0; while (srcCount < 4) { int i = is.read(); if (i == -1) break; chk = (chk << 8) | i; srcBuf[srcCount++] = (char) i; } if (srcCount == 4) { switch (chk) { case 0x00000FEFF : enc = "UTF-32BE"; srcCount = 0; break; case 0x0FFFE0000 : enc = "UTF-32LE"; srcCount = 0; break; case 0x03c : enc = "UTF-32BE"; srcBuf[0] = '<'; srcCount = 1; break; case 0x03c000000 : enc = "UTF-32LE"; srcBuf[0] = '<'; srcCount = 1; break; case 0x0003c003f : enc = "UTF-16BE"; srcBuf[0] = '<'; srcBuf[1] = '?'; srcCount = 2; break; case 0x03c003f00 : enc = "UTF-16LE"; srcBuf[0] = '<'; srcBuf[1] = '?'; srcCount = 2; break; case 0x03c3f786d : while (true) { int i = is.read(); if (i == -1) break; srcBuf[srcCount++] = (char) i; if (i == '>') { String s = new String(srcBuf, 0, srcCount); int i0 = s.indexOf("encoding"); if (i0 != -1) { while (s.charAt(i0) != '"' && s.charAt(i0) != '\'') i0++; char deli = s.charAt(i0++); int i1 = s.indexOf(deli, i0); enc = s.substring(i0, i1); } break; } } default : if ((chk & 0x0ffff0000) == 0x0FEFF0000) { enc = "UTF-16BE"; srcBuf[0] = (char) ((srcBuf[2] << 8) | srcBuf[3]); srcCount = 1; } else if ((chk & 0x0ffff0000) == 0x0fffe0000) { enc = "UTF-16LE"; srcBuf[0] = (char) ((srcBuf[3] << 8) | srcBuf[2]); srcCount = 1; } else if ((chk & 0x0ffffff00) == 0x0EFBBBF00) { enc = "UTF-8"; srcBuf[0] = srcBuf[3]; srcCount = 1; } } } } if (enc == null) enc = "UTF-8"; int sc = srcCount; setInput(new InputStreamReader(is, enc)); encoding = _enc; srcCount = sc; } catch (Exception e) { throw new XmlPullParserException( "Invalid stream or encoding: " + e.toString(), this, e); } } public boolean getFeature(String feature) { if (XmlPullParser.FEATURE_PROCESS_NAMESPACES.equals(feature)) return processNsp; else if (isProp(feature, false, "relaxed")) return relaxed; else return false; } public String getInputEncoding() { return encoding; } public void defineEntityReplacementText(String entity, String value) throws XmlPullParserException { if (entityMap == null) throw new RuntimeException("entity replacement text must be defined after setInput!"); entityMap.put(entity, value); } public Object getProperty(String property) { if (isProp(property, true, "xmldecl-version")) return version; if (isProp(property, true, "xmldecl-standalone")) return standalone; if (isProp(property, true, "location")) return location != null ? location : reader.toString(); return null; } public int getNamespaceCount(int depth) { if (depth > this.depth) throw new IndexOutOfBoundsException(); return nspCounts[depth]; } public String getNamespacePrefix(int pos) { return nspStack[pos << 1]; } public String getNamespaceUri(int pos) { return nspStack[(pos << 1) + 1]; } public String getNamespace(String prefix) { if ("xml".equals(prefix)) return "http://www.w3.org/XML/1998/namespace"; if ("xmlns".equals(prefix)) return "http://www.w3.org/2000/xmlns/"; for (int i = (getNamespaceCount(depth) << 1) - 2; i >= 0; i -= 2) { if (prefix == null) { if (nspStack[i] == null) return nspStack[i + 1]; } else if (prefix.equals(nspStack[i])) return nspStack[i + 1]; } return null; } public int getDepth() { return depth; } public String getPositionDescription() { StringBuffer buf = new StringBuffer(type < TYPES.length ? TYPES[type] : "unknown"); buf.append(' '); if (type == START_TAG || type == END_TAG) { if (degenerated) buf.append("(empty) "); buf.append('<'); if (type == END_TAG) buf.append('/'); if (prefix != null) buf.append("{" + namespace + "}" + prefix + ":"); buf.append(name); int cnt = attributeCount << 2; for (int i = 0; i < cnt; i += 4) { buf.append(' '); if (attributes[i + 1] != null) buf.append( "{" + attributes[i] + "}" + attributes[i + 1] + ":"); buf.append(attributes[i + 2] + "='" + attributes[i + 3] + "'"); } buf.append('>'); } else if (type == IGNORABLE_WHITESPACE); else if (type != TEXT) buf.append(getText()); else if (isWhitespace) buf.append("(whitespace)"); else { String text = getText(); if (text.length() > 16) text = text.substring(0, 16) + "..."; buf.append(text); } buf.append("@"+line + ":" + column); if(location != null){ buf.append(" in "); buf.append(location); } else if(reader != null){ buf.append(" in "); buf.append(reader.toString()); } return buf.toString(); } public int getLineNumber() { return line; } public int getColumnNumber() { return column; } public boolean isWhitespace() throws XmlPullParserException { if (type != TEXT && type != IGNORABLE_WHITESPACE && type != CDSECT) exception(ILLEGAL_TYPE); return isWhitespace; } public String getText() { return type < TEXT || (type == ENTITY_REF && unresolved) ? null : get(0); } public char[] getTextCharacters(int[] poslen) { if (type >= TEXT) { if (type == ENTITY_REF) { poslen[0] = 0; poslen[1] = name.length(); return name.toCharArray(); } poslen[0] = 0; poslen[1] = txtPos; return txtBuf; } poslen[0] = -1; poslen[1] = -1; return null; } public String getNamespace() { return namespace; } public String getName() { return name; } public String getPrefix() { return prefix; } public boolean isEmptyElementTag() throws XmlPullParserException { if (type != START_TAG) exception(ILLEGAL_TYPE); return degenerated; } public int getAttributeCount() { return attributeCount; } public String getAttributeType(int index) { return "CDATA"; } public boolean isAttributeDefault(int index) { return false; } public String getAttributeNamespace(int index) { if (index >= attributeCount) throw new IndexOutOfBoundsException(); return attributes[index << 2]; } public String getAttributeName(int index) { if (index >= attributeCount) throw new IndexOutOfBoundsException(); return attributes[(index << 2) + 2]; } public String getAttributePrefix(int index) { if (index >= attributeCount) throw new IndexOutOfBoundsException(); return attributes[(index << 2) + 1]; } public String getAttributeValue(int index) { if (index >= attributeCount) throw new IndexOutOfBoundsException(); return attributes[(index << 2) + 3]; } public String getAttributeValue(String namespace, String name) { for (int i = (attributeCount << 2) - 4; i >= 0; i -= 4) { if (attributes[i + 2].equals(name) && (namespace == null || attributes[i].equals(namespace))) return attributes[i + 3]; } return null; } public int getEventType() throws XmlPullParserException { return type; } public int next() throws XmlPullParserException, IOException { txtPos = 0; isWhitespace = true; int minType = 9999; token = false; do { nextImpl(); if (type < minType) minType = type; // if (curr <= TEXT) type = curr; } while (minType > ENTITY_REF // ignorable || (minType >= TEXT && peekType() >= TEXT)); type = minType; if (type > TEXT) type = TEXT; return type; } public int nextToken() throws XmlPullParserException, IOException { isWhitespace = true; txtPos = 0; token = true; nextImpl(); return type; } // // utility methods to make XML parsing easier ... public int nextTag() throws XmlPullParserException, IOException { next(); if (type == TEXT && isWhitespace) next(); if (type != END_TAG && type != START_TAG) exception("unexpected type"); return type; } public void require(int type, String namespace, String name) throws XmlPullParserException, IOException { if (type != this.type || (namespace != null && !namespace.equals(getNamespace())) || (name != null && !name.equals(getName()))) exception( "expected: " + TYPES[type] + " {" + namespace + "}" + name); } public String nextText() throws XmlPullParserException, IOException { if (type != START_TAG) exception("precondition: START_TAG"); next(); String result; if (type == TEXT) { result = getText(); next(); } else result = ""; if (type != END_TAG) exception("END_TAG expected"); return result; } public void setFeature(String feature, boolean value) throws XmlPullParserException { if (XmlPullParser.FEATURE_PROCESS_NAMESPACES.equals(feature)) processNsp = value; else if (isProp(feature, false, "relaxed")) relaxed = value; else exception("unsupported feature: " + feature); } public void setProperty(String property, Object value) throws XmlPullParserException { if(isProp(property, true, "location")) location = value; else throw new XmlPullParserException("unsupported property: " + property); } /** * Skip sub tree that is currently porser positioned on. *
NOTE: parser must be on START_TAG and when funtion returns * parser will be positioned on corresponding END_TAG. */ // Implementation copied from Alek's mail... public void skipSubTree() throws XmlPullParserException, IOException { require(START_TAG, null, null); int level = 1; while (level > 0) { int eventType = next(); if (eventType == END_TAG) { --level; } else if (eventType == START_TAG) { ++level; } } } } ================================================ FILE: src/main/java/org/kxml2/io/KXmlSerializer.java ================================================ /* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ package org.kxml2.io; import java.io.*; import org.xmlpull.v1.*; public class KXmlSerializer implements XmlSerializer { // static final String UNDEFINED = ":"; private Writer writer; private boolean pending; private int auto; private int depth; private String[] elementStack = new String[12]; //nsp/prefix/name private int[] nspCounts = new int[4]; private String[] nspStack = new String[8]; //prefix/nsp; both empty are "" private boolean[] indent = new boolean[4]; private boolean unicode; private String encoding; private final void check(boolean close) throws IOException { if (!pending) return; depth++; pending = false; if (indent.length <= depth) { boolean[] hlp = new boolean[depth + 4]; System.arraycopy(indent, 0, hlp, 0, depth); indent = hlp; } indent[depth] = indent[depth - 1]; for (int i = nspCounts[depth - 1]; i < nspCounts[depth]; i++) { writer.write(' '); writer.write("xmlns"); if (!"".equals(nspStack[i * 2])) { writer.write(':'); writer.write(nspStack[i * 2]); } else if ("".equals(getNamespace()) && !"".equals(nspStack[i * 2 + 1])) throw new IllegalStateException("Cannot set default namespace for elements in no namespace"); writer.write("=\""); writeEscaped(nspStack[i * 2 + 1], '"'); writer.write('"'); } if (nspCounts.length <= depth + 1) { int[] hlp = new int[depth + 8]; System.arraycopy(nspCounts, 0, hlp, 0, depth + 1); nspCounts = hlp; } nspCounts[depth + 1] = nspCounts[depth]; // nspCounts[depth + 2] = nspCounts[depth]; writer.write(close ? " />" : ">"); } private final void writeEscaped(String s, int quot) throws IOException { for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); switch (c) { case '\n': case '\r': case '\t': if(quot == -1) writer.write(c); else writer.write("&#"+((int) c)+';'); break; case '&' : writer.write("&"); break; case '>' : writer.write(">"); break; case '<' : writer.write("<"); break; case '"' : case '\'' : if (c == quot) { writer.write( c == '"' ? """ : "'"); break; } default : //if(c < ' ') // throw new IllegalArgumentException("Illegal control code:"+((int) c)); if (i < s.length() - 1) { char cLow = s.charAt(i + 1); // c is high surrogate and cLow is low surrogate if (c >= 0xd800 && c <= 0xdbff && cLow >= 0xdc00 && cLow <= 0xdfff) { // write surrogate pair as single code point int n = ((c - 0xd800) << 10) + (cLow - 0xdc00) + 0x010000; writer.write("&#" + n + ";"); i++; // Skip the low surrogate break; } // Does nothing smart about orphan surrogates, just output them "as is" } if (c >= ' ' && c !='@' && (c < 127 || unicode)) { writer.write(c); } else { writer.write("&#" + ((int) c) + ";"); } } } } /* private final void writeIndent() throws IOException { writer.write("\r\n"); for (int i = 0; i < depth; i++) writer.write(' '); }*/ public void docdecl(String dd) throws IOException { writer.write(""); } public void endDocument() throws IOException { while (depth > 0) { endTag( elementStack[depth * 3 - 3], elementStack[depth * 3 - 1]); } flush(); } public void entityRef(String name) throws IOException { check(false); writer.write('&'); writer.write(name); writer.write(';'); } public boolean getFeature(String name) { //return false; return ( "http://xmlpull.org/v1/doc/features.html#indent-output" .equals( name)) ? indent[depth] : false; } public String getPrefix(String namespace, boolean create) { try { return getPrefix(namespace, false, create); } catch (IOException e) { throw new RuntimeException(e.toString()); } } private final String getPrefix( String namespace, boolean includeDefault, boolean create) throws IOException { for (int i = nspCounts[depth + 1] * 2 - 2; i >= 0; i -= 2) { if (nspStack[i + 1].equals(namespace) && (includeDefault || !nspStack[i].equals(""))) { String cand = nspStack[i]; for (int j = i + 2; j < nspCounts[depth + 1] * 2; j++) { if (nspStack[j].equals(cand)) { cand = null; break; } } if (cand != null) return cand; } } if (!create) return null; String prefix; if ("".equals(namespace)) prefix = ""; else { do { prefix = "n" + (auto++); for (int i = nspCounts[depth + 1] * 2 - 2; i >= 0; i -= 2) { if (prefix.equals(nspStack[i])) { prefix = null; break; } } } while (prefix == null); } boolean p = pending; pending = false; setPrefix(prefix, namespace); pending = p; return prefix; } public Object getProperty(String name) { throw new RuntimeException("Unsupported property"); } public void ignorableWhitespace(String s) throws IOException { text(s); } public void setFeature(String name, boolean value) { if ("http://xmlpull.org/v1/doc/features.html#indent-output" .equals(name)) { indent[depth] = value; } else throw new RuntimeException("Unsupported Feature"); } public void setProperty(String name, Object value) { throw new RuntimeException( "Unsupported Property:" + value); } public void setPrefix(String prefix, String namespace) throws IOException { check(false); if (prefix == null) prefix = ""; if (namespace == null) namespace = ""; String defined = getPrefix(namespace, true, false); // boil out if already defined if (prefix.equals(defined)) return; int pos = (nspCounts[depth + 1]++) << 1; if (nspStack.length < pos + 1) { String[] hlp = new String[nspStack.length + 16]; System.arraycopy(nspStack, 0, hlp, 0, pos); nspStack = hlp; } nspStack[pos++] = prefix; nspStack[pos] = namespace; } public void setOutput(Writer writer) { this.writer = writer; // elementStack = new String[12]; //nsp/prefix/name //nspCounts = new int[4]; //nspStack = new String[8]; //prefix/nsp //indent = new boolean[4]; nspCounts[0] = 2; nspCounts[1] = 2; nspStack[0] = ""; nspStack[1] = ""; nspStack[2] = "xml"; nspStack[3] = "http://www.w3.org/XML/1998/namespace"; pending = false; auto = 0; depth = 0; unicode = false; } public void setOutput(OutputStream os, String encoding) throws IOException { if (os == null) throw new IllegalArgumentException(); setOutput( encoding == null ? new OutputStreamWriter(os) : new OutputStreamWriter(os, encoding)); this.encoding = encoding; if (encoding != null && encoding.toLowerCase().startsWith("utf")) unicode = true; } public void startDocument( String encoding, Boolean standalone) throws IOException { writer.write(""); } public XmlSerializer startTag(String namespace, String name) throws IOException { check(false); // if (namespace == null) // namespace = ""; if (indent[depth]) { writer.write("\r\n"); for (int i = 0; i < depth; i++) writer.write(" "); } int esp = depth * 3; if (elementStack.length < esp + 3) { String[] hlp = new String[elementStack.length + 12]; System.arraycopy(elementStack, 0, hlp, 0, esp); elementStack = hlp; } String prefix = namespace == null ? "" : getPrefix(namespace, true, true); if ("".equals(namespace)) { for (int i = nspCounts[depth]; i < nspCounts[depth + 1]; i++) { if ("".equals(nspStack[i * 2]) && !"".equals(nspStack[i * 2 + 1])) { throw new IllegalStateException("Cannot set default namespace for elements in no namespace"); } } } elementStack[esp++] = namespace; elementStack[esp++] = prefix; elementStack[esp] = name; writer.write('<'); if (!"".equals(prefix)) { writer.write(prefix); writer.write(':'); } writer.write(name); pending = true; return this; } public XmlSerializer attribute( String namespace, String name, String value) throws IOException { if (!pending) throw new IllegalStateException("illegal position for attribute"); // int cnt = nspCounts[depth]; if (namespace == null) namespace = ""; // depth--; // pending = false; String prefix = "".equals(namespace) ? "" : getPrefix(namespace, false, true); // pending = true; // depth++; /* if (cnt != nspCounts[depth]) { writer.write(' '); writer.write("xmlns"); if (nspStack[cnt * 2] != null) { writer.write(':'); writer.write(nspStack[cnt * 2]); } writer.write("=\""); writeEscaped(nspStack[cnt * 2 + 1], '"'); writer.write('"'); } */ writer.write(' '); if (!"".equals(prefix)) { writer.write(prefix); writer.write(':'); } writer.write(name); writer.write('='); char q = value.indexOf('"') == -1 ? '"' : '\''; writer.write(q); writeEscaped(value, q); writer.write(q); return this; } public void flush() throws IOException { check(false); writer.flush(); } /* public void close() throws IOException { check(); writer.close(); } */ public XmlSerializer endTag(String namespace, String name) throws IOException { if (!pending) depth--; // if (namespace == null) // namespace = ""; if ((namespace == null && elementStack[depth * 3] != null) || (namespace != null && !namespace.equals(elementStack[depth * 3])) || !elementStack[depth * 3 + 2].equals(name)) throw new IllegalArgumentException(" does not match start"); if (pending) { check(true); depth--; } else { if (indent[depth + 1]) { writer.write("\r\n"); for (int i = 0; i < depth; i++) writer.write(" "); } writer.write("'); } nspCounts[depth + 1] = nspCounts[depth]; return this; } public String getNamespace() { return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 3]; } public String getName() { return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 1]; } public int getDepth() { return pending ? depth + 1 : depth; } public XmlSerializer text(String text) throws IOException { check(false); indent[depth] = false; writeEscaped(text, -1); return this; } public XmlSerializer text(char[] text, int start, int len) throws IOException { text(new String(text, start, len)); return this; } public void cdsect(String data) throws IOException { check(false); writer.write(""); } public void comment(String comment) throws IOException { check(false); writer.write(""); } public void processingInstruction(String pi) throws IOException { check(false); writer.write(""); } } ================================================ FILE: src/main/java/org/kxml2/kdom/Document.java ================================================ /* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ package org.kxml2.kdom; import java.io.*; import org.xmlpull.v1.*; /** The document consists of some legacy events and a single root element. This class basically adds some consistency checks to Node. */ public class Document extends Node { protected int rootIndex = -1; String encoding; Boolean standalone; /** returns "#document" */ public String getEncoding () { return encoding; } public void setEncoding(String enc) { this.encoding = enc; } public void setStandalone (Boolean standalone) { this.standalone = standalone; } public Boolean getStandalone() { return standalone; } public String getName() { return "#document"; } /** Adds a child at the given index position. Throws an exception when a second root element is added */ public void addChild(int index, int type, Object child) { if (type == ELEMENT) { // if (rootIndex != -1) // throw new RuntimeException("Only one document root element allowed"); rootIndex = index; } else if (rootIndex >= index) rootIndex++; super.addChild(index, type, child); } /** reads the document and checks if the last event is END_DOCUMENT. If not, an exception is thrown. The end event is consumed. For parsing partial XML structures, consider using Node.parse (). */ public void parse(XmlPullParser parser) throws IOException, XmlPullParserException { parser.require(XmlPullParser.START_DOCUMENT, null, null); parser.nextToken (); encoding = parser.getInputEncoding(); standalone = (Boolean)parser.getProperty ("http://xmlpull.org/v1/doc/properties.html#xmldecl-standalone"); super.parse(parser); if (parser.getEventType() != XmlPullParser.END_DOCUMENT) throw new RuntimeException("Document end expected!"); } public void removeChild(int index) { if (index == rootIndex) rootIndex = -1; else if (index < rootIndex) rootIndex--; super.removeChild(index); } /** returns the root element of this document. */ public Element getRootElement() { if (rootIndex == -1) throw new RuntimeException("Document has no root element!"); return (Element) getChild(rootIndex); } /** Writes this node to the given XmlWriter. For node and document, this method is identical to writeChildren, except that the stream is flushed automatically. */ public void write(XmlSerializer writer) throws IOException { writer.startDocument(encoding, standalone); writeChildren(writer); writer.endDocument(); } } ================================================ FILE: src/main/java/org/kxml2/kdom/Element.java ================================================ /* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ package org.kxml2.kdom; import java.io.*; import java.util.*; import org.xmlpull.v1.*; /** * In order to create an element, please use the createElement method * instead of invoking the constructor directly. The right place to * add user defined initialization code is the init method. */ public class Element extends Node { protected String namespace; protected String name; protected Vector attributes; protected Node parent; protected Vector prefixes; public Element() { } /** * called when all properties are set, but before children * are parsed. Please do not use setParent for initialization * code any longer. */ public void init() { } /** * removes all children and attributes */ public void clear() { attributes = null; children = null; } /** * Forwards creation request to parent if any, otherwise * calls super.createElement. */ public Element createElement( String namespace, String name) { return (this.parent == null) ? super.createElement(namespace, name) : this.parent.createElement(namespace, name); } /** * Returns the number of attributes of this element. */ public int getAttributeCount() { return attributes == null ? 0 : attributes.size (); } public String getAttributeNamespace (int index) { return ((String []) attributes.elementAt (index)) [0]; } /* public String getAttributePrefix (int index) { return ((String []) attributes.elementAt (index)) [1]; }*/ public String getAttributeName (int index) { return ((String []) attributes.elementAt (index)) [1]; } public String getAttributeValue (int index) { return ((String []) attributes.elementAt (index)) [2]; } public String getAttributeValue (String namespace, String name) { for (int i = 0; i < getAttributeCount (); i++) { if (name.equals (getAttributeName (i)) && (namespace == null || namespace.equals (getAttributeNamespace(i)))) { return getAttributeValue (i); } } return null; } /** * Returns the root node, determined by ascending to the * all parents un of the root element. */ public Node getRoot() { Element current = this; while (current.parent != null) { if (!(current.parent instanceof Element)) return current.parent; current = (Element) current.parent; } return current; } /** * returns the (local) name of the element */ public String getName() { return name; } /** * returns the namespace of the element */ public String getNamespace() { return namespace; } /** * returns the namespace for the given prefix */ public String getNamespaceUri (String prefix) { int cnt = getNamespaceCount (); for (int i = 0; i < cnt; i++) { if (prefix == getNamespacePrefix (i) || (prefix != null && prefix.equals (getNamespacePrefix (i)))) return getNamespaceUri (i); } return parent instanceof Element ? ((Element) parent).getNamespaceUri (prefix) : null; } /** * returns the number of declared namespaces, NOT including * parent elements */ public int getNamespaceCount () { return (prefixes == null ? 0 : prefixes.size ()); } public String getNamespacePrefix (int i) { return ((String []) prefixes.elementAt (i)) [0]; } public String getNamespaceUri (int i) { return ((String []) prefixes.elementAt (i)) [1]; } /** * Returns the parent node of this element */ public Node getParent() { return parent; } /* * Returns the parent element if available, null otherwise public Element getParentElement() { return (parent instanceof Element) ? ((Element) parent) : null; } */ /** * Builds the child elements from the given Parser. By overwriting * parse, an element can take complete control over parsing its * subtree. */ public void parse(XmlPullParser parser) throws IOException, XmlPullParserException { for (int i = parser.getNamespaceCount (parser.getDepth () - 1); i < parser.getNamespaceCount (parser.getDepth ()); i++) { setPrefix (parser.getNamespacePrefix (i), parser.getNamespaceUri(i)); } for (int i = 0; i < parser.getAttributeCount (); i++) setAttribute (parser.getAttributeNamespace (i), // parser.getAttributePrefix (i), parser.getAttributeName (i), parser.getAttributeValue (i)); // if (prefixMap == null) throw new RuntimeException ("!!"); init(); if (parser.isEmptyElementTag()) parser.nextToken (); else { parser.nextToken (); super.parse(parser); if (getChildCount() == 0) addChild(IGNORABLE_WHITESPACE, ""); } parser.require( XmlPullParser.END_TAG, getNamespace(), getName()); parser.nextToken (); } /** * Sets the given attribute; a value of null removes the attribute */ public void setAttribute (String namespace, String name, String value) { if (attributes == null) attributes = new Vector (); if (namespace == null) namespace = ""; for (int i = attributes.size()-1; i >=0; i--){ String[] attribut = (String[]) attributes.elementAt(i); if (attribut[0].equals(namespace) && attribut[1].equals(name)){ if (value == null) { attributes.removeElementAt(i); } else { attribut[2] = value; } return; } } attributes.addElement (new String [] {namespace, name, value}); } /** * Sets the given prefix; a namespace value of null removess the * prefix */ public void setPrefix (String prefix, String namespace) { if (prefixes == null) prefixes = new Vector (); prefixes.addElement (new String [] {prefix, namespace}); } /** * sets the name of the element */ public void setName(String name) { this.name = name; } /** * sets the namespace of the element. Please note: For no * namespace, please use Xml.NO_NAMESPACE, null is not a legal * value. Currently, null is converted to Xml.NO_NAMESPACE, but * future versions may throw an exception. */ public void setNamespace(String namespace) { if (namespace == null) throw new NullPointerException ("Use \"\" for empty namespace"); this.namespace = namespace; } /** * Sets the Parent of this element. Automatically called from the * add method. Please use with care, you can simply * create inconsitencies in the document tree structure using * this method! */ protected void setParent(Node parent) { this.parent = parent; } /** * Writes this element and all children to the given XmlWriter. */ public void write(XmlSerializer writer) throws IOException { if (prefixes != null) { for (int i = 0; i < prefixes.size (); i++) { writer.setPrefix (getNamespacePrefix (i), getNamespaceUri (i)); } } writer.startTag( getNamespace(), getName()); int len = getAttributeCount(); for (int i = 0; i < len; i++) { writer.attribute( getAttributeNamespace(i), getAttributeName(i), getAttributeValue(i)); } writeChildren(writer); writer.endTag(getNamespace (), getName ()); } } ================================================ FILE: src/main/java/org/kxml2/kdom/Node.java ================================================ /* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ package org.kxml2.kdom; import java.util.*; import java.io.*; import org.xmlpull.v1.*; /** A common base class for Document and Element, also used for storing XML fragments. */ public class Node { //implements XmlIO{ public static final int DOCUMENT = 0; public static final int ELEMENT = 2; public static final int TEXT = 4; public static final int CDSECT = 5; public static final int ENTITY_REF = 6; public static final int IGNORABLE_WHITESPACE = 7; public static final int PROCESSING_INSTRUCTION = 8; public static final int COMMENT = 9; public static final int DOCDECL = 10; protected Vector children; protected StringBuffer types; /** inserts the given child object of the given type at the given index. */ public void addChild(int index, int type, Object child) { if (child == null) throw new NullPointerException(); if (children == null) { children = new Vector(); types = new StringBuffer(); } if (type == ELEMENT) { if (!(child instanceof Element)) throw new RuntimeException("Element obj expected)"); ((Element) child).setParent(this); } else if (!(child instanceof String)) throw new RuntimeException("String expected"); children.insertElementAt(child, index); types.insert(index, (char) type); } /** convenience method for addChild (getChildCount (), child) */ public void addChild(int type, Object child) { addChild(getChildCount(), type, child); } /** Builds a default element with the given properties. Elements should always be created using this method instead of the constructor in order to enable construction of specialized subclasses by deriving custom Document classes. Please note: For no namespace, please use Xml.NO_NAMESPACE, null is not a legal value. Currently, null is converted to Xml.NO_NAMESPACE, but future versions may throw an exception. */ public Element createElement(String namespace, String name) { Element e = new Element(); e.namespace = namespace == null ? "" : namespace; e.name = name; return e; } /** Returns the child object at the given index. For child elements, an Element object is returned. For all other child types, a String is returned. */ public Object getChild(int index) { return children.elementAt(index); } /** Returns the number of child objects */ public int getChildCount() { return children == null ? 0 : children.size(); } /** returns the element at the given index. If the node at the given index is a text node, null is returned */ public Element getElement(int index) { Object child = getChild(index); return (child instanceof Element) ? (Element) child : null; } /** Returns the element with the given namespace and name. If the element is not found, or more than one matching elements are found, an exception is thrown. */ public Element getElement(String namespace, String name) { int i = indexOf(namespace, name, 0); int j = indexOf(namespace, name, i + 1); if (i == -1 || j != -1) throw new RuntimeException( "Element {" + namespace + "}" + name + (i == -1 ? " not found in " : " more than once in ") + this); return getElement(i); } /* returns "#document-fragment". For elements, the element name is returned public String getName() { return "#document-fragment"; } /** Returns the namespace of the current element. For Node and Document, Xml.NO_NAMESPACE is returned. public String getNamespace() { return ""; } public int getNamespaceCount () { return 0; } /** returns the text content if the element has text-only content. Throws an exception for mixed content public String getText() { StringBuffer buf = new StringBuffer(); int len = getChildCount(); for (int i = 0; i < len; i++) { if (isText(i)) buf.append(getText(i)); else if (getType(i) == ELEMENT) throw new RuntimeException("not text-only content!"); } return buf.toString(); } */ /** Returns the text node with the given index or null if the node with the given index is not a text node. */ public String getText(int index) { return (isText(index)) ? (String) getChild(index) : null; } /** Returns the type of the child at the given index. Possible types are ELEMENT, TEXT, COMMENT, and PROCESSING_INSTRUCTION */ public int getType(int index) { return types.charAt(index); } /** Convenience method for indexOf (getNamespace (), name, startIndex). public int indexOf(String name, int startIndex) { return indexOf(getNamespace(), name, startIndex); } */ /** Performs search for an element with the given namespace and name, starting at the given start index. A null namespace matches any namespace, please use Xml.NO_NAMESPACE for no namespace). returns -1 if no matching element was found. */ public int indexOf(String namespace, String name, int startIndex) { int len = getChildCount(); for (int i = startIndex; i < len; i++) { Element child = getElement(i); if (child != null && name.equals(child.getName()) && (namespace == null || namespace.equals(child.getNamespace()))) return i; } return -1; } public boolean isText(int i) { int t = getType(i); return t == TEXT || t == IGNORABLE_WHITESPACE || t == CDSECT; } /** Recursively builds the child elements from the given parser until an end tag or end document is found. The end tag is not consumed. */ public void parse(XmlPullParser parser) throws IOException, XmlPullParserException { boolean leave = false; do { int type = parser.getEventType(); // System.out.println(parser.getPositionDescription()); switch (type) { case XmlPullParser.START_TAG : { Element child = createElement( parser.getNamespace(), parser.getName()); // child.setAttributes (event.getAttributes ()); addChild(ELEMENT, child); // order is important here since // setparent may perform some init code! child.parse(parser); break; } case XmlPullParser.END_DOCUMENT : case XmlPullParser.END_TAG : leave = true; break; default : if (parser.getText() != null) addChild( type == XmlPullParser.ENTITY_REF ? TEXT : type, parser.getText()); else if ( type == XmlPullParser.ENTITY_REF && parser.getName() != null) { addChild(ENTITY_REF, parser.getName()); } parser.nextToken(); } } while (!leave); } /** Removes the child object at the given index */ public void removeChild(int idx) { children.removeElementAt(idx); /*** Modification by HHS - start ***/ // types.deleteCharAt (index); /***/ int n = types.length() - 1; for (int i = idx; i < n; i++) types.setCharAt(i, types.charAt(i + 1)); types.setLength(n); /*** Modification by HHS - end ***/ } /* returns a valid XML representation of this Element including attributes and children. public String toString() { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); XmlWriter xw = new XmlWriter(new OutputStreamWriter(bos)); write(xw); xw.close(); return new String(bos.toByteArray()); } catch (IOException e) { throw new RuntimeException(e.toString()); } } */ /** Writes this node to the given XmlWriter. For node and document, this method is identical to writeChildren, except that the stream is flushed automatically. */ public void write(XmlSerializer writer) throws IOException { writeChildren(writer); writer.flush(); } /** Writes the children of this node to the given XmlWriter. */ public void writeChildren(XmlSerializer writer) throws IOException { if (children == null) return; int len = children.size(); for (int i = 0; i < len; i++) { int type = getType(i); Object child = children.elementAt(i); switch (type) { case ELEMENT : ((Element) child).write(writer); break; case TEXT : writer.text((String) child); break; case IGNORABLE_WHITESPACE : writer.ignorableWhitespace((String) child); break; case CDSECT : writer.cdsect((String) child); break; case COMMENT : writer.comment((String) child); break; case ENTITY_REF : writer.entityRef((String) child); break; case PROCESSING_INSTRUCTION : writer.processingInstruction((String) child); break; case DOCDECL : writer.docdecl((String) child); break; default : throw new RuntimeException("Illegal type: " + type); } } } } ================================================ FILE: src/main/java/org/kxml2/wap/Wbxml.java ================================================ /* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ package org.kxml2.wap; /** contains the WBXML constants */ public interface Wbxml { static public final int SWITCH_PAGE = 0; static public final int END = 1; static public final int ENTITY = 2; static public final int STR_I = 3; static public final int LITERAL = 4; static public final int EXT_I_0 = 0x40; static public final int EXT_I_1 = 0x41; static public final int EXT_I_2 = 0x42; static public final int PI = 0x43; static public final int LITERAL_C = 0x44; static public final int EXT_T_0 = 0x80; static public final int EXT_T_1 = 0x81; static public final int EXT_T_2 = 0x82; static public final int STR_T = 0x83; static public final int LITERAL_A = 0x084; static public final int EXT_0 = 0x0c0; static public final int EXT_1 = 0x0c1; static public final int EXT_2 = 0x0c2; static public final int OPAQUE = 0x0c3; static public final int LITERAL_AC = 0x0c4; } ================================================ FILE: src/main/java/org/kxml2/wap/WbxmlParser.java ================================================ /* Copyright (c) 2002,2003,2004 Stefan Haustein, Oberhausen, Rhld., Germany * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ // Contributors: Bjorn Aadland, Chris Bartley, Nicola Fankhauser, // Victor Havin, Christian Kurzke, Bogdan Onoiu, // Elias Ross, Jain Sanjay, David Santoro. package org.kxml2.wap; import java.io.*; import java.util.Vector; import java.util.Hashtable; import org.xmlpull.v1.*; public class WbxmlParser implements XmlPullParser { static final String HEX_DIGITS = "0123456789abcdef"; /** Parser event type for Wbxml-specific events. The Wbxml event code can be * accessed with getWapCode() */ public static final int WAP_EXTENSION = 64; static final private String UNEXPECTED_EOF = "Unexpected EOF"; static final private String ILLEGAL_TYPE = "Wrong event type"; private InputStream in; private int TAG_TABLE = 0; private int ATTR_START_TABLE = 1; private int ATTR_VALUE_TABLE = 2; private String[] attrStartTable; private String[] attrValueTable; private String[] tagTable; private byte[] stringTable; private Hashtable cacheStringTable = null; private boolean processNsp; private int depth; private String[] elementStack = new String[16]; private String[] nspStack = new String[8]; private int[] nspCounts = new int[4]; private int attributeCount; private String[] attributes = new String[16]; private int nextId = -2; private Vector tables = new Vector(); private int version; private int publicIdentifierId; // StartTag current; // ParseEvent next; private String prefix; private String namespace; private String name; private String text; private Object wapExtensionData; private int wapCode; private int type; private boolean degenerated; private boolean isWhitespace; private String encoding; public boolean getFeature(String feature) { if (XmlPullParser .FEATURE_PROCESS_NAMESPACES .equals(feature)) return processNsp; else return false; } public String getInputEncoding() { return encoding; } public void defineEntityReplacementText( String entity, String value) throws XmlPullParserException { // just ignore, has no effect } public Object getProperty(String property) { return null; } public int getNamespaceCount(int depth) { if (depth > this.depth) throw new IndexOutOfBoundsException(); return nspCounts[depth]; } public String getNamespacePrefix(int pos) { return nspStack[pos << 1]; } public String getNamespaceUri(int pos) { return nspStack[(pos << 1) + 1]; } public String getNamespace(String prefix) { if ("xml".equals(prefix)) return "http://www.w3.org/XML/1998/namespace"; if ("xmlns".equals(prefix)) return "http://www.w3.org/2000/xmlns/"; for (int i = (getNamespaceCount(depth) << 1) - 2; i >= 0; i -= 2) { if (prefix == null) { if (nspStack[i] == null) return nspStack[i + 1]; } else if (prefix.equals(nspStack[i])) return nspStack[i + 1]; } return null; } public int getDepth() { return depth; } public String getPositionDescription() { StringBuffer buf = new StringBuffer( type < TYPES.length ? TYPES[type] : "unknown"); buf.append(' '); if (type == START_TAG || type == END_TAG) { if (degenerated) buf.append("(empty) "); buf.append('<'); if (type == END_TAG) buf.append('/'); if (prefix != null) buf.append("{" + namespace + "}" + prefix + ":"); buf.append(name); int cnt = attributeCount << 2; for (int i = 0; i < cnt; i += 4) { buf.append(' '); if (attributes[i + 1] != null) buf.append( "{" + attributes[i] + "}" + attributes[i + 1] + ":"); buf.append( attributes[i + 2] + "='" + attributes[i + 3] + "'"); } buf.append('>'); } else if (type == IGNORABLE_WHITESPACE); else if (type != TEXT) buf.append(getText()); else if (isWhitespace) buf.append("(whitespace)"); else { String text = getText(); if (text.length() > 16) text = text.substring(0, 16) + "..."; buf.append(text); } return buf.toString(); } public int getLineNumber() { return -1; } public int getColumnNumber() { return -1; } public boolean isWhitespace() throws XmlPullParserException { if (type != TEXT && type != IGNORABLE_WHITESPACE && type != CDSECT) exception(ILLEGAL_TYPE); return isWhitespace; } public String getText() { return text; } public char[] getTextCharacters(int[] poslen) { if (type >= TEXT) { poslen[0] = 0; poslen[1] = text.length(); char[] buf = new char[text.length()]; text.getChars(0, text.length(), buf, 0); return buf; } poslen[0] = -1; poslen[1] = -1; return null; } public String getNamespace() { return namespace; } public String getName() { return name; } public String getPrefix() { return prefix; } public boolean isEmptyElementTag() throws XmlPullParserException { if (type != START_TAG) exception(ILLEGAL_TYPE); return degenerated; } public int getAttributeCount() { return attributeCount; } public String getAttributeType(int index) { return "CDATA"; } public boolean isAttributeDefault(int index) { return false; } public String getAttributeNamespace(int index) { if (index >= attributeCount) throw new IndexOutOfBoundsException(); return attributes[index << 2]; } public String getAttributeName(int index) { if (index >= attributeCount) throw new IndexOutOfBoundsException(); return attributes[(index << 2) + 2]; } public String getAttributePrefix(int index) { if (index >= attributeCount) throw new IndexOutOfBoundsException(); return attributes[(index << 2) + 1]; } public String getAttributeValue(int index) { if (index >= attributeCount) throw new IndexOutOfBoundsException(); return attributes[(index << 2) + 3]; } public String getAttributeValue( String namespace, String name) { for (int i = (attributeCount << 2) - 4; i >= 0; i -= 4) { if (attributes[i + 2].equals(name) && (namespace == null || attributes[i].equals(namespace))) return attributes[i + 3]; } return null; } public int getEventType() throws XmlPullParserException { return type; } // TODO: Reuse resolveWapExtension here? Raw Wap extensions would still be accessible // via nextToken(); ....? public int next() throws XmlPullParserException, IOException { isWhitespace = true; int minType = 9999; while (true) { String save = text; nextImpl(); if (type < minType) minType = type; if (minType > CDSECT) continue; // no "real" event so far if (minType >= TEXT) { // text, see if accumulate if (save != null) text = text == null ? save : save + text; switch(peekId()) { case Wbxml.ENTITY: case Wbxml.STR_I: case Wbxml.STR_T: case Wbxml.LITERAL: case Wbxml.LITERAL_C: case Wbxml.LITERAL_A: case Wbxml.LITERAL_AC: continue; } } break; } type = minType; if (type > TEXT) type = TEXT; return type; } public int nextToken() throws XmlPullParserException, IOException { isWhitespace = true; nextImpl(); return type; } public int nextTag() throws XmlPullParserException, IOException { next(); if (type == TEXT && isWhitespace) next(); if (type != END_TAG && type != START_TAG) exception("unexpected type"); return type; } public String nextText() throws XmlPullParserException, IOException { if (type != START_TAG) exception("precondition: START_TAG"); next(); String result; if (type == TEXT) { result = getText(); next(); } else result = ""; if (type != END_TAG) exception("END_TAG expected"); return result; } public void require(int type, String namespace, String name) throws XmlPullParserException, IOException { if (type != this.type || (namespace != null && !namespace.equals(getNamespace())) || (name != null && !name.equals(getName()))) exception( "expected: " + (type == WAP_EXTENSION ? "WAP Ext." : (TYPES[type] + " {" + namespace + "}" + name))); } public void setInput(Reader reader) throws XmlPullParserException { exception("InputStream required"); } public void setInput(InputStream in, String enc) throws XmlPullParserException { this.in = in; try { version = readByte(); publicIdentifierId = readInt(); if (publicIdentifierId == 0) readInt(); int charset = readInt(); // skip charset if (null == enc){ switch (charset){ case 4: encoding = "ISO-8859-1"; break; case 106: encoding = "UTF-8"; break; // add more if you need them // http://www.iana.org/assignments/character-sets // case MIBenum: encoding = Name break; default: throw new UnsupportedEncodingException(""+charset); } }else{ encoding = enc; } int strTabSize = readInt(); stringTable = new byte[strTabSize]; int ok = 0; while(ok < strTabSize){ int cnt = in.read(stringTable, ok, strTabSize - ok); if(cnt <= 0) break; ok += cnt; } selectPage(0, true); selectPage(0, false); } catch (IOException e) { exception("Illegal input format"); } } public void setFeature(String feature, boolean value) throws XmlPullParserException { if (XmlPullParser.FEATURE_PROCESS_NAMESPACES.equals(feature)) processNsp = value; else exception("unsupported feature: " + feature); } public void setProperty(String property, Object value) throws XmlPullParserException { throw new XmlPullParserException("unsupported property: " + property); } // ---------------------- private / internal methods private final boolean adjustNsp() throws XmlPullParserException { boolean any = false; for (int i = 0; i < attributeCount << 2; i += 4) { // * 4 - 4; i >= 0; i -= 4) { String attrName = attributes[i + 2]; int cut = attrName.indexOf(':'); String prefix; if (cut != -1) { prefix = attrName.substring(0, cut); attrName = attrName.substring(cut + 1); } else if (attrName.equals("xmlns")) { prefix = attrName; attrName = null; } else continue; if (!prefix.equals("xmlns")) { any = true; } else { int j = (nspCounts[depth]++) << 1; nspStack = ensureCapacity(nspStack, j + 2); nspStack[j] = attrName; nspStack[j + 1] = attributes[i + 3]; if (attrName != null && attributes[i + 3].equals("")) exception("illegal empty namespace"); // prefixMap = new PrefixMap (prefixMap, attrName, attr.getValue ()); //System.out.println (prefixMap); System.arraycopy( attributes, i + 4, attributes, i, ((--attributeCount) << 2) - i); i -= 4; } } if (any) { for (int i = (attributeCount << 2) - 4; i >= 0; i -= 4) { String attrName = attributes[i + 2]; int cut = attrName.indexOf(':'); if (cut == 0) throw new RuntimeException( "illegal attribute name: " + attrName + " at " + this); else if (cut != -1) { String attrPrefix = attrName.substring(0, cut); attrName = attrName.substring(cut + 1); String attrNs = getNamespace(attrPrefix); if (attrNs == null) throw new RuntimeException( "Undefined Prefix: " + attrPrefix + " in " + this); attributes[i] = attrNs; attributes[i + 1] = attrPrefix; attributes[i + 2] = attrName; for (int j = (attributeCount << 2) - 4; j > i; j -= 4) if (attrName.equals(attributes[j + 2]) && attrNs.equals(attributes[j])) exception( "Duplicate Attribute: {" + attrNs + "}" + attrName); } } } int cut = name.indexOf(':'); if (cut == 0) exception("illegal tag name: " + name); else if (cut != -1) { prefix = name.substring(0, cut); name = name.substring(cut + 1); } this.namespace = getNamespace(prefix); if (this.namespace == null) { if (prefix != null) exception("undefined prefix: " + prefix); this.namespace = NO_NAMESPACE; } return any; } private final void setTable(int page, int type, String[] table) { if(stringTable != null){ throw new RuntimeException("setXxxTable must be called before setInput!"); } while(tables.size() < 3*page +3){ tables.addElement(null); } tables.setElementAt(table, page*3+type); } private final void exception(String desc) throws XmlPullParserException { throw new XmlPullParserException(desc, this, null); } private void selectPage(int nr, boolean tags) throws XmlPullParserException{ if(tables.size() == 0 && nr == 0) return; if(nr*3 > tables.size()) exception("Code Page "+nr+" undefined!"); if(tags) tagTable = (String[]) tables.elementAt(nr * 3 + TAG_TABLE); else { attrStartTable = (String[]) tables.elementAt(nr * 3 + ATTR_START_TABLE); attrValueTable = (String[]) tables.elementAt(nr * 3 + ATTR_VALUE_TABLE); } } private final void nextImpl() throws IOException, XmlPullParserException { String s; if (type == END_TAG) { depth--; } if (degenerated) { type = XmlPullParser.END_TAG; degenerated = false; return; } text = null; prefix = null; name = null; int id = peekId (); while(id == Wbxml.SWITCH_PAGE){ nextId = -2; selectPage(readByte(), true); id = peekId(); } nextId = -2; switch (id) { case -1 : type = XmlPullParser.END_DOCUMENT; break; case Wbxml.END : { int sp = (depth - 1) << 2; type = END_TAG; namespace = elementStack[sp]; prefix = elementStack[sp + 1]; name = elementStack[sp + 2]; } break; case Wbxml.ENTITY : { type = ENTITY_REF; char c = (char) readInt(); text = "" + c; name = "#" + ((int) c); } break; case Wbxml.STR_I : type = TEXT; text = readStrI(); break; case Wbxml.EXT_I_0 : case Wbxml.EXT_I_1 : case Wbxml.EXT_I_2 : case Wbxml.EXT_T_0 : case Wbxml.EXT_T_1 : case Wbxml.EXT_T_2 : case Wbxml.EXT_0 : case Wbxml.EXT_1 : case Wbxml.EXT_2 : case Wbxml.OPAQUE : type = WAP_EXTENSION; wapCode = id; wapExtensionData = parseWapExtension(id); break; case Wbxml.PI : throw new RuntimeException("PI curr. not supp."); // readPI; // break; case Wbxml.STR_T : { type = TEXT; text = readStrT(); } break; default : parseElement(id); } // } // while (next == null); // return next; } /** Overwrite this method to intercept all wap events */ public Object parseWapExtension(int id) throws IOException, XmlPullParserException { switch (id) { case Wbxml.EXT_I_0 : case Wbxml.EXT_I_1 : case Wbxml.EXT_I_2 : return readStrI(); case Wbxml.EXT_T_0 : case Wbxml.EXT_T_1 : case Wbxml.EXT_T_2 : return new Integer(readInt()); case Wbxml.EXT_0 : case Wbxml.EXT_1 : case Wbxml.EXT_2 : return null; case Wbxml.OPAQUE : { int count = readInt(); byte[] buf = new byte[count]; while(count > 0){ count -= in.read(buf, buf.length-count, count); } return buf; } // case OPAQUE default: exception("illegal id: "+id); return null; // dead code } // SWITCH } public void readAttr() throws IOException, XmlPullParserException { int id = readByte(); int i = 0; while (id != 1) { while(id == Wbxml.SWITCH_PAGE){ selectPage(readByte(), false); id = readByte(); } String name = resolveId(attrStartTable, id); StringBuffer value; int cut = name.indexOf('='); if (cut == -1) value = new StringBuffer(); else { value = new StringBuffer(name.substring(cut + 1)); name = name.substring(0, cut); } id = readByte(); while (id > 128 || id == Wbxml.SWITCH_PAGE || id == Wbxml.ENTITY || id == Wbxml.STR_I || id == Wbxml.STR_T || (id >= Wbxml.EXT_I_0 && id <= Wbxml.EXT_I_2) || (id >= Wbxml.EXT_T_0 && id <= Wbxml.EXT_T_2)) { switch (id) { case Wbxml.SWITCH_PAGE : selectPage(readByte(), false); break; case Wbxml.ENTITY : value.append((char) readInt()); break; case Wbxml.STR_I : value.append(readStrI()); break; case Wbxml.EXT_I_0 : case Wbxml.EXT_I_1 : case Wbxml.EXT_I_2 : case Wbxml.EXT_T_0 : case Wbxml.EXT_T_1 : case Wbxml.EXT_T_2 : case Wbxml.EXT_0 : case Wbxml.EXT_1 : case Wbxml.EXT_2 : case Wbxml.OPAQUE : value.append(resolveWapExtension(id, parseWapExtension(id))); break; case Wbxml.STR_T : value.append(readStrT()); break; default : value.append( resolveId(attrValueTable, id)); } id = readByte(); } attributes = ensureCapacity(attributes, i + 4); attributes[i++] = ""; attributes[i++] = null; attributes[i++] = name; attributes[i++] = value.toString(); attributeCount++; } } private int peekId () throws IOException { if (nextId == -2) { nextId = in.read (); } return nextId; } /** overwrite for own WAP extension handling in attributes and high level parsing * (above nextToken() level) */ protected String resolveWapExtension(int id, Object data){ if(data instanceof byte[]){ StringBuffer sb = new StringBuffer(); byte[] b = (byte[]) data; for (int i = 0; i < b.length; i++) { sb.append(HEX_DIGITS.charAt((b[i] >> 4) & 0x0f)); sb.append(HEX_DIGITS.charAt(b[i] & 0x0f)); } return sb.toString(); } return "$("+data+")"; } String resolveId(String[] tab, int id) throws IOException { int idx = (id & 0x07f) - 5; if (idx == -1){ wapCode = -1; return readStrT(); } if (idx < 0 || tab == null || idx >= tab.length || tab[idx] == null) throw new IOException("id " + id + " undef."); wapCode = idx+5; return tab[idx]; } void parseElement(int id) throws IOException, XmlPullParserException { type = START_TAG; name = resolveId(tagTable, id & 0x03f); attributeCount = 0; if ((id & 128) != 0) { readAttr(); } degenerated = (id & 64) == 0; int sp = depth++ << 2; // transfer to element stack elementStack = ensureCapacity(elementStack, sp + 4); elementStack[sp + 3] = name; if (depth >= nspCounts.length) { int[] bigger = new int[depth + 4]; System.arraycopy(nspCounts, 0, bigger, 0, nspCounts.length); nspCounts = bigger; } nspCounts[depth] = nspCounts[depth - 1]; for (int i = attributeCount - 1; i > 0; i--) { for (int j = 0; j < i; j++) { if (getAttributeName(i) .equals(getAttributeName(j))) exception( "Duplicate Attribute: " + getAttributeName(i)); } } if (processNsp) adjustNsp(); else namespace = ""; elementStack[sp] = namespace; elementStack[sp + 1] = prefix; elementStack[sp + 2] = name; } private final String[] ensureCapacity( String[] arr, int required) { if (arr.length >= required) return arr; String[] bigger = new String[required + 16]; System.arraycopy(arr, 0, bigger, 0, arr.length); return bigger; } int readByte() throws IOException { int i = in.read(); if (i == -1) throw new IOException("Unexpected EOF"); return i; } int readInt() throws IOException { int result = 0; int i; do { i = readByte(); result = (result << 7) | (i & 0x7f); } while ((i & 0x80) != 0); return result; } String readStrI() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); boolean wsp = true; while (true){ int i = in.read(); if (i == 0){ break; } if (i == -1){ throw new IOException(UNEXPECTED_EOF); } if (i > 32){ wsp = false; } buf.write(i); } isWhitespace = wsp; String result = new String(buf.toByteArray(), encoding); buf.close(); return result; } String readStrT() throws IOException { int pos = readInt(); // As the main reason of stringTable is compression we build a cache of Strings // stringTable is supposed to help create Strings from parts which means some cache hit rate // This will help to minimize the Strings created when invoking readStrT() repeatedly if (cacheStringTable == null){ //Lazy init if device is not using StringTable but inline 0x03 strings cacheStringTable = new Hashtable(); } String forReturn = (String) cacheStringTable.get(new Integer(pos)); if (forReturn == null){ int end = pos; while(end < stringTable.length && stringTable[end] != '\0'){ end++; } forReturn = new String(stringTable, pos, end-pos, encoding); cacheStringTable.put(new Integer(pos), forReturn); } return forReturn; } /** * Sets the tag table for a given page. * The first string in the array defines tag 5, the second tag 6 etc. */ public void setTagTable(int page, String[] table) { setTable(page, TAG_TABLE, table); // this.tagTable = tagTable; // if (page != 0) // throw new RuntimeException("code pages curr. not supp."); } /** Sets the attribute start Table for a given page. * The first string in the array defines attribute * 5, the second attribute 6 etc. Please use the * character '=' (without quote!) as delimiter * between the attribute name and the (start of the) value */ public void setAttrStartTable( int page, String[] table) { setTable(page, ATTR_START_TABLE, table); } /** Sets the attribute value Table for a given page. * The first string in the array defines attribute value 0x85, * the second attribute value 0x86 etc. */ public void setAttrValueTable( int page, String[] table) { setTable(page, ATTR_VALUE_TABLE, table); } /** Returns the token ID for start tags or the event type for wap proprietary events * such as OPAQUE. */ public int getWapCode(){ return wapCode; } public Object getWapExtensionData(){ return wapExtensionData; } } ================================================ FILE: src/main/java/org/kxml2/wap/WbxmlSerializer.java ================================================ /* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ //Contributors: Jonathan Cox, Bogdan Onoiu, Jerry Tian package org.kxml2.wap; import java.io.*; import java.util.*; import org.xmlpull.v1.*; // TODO: make some of the "direct" WBXML token writing methods public?? /** * A class for writing WBXML. Does not support namespaces yet. */ public class WbxmlSerializer implements XmlSerializer { Hashtable stringTable = new Hashtable(); OutputStream out; ByteArrayOutputStream buf = new ByteArrayOutputStream(); ByteArrayOutputStream stringTableBuf = new ByteArrayOutputStream(); String pending; int depth; String name; String namespace; Vector attributes = new Vector(); Hashtable attrStartTable = new Hashtable(); Hashtable attrValueTable = new Hashtable(); Hashtable tagTable = new Hashtable(); private int attrPage; private int tagPage; private String encoding; private boolean headerSent = false; /** * Write an attribute. * Calls to attribute() MUST follow a call to startTag() immediately. * If there is no prefix defined for the given namespace, * a prefix will be defined automatically. */ public XmlSerializer attribute(String namespace, String name, String value) { attributes.addElement(name); attributes.addElement(value); return this; } public void cdsect (String cdsect) throws IOException{ text (cdsect); } /** * Add comment. Ignore for WBXML. */ public void comment (String comment) { // silently ignore comment } /** * Docdecl isn't supported for WBXML. */ public void docdecl (String docdecl) { throw new RuntimeException ("Cannot write docdecl for WBXML"); } /** * EntityReference not supported for WBXML. */ public void entityRef (String er) { throw new RuntimeException ("EntityReference not supported for WBXML"); } /** * Return current tag depth. */ public int getDepth() { return depth; } /** * Return the current value of the feature with given name. */ public boolean getFeature (String name) { return false; } /** * Returns the namespace URI of the current element as set by startTag(). * Namespaces is not yet implemented. */ public String getNamespace() { // Namespaces is not yet implemented. So only null can be setted return null; } /** * Returns the name of the current element as set by startTag(). * It can only be null before first call to startTag() or when last endTag() * is called to close first startTag(). */ public String getName() { return pending; } /** * Prefix for namespace not supported for WBXML. Not yet implemented. */ public String getPrefix(String nsp, boolean create) { throw new RuntimeException ("NYI"); } /** * Look up the value of a property. * @param name The name of property. Name is any fully-qualified URI. * @return The value of named property. */ public Object getProperty (String name) { return null; } public void ignorableWhitespace (String sp) { } /** * Finish writing. * All unclosed start tags will be closed and output will be flushed. * After calling this method no more output can be serialized until * next call to setOutput(). */ public void endDocument() throws IOException { flush(); } /** * Write all pending output to the stream. * After first call string table willn't be used and you can't add tag * which is not in tag table. */ public void flush() throws IOException { checkPending(false); if (!headerSent) { writeInt(out, stringTableBuf.size()); out.write(stringTableBuf.toByteArray()); headerSent = true; } out.write(buf.toByteArray()); buf.reset(); } public void checkPending(boolean degenerated) throws IOException { if (pending == null) return; int len = attributes.size(); int[] idx = (int[]) tagTable.get(pending); // if no entry in known table, then add as literal if (idx == null) { buf.write(len == 0 ? (degenerated ? Wbxml.LITERAL : Wbxml.LITERAL_C) : (degenerated ? Wbxml.LITERAL_A : Wbxml.LITERAL_AC)); writeStrT(pending, false); } else { if(idx[0] != tagPage){ tagPage=idx[0]; buf.write(Wbxml.SWITCH_PAGE); buf.write(tagPage); } buf.write(len == 0 ? (degenerated ? idx[1] : idx[1] | 64) : (degenerated ? idx[1] | 128 : idx[1] | 192)); } for (int i = 0; i < len;) { idx = (int[]) attrStartTable.get(attributes.elementAt(i)); if (idx == null) { buf.write(Wbxml.LITERAL); writeStrT((String) attributes.elementAt(i), false); } else { if(idx[0] != attrPage){ attrPage = idx[0]; buf.write(0); buf.write(attrPage); } buf.write(idx[1]); } idx = (int[]) attrValueTable.get(attributes.elementAt(++i)); if (idx == null) { writeStr((String) attributes.elementAt(i)); } else { if(idx[0] != attrPage){ attrPage = idx[0]; buf.write(0); buf.write(attrPage); } buf.write(idx[1]); } ++i; } if (len > 0) buf.write(Wbxml.END); pending = null; attributes.removeAllElements(); } /** * Not Yet Implemented. */ public void processingInstruction(String pi) { throw new RuntimeException ("PI NYI"); } /** * Set feature identified by name. There are no supported functions. */ public void setFeature(String name, boolean value) { throw new IllegalArgumentException ("unknown feature "+name); } /** * Set the output to the given writer. Wbxml requires an OutputStream. */ public void setOutput (Writer writer) { throw new RuntimeException ("Wbxml requires an OutputStream!"); } /** * Set to use binary output stream with given encoding. */ public void setOutput (OutputStream out, String encoding) throws IOException { this.encoding = encoding == null ? "UTF-8" : encoding; this.out = out; buf = new ByteArrayOutputStream(); stringTableBuf = new ByteArrayOutputStream(); headerSent = false; // ok, write header } /** * Binds the given prefix to the given namespace. Not yet implemented. */ public void setPrefix(String prefix, String nsp) { throw new RuntimeException("NYI"); } /** * Set the value of a property. There are no supported properties. */ public void setProperty(String property, Object value) { throw new IllegalArgumentException ("unknown property "+property); } /** * Write version and encoding information. * This method can only be called just after setOutput. * @param encoding Document encoding. Default is UTF-8. * @param standalone Not used in WBXML. */ public void startDocument(String encoding, Boolean standalone) throws IOException { out.write(0x03); // version 1.3 // http://www.openmobilealliance.org/tech/omna/omna-wbxml-public-docid.htm out.write(0x01); // unknown or missing public identifier // default encoding is UTF-8 if(encoding != null){ this.encoding = encoding; } if (this.encoding.toUpperCase().equals("UTF-8")){ out.write(106); }else if (this.encoding.toUpperCase().equals("ISO-8859-1")){ out.write(0x04); }else{ throw new UnsupportedEncodingException(encoding); } } public XmlSerializer startTag(String namespace, String name) throws IOException { if (namespace != null && !"".equals(namespace)) throw new RuntimeException ("NSP NYI"); //current = new State(current, prefixMap, name); checkPending(false); pending = name; depth++; return this; } public XmlSerializer text(char[] chars, int start, int len) throws IOException { checkPending(false); writeStr(new String(chars, start, len)); return this; } public XmlSerializer text(String text) throws IOException { checkPending(false); writeStr(text); return this; } /** * Used in text() and attribute() to write text. */ private void writeStr(String text) throws IOException{ int p0 = 0; int lastCut = 0; int len = text.length(); if (headerSent) { writeStrI(buf, text); return; } while(p0 < len){ while(p0 < len && text.charAt(p0) < 'A' ){ // skip interpunctation p0++; } int p1 = p0; while (p1 < len && text.charAt(p1) >= 'A'){ p1++; } if (p1 - p0 > 10) { if (p0 > lastCut && text.charAt(p0-1) == ' ' && stringTable.get(text.substring(p0, p1)) == null){ buf.write(Wbxml.STR_T); writeStrT(text.substring(lastCut, p1), false); } else { if(p0 > lastCut && text.charAt(p0-1) == ' '){ p0--; } if(p0 > lastCut){ buf.write(Wbxml.STR_T); writeStrT(text.substring(lastCut, p0), false); } buf.write(Wbxml.STR_T); writeStrT(text.substring(p0, p1), true); } lastCut = p1; } p0 = p1; } if(lastCut < len){ buf.write(Wbxml.STR_T); writeStrT(text.substring(lastCut, len), false); } } public XmlSerializer endTag(String namespace, String name) throws IOException { // current = current.prev; if (pending != null) { checkPending(true); } else { buf.write(Wbxml.END); } depth--; return this; } /** * @throws IOException */ public void writeWapExtension(int type, Object data) throws IOException { checkPending(false); buf.write(type); switch(type){ case Wbxml.EXT_0: case Wbxml.EXT_1: case Wbxml.EXT_2: break; case Wbxml.OPAQUE: byte[] bytes = (byte[]) data; writeInt(buf, bytes.length); buf.write(bytes); break; case Wbxml.EXT_I_0: case Wbxml.EXT_I_1: case Wbxml.EXT_I_2: writeStrI(buf, (String) data); break; case Wbxml.EXT_T_0: case Wbxml.EXT_T_1: case Wbxml.EXT_T_2: writeStrT((String) data, false); break; default: throw new IllegalArgumentException(); } } // ------------- internal methods -------------------------- static void writeInt(OutputStream out, int i) throws IOException { byte[] buf = new byte[5]; int idx = 0; do { buf[idx++] = (byte) (i & 0x7f); i = i >> 7; } while (i != 0); while (idx > 1) { out.write(buf[--idx] | 0x80); } out.write(buf[0]); } void writeStrI(OutputStream out, String s) throws IOException { byte[] data = s.getBytes(encoding); out.write(data); out.write(0); } private final void writeStrT(String s, boolean mayPrependSpace) throws IOException { Integer idx = (Integer) stringTable.get(s); writeInt(buf, idx == null ? addToStringTable(s, mayPrependSpace) : idx.intValue()); } /** * Add string to string table. Not permitted after string table has been flushed. * * @param s string to be added to the string table * @param mayPrependSpace is set, a space is prepended to the string to archieve better compression results * @return offset of s in the string table */ public int addToStringTable(String s, boolean mayPrependSpace) throws IOException { if (headerSent) { throw new IOException("stringtable sent"); } int i = stringTableBuf.size(); int offset = i; if(s.charAt(0) >= '0' && mayPrependSpace){ s = ' ' + s; offset++; } stringTable.put(s, new Integer(i)); if(s.charAt(0) == ' '){ stringTable.put(s.substring(1), new Integer(i+1)); } int j = s.lastIndexOf(' '); if(j > 1){ String t = s.substring(j); int k = t.getBytes("utf-8").length; stringTable.put(t, new Integer(i+k)); stringTable.put(s.substring(j+1), new Integer(i+k+1)); } writeStrI(stringTableBuf, s); stringTableBuf.flush(); return offset; } /** * Sets the tag table for a given page. * The first string in the array defines tag 5, the second tag 6 etc. */ public void setTagTable(int page, String[] tagTable) { // TODO: clear entries in tagTable? for (int i = 0; i < tagTable.length; i++) { if (tagTable[i] != null) { Object idx = new int[]{page, i+5}; this.tagTable.put(tagTable[i], idx); } } } /** * Sets the attribute start Table for a given page. * The first string in the array defines attribute * 5, the second attribute 6 etc. * Please use the * character '=' (without quote!) as delimiter * between the attribute name and the (start of the) value */ public void setAttrStartTable(int page, String[] attrStartTable) { for (int i = 0; i < attrStartTable.length; i++) { if (attrStartTable[i] != null) { Object idx = new int[] {page, i + 5}; this.attrStartTable.put(attrStartTable[i], idx); } } } /** * Sets the attribute value Table for a given page. * The first string in the array defines attribute value 0x85, * the second attribute value 0x86 etc. * Must be called BEFORE use attribute(), flush() etc. */ public void setAttrValueTable(int page, String[] attrValueTable) { // clear entries in this.table! for (int i = 0; i < attrValueTable.length; i++) { if (attrValueTable[i] != null) { Object idx = new int[]{page, i + 0x085}; this.attrValueTable.put(attrValueTable[i], idx); } } } } ================================================ FILE: src/main/java/org/kxml2/wap/syncml/SyncML.java ================================================ package org.kxml2.wap.syncml; import org.kxml2.wap.*; public abstract class SyncML { // SyncML-Common (-//SYNCML//DTD SyncML 1.2//EN and -//SYNCML//DTD MetInf 1.2//EN) support public static WbxmlParser createParser() { WbxmlParser p = new WbxmlParser(); p.setTagTable(0, TAG_TABLE_0); p.setTagTable(1, TAG_TABLE_1); return p; } public static WbxmlSerializer createSerializer() { WbxmlSerializer s = new WbxmlSerializer(); s.setTagTable(0, TAG_TABLE_0); s.setTagTable(1, TAG_TABLE_1); return s; } // SyncML-Common + DMDDF (-//OMA//DTD-DM-DDF 1.2//EN) support public static WbxmlParser createDMParser() { WbxmlParser p = createParser(); p.setTagTable(2, TAG_TABLE_2_DM); return p; } public static WbxmlSerializer createDMSerializer() { WbxmlSerializer s = createSerializer(); s.setTagTable(2, TAG_TABLE_2_DM); return s; } // Tables public static final String [] TAG_TABLE_0 = { // -//SYNCML//DTD SyncML 1.2//EN "Add", // 0x05 "Alert", // 0x06 "Archive", // 0x07 "Atomic", // 0x08 "Chal", // 0x09 "Cmd", // 0x0a "CmdID", // 0x0b "CmdRef", // 0x0c "Copy", // 0x0d "Cred", // 0x0e "Data", // 0x0f "Delete", // 0x10 "Exec", // 0x11 "Final", // 0x12 "Get", // 0x13 "Item", // 0x14 "Lang", // 0x15 "LocName", // 0x16 "LocURI", // 0x17 "Map", // 0x18 "MapItem", // 0x19 "Meta", // 0x1a "MsgID", // 0x1b "MsgRef", // 0x1c "NoResp", // 0x1d "NoResults", // 0x1e "Put", // 0x1f "Replace", // 0x20 "RespURI", // 0x21 "Results", // 0x22 "Search", // 0x23 "Sequence", // 0x24 "SessionID", // 0x25 "SftDel", // 0x26 "Source", // 0x27 "SourceRef", // 0x28 "Status", // 0x29 "Sync", // 0x2a "SyncBody", // 0x2b "SyncHdr", // 0x2c "SyncML", // 0x2d "Target", // 0x2e "TargetRef", // 0x2f "Reserved for future use", // 0x30 "VerDTD", // 0x31 "VerProto", // 0x32 "NumberOfChanged",// 0x33 "MoreData", // 0x34 "Field", // 0x35 "Filter", // 0x36 "Record", // 0x37 "FilterType", // 0x38 "SourceParent", // 0x39 "TargetParent", // 0x3a "Move", // 0x3b "Correlator" // 0x3c }; public static final String [] TAG_TABLE_1 = { // -//SYNCML//DTD MetInf 1.2//EN "Anchor", // 0x05 "EMI", // 0x06 "Format", // 0x07 "FreeID", // 0x08 "FreeMem", // 0x09 "Last", // 0x0a "Mark", // 0x0b "MaxMsgSize", // 0x0c "Mem", // 0x0d "MetInf", // 0x0e "Next", // 0x0f "NextNonce", // 0x10 "SharedMem", // 0x11 "Size", // 0x12 "Type", // 0x13 "Version", // 0x14 "MaxObjSize", // 0x15 "FieldLevel" // 0x16 }; public static final String [] TAG_TABLE_2_DM = { // -//OMA//DTD-DM-DDF 1.2//EN "AccessType", // 0x05 "ACL", // 0x06 "Add", // 0x07 "b64", // 0x08 "bin", // 0x09 "bool", // 0x0a "chr", // 0x0b "CaseSense", // 0x0c "CIS", // 0x0d "Copy", // 0x0e "CS", // 0x0f "date", // 0x10 "DDFName", // 0x11 "DefaultValue", // 0x12 "Delete", // 0x13 "Description", // 0x14 "DDFFormat", // 0x15 "DFProperties", // 0x16 "DFTitle", // 0x17 "DFType", // 0x18 "Dynamic", // 0x19 "Exec", // 0x1a "float", // 0x1b "Format", // 0x1c "Get", // 0x1d "int", // 0x1e "Man", // 0x1f "MgmtTree", // 0x20 "MIME", // 0x21 "Mod", // 0x22 "Name", // 0x23 "Node", // 0x24 "node", // 0x25 "NodeName", // 0x26 "null", // 0x27 "Occurence", // 0x28 "One", // 0x29 "OneOrMore", // 0x2a "OneOrN", // 0x2b "Path", // 0x2c "Permanent", // 0x2d "Replace", // 0x2e "RTProperties", // 0x2f "Scope", // 0x30 "Size", // 0x31 "time", // 0x32 "Title", // 0x33 "TStamp", // 0x34 "Type", // 0x35 "Value", // 0x36 "VerDTD", // 0x37 "VerNo", // 0x38 "xml", // 0x39 "ZeroOrMore", // 0x3a "ZeroOrN", // 0x3b "ZeroOrOne" // 0x3c }; } ================================================ FILE: src/main/java/org/kxml2/wap/wml/Wml.java ================================================ package org.kxml2.wap.wml; import org.kxml2.wap.*; /** This class contains the wml coding tables for elements * and attributes needed by the WmlParser. */ public abstract class Wml { /** Creates a WbxmlParser with the WML code pages set */ public static WbxmlParser createParser() { WbxmlParser p = new WbxmlParser(); p.setTagTable(0, TAG_TABLE); p.setAttrStartTable(0, ATTR_START_TABLE); p.setAttrValueTable(0, ATTR_VALUE_TABLE); return p; } public static WbxmlSerializer createSerializer() { WbxmlSerializer s = new WbxmlSerializer(); s.setTagTable(0, TAG_TABLE); s.setAttrStartTable(0, ATTR_START_TABLE); s.setAttrValueTable(0, ATTR_VALUE_TABLE); return s; } public static final String [] TAG_TABLE = { null, // 05 null, // 06 null, // 07 null, // 08 null, // 09 null, // 0A null, // 0B null, // 0C null, // 0D null, // 0E null, // 0F null, // 10 null, // 11 null, // 12 null, // 13 null, // 14 null, // 15 null, // 16 null, // 17 null, // 18 null, // 19 null, // 1A null, // 1B "a", // 1C "td", // 1D "tr", // 1E "table", // 1F "p", // 20 "postfield", // 21 "anchor", // 22 "access", // 23 "b", // 24 "big", // 25 "br", // 26 "card", // 27 "do", // 28 "em", // 29 "fieldset", // 2A "go", // 2B "head", // 2C "i", // 2D "img", // 2E "input", // 2F "meta", // 30 "noop", // 31 "prev", // 32 "onevent", // 33 "optgroup", // 34 "option", // 35 "refresh", // 36 "select", // 37 "small", // 38 "strong", // 39 null, // 3A "template", // 3B "timer", // 3C "u", // 3D "setvar", // 3E "wml", // 3F }; public static final String [] ATTR_START_TABLE = { "accept-charset", // 05 "align=bottom", // 06 "align=center", // 07 "align=left", // 08 "align=middle", // 09 "align=right", // 0A "align=top", // 0B "alt", // 0C "content", // 0D null, // 0E "domain", // 0F "emptyok=false", // 10 "emptyok=true", // 11 "format", // 12 "height", // 13 "hspace", // 14 "ivalue", // 15 "iname", // 16 null, // 17 "label", // 18 "localsrc", // 19 "maxlength", // 1A "method=get", // 1B "method=post", // 1C "mode=nowrap", // 1D "mode=wrap", // 1E "multiple=false", // 1F "multiple=true", // 20 "name", // 21 "newcontext=false", // 22 "newcontext=true", // 23 "onpick", // 24 "onenterbackward", // 25 "onenterforward", // 26 "ontimer", // 27 "optimal=false", // 28 "optimal=true", // 29 "path", // 2A null, // 2B null, // 2C null, // 2D "scheme", // 2E "sendreferer=false", // 2F "sendreferer=true", // 30 "size", // 31 "src", // 32 "ordered=true", // 33 "ordered=false", // 34 "tabindex", // 35 "title", // 36 "type", // 37 "type=accept", // 38 "type=delete", // 39 "type=help", // 3A "type=password", // 3B "type=onpick", // 3C "type=onenterbackward", // 3D "type=onenterforward", // 3E "type=ontimer", // 3F null, // 40 null, // 41 null, // 42 null, // 43 null, // 44 "type=options", // 45 "type=prev", // 46 "type=reset", // 47 "type=text", // 48 "type=vnd.", // 49 "href", // 4A "href=http://", // 4B "href=https://", // 4C "value", // 4D "vspace", // 4E "width", // 4F "xml:lang", // 50 null, // 51 "align", // 52 "columns", // 53 "class", // 54 "id", // 55 "forua=false", // 56 "forua=true", // 57 "src=http://", // 58 "src=https://", // 59 "http-equiv", // 5A "http-equiv=Content-Type", // 5B "content=application/vnd.wap.wmlc;charset=", // 5C "http-equiv=Expires", // 5D null, // 5E null, // 5F }; public static final String [] ATTR_VALUE_TABLE = { ".com/", // 85 ".edu/", // 86 ".net/", // 87 ".org/", // 88 "accept", // 89 "bottom", // 8A "clear", // 8B "delete", // 8C "help", // 8D "http://", // 8E "http://www.", // 8F "https://", // 90 "https://www.", // 91 null, // 92 "middle", // 93 "nowrap", // 94 "onpick", // 95 "onenterbackward", // 96 "onenterforward", // 97 "ontimer", // 98 "options", // 99 "password", // 9A "reset", // 9B null, // 9C "text", // 9D "top", // 9E "unknown", // 9F "wrap", // A0 "www.", // A1 }; } ================================================ FILE: src/main/java/org/kxml2/wap/wv/WV.java ================================================ package org.kxml2.wap.wv; import java.io.IOException; import org.kxml2.wap.*; /* * WV.java * * Created on 25 September 2003, 10:40 */ /** * Wireless Village CSP 1.1 ("OMA-WV-CSP-V1_1-20021001-A.pdf") * Wireless Village CSP 1.2 ("OMA-IMPS-WV-CSP_WBXML-v1_2-20030221-C.PDF") * There are some bugs in the 1.2 spec but this is Ok. 1.2 is candidate * * @author Bogdan Onoiu */ public abstract class WV { public static WbxmlParser createParser () throws IOException { WbxmlParser parser = new WbxmlParser(); parser.setTagTable (0, WV.tagTablePage0); parser.setTagTable (1, WV.tagTablePage1); parser.setTagTable (2, WV.tagTablePage2); parser.setTagTable (3, WV.tagTablePage3); parser.setTagTable (4, WV.tagTablePage4); parser.setTagTable (5, WV.tagTablePage5); parser.setTagTable (6, WV.tagTablePage6); parser.setTagTable (7, WV.tagTablePage7); parser.setTagTable (8, WV.tagTablePage8); parser.setTagTable (9, WV.tagTablePage9); parser.setTagTable (10, WV.tagTablePageA); parser.setAttrStartTable (0, WV.attrStartTable); parser.setAttrValueTable (0, WV.attrValueTable); return parser; } public static final String [] tagTablePage0 = { /* Common ... continue on Page 0x09 */ "Acceptance", //0x00, 0x05 "AddList", //0x00, 0x06 "AddNickList", //0x00, 0x07 "SName", //0x00, 0x08 "WV-CSP-Message", //0x00, 0x09 "ClientID", //0x00, 0x0A "Code", //0x00, 0x0B "ContactList", //0x00, 0x0C "ContentData", //0x00, 0x0D "ContentEncoding",//0x00, 0x0E "ContentSize", //0x00, 0x0F "ContentType", //0x00, 0x10 "DateTime", //0x00, 0x11 "Description", //0x00, 0x12 "DetailedResult", //0x00, 0x13 "EntityList", //0x00, 0x14 "Group", //0x00, 0x15 "GroupID", //0x00, 0x16 "GroupList", //0x00, 0x17 "InUse", //0x00, 0x18 "Logo", //0x00, 0x19 "MessageCount", //0x00, 0x1A "MessageID", //0x00, 0x1B "MessageURI", //0x00, 0x1C "MSISDN", //0x00, 0x1D "Name", //0x00, 0x1E "NickList", //0x00, 0x1F "NickName", //0x00, 0x20 "Poll", //0x00, 0x21 "Presence", //0x00, 0x22 "PresenceSubList",//0x00, 0x23 "PresenceValue", //0x00, 0x24 "Property", //0x00, 0x25 "Qualifier", //0x00, 0x26 "Recipient", //0x00, 0x27 "RemoveList", //0x00, 0x28 "RemoveNickList", //0x00, 0x29 "Result", //0x00, 0x2A "ScreenName", //0x00, 0x2B "Sender", //0x00, 0x2C "Session", //0x00, 0x2D "SessionDescriptor",//0x00, 0x2E "SessionID", //0x00, 0x2F "SessionType", //0x00, 0x30 "Status", //0x00, 0x31 "Transaction", //0x00, 0x32 "TransactionContent",//0x00, 0x33 "TransactionDescriptor",//0x00, 0x34 "TransactionID", //0x00, 0x35 "TransactionMode",//0x00, 0x36 "URL", //0x00, 0x37 "URLList", //0x00, 0x38 "User", //0x00, 0x39 "UserID", //0x00, 0x3A "UserList", //0x00, 0x3B "Validity", //0x00, 0x3C "Value", //0x00, 0x3D }; public static final String [] tagTablePage1 = { /* Access ... continue on Page 0x0A */ "AllFunctions", // 0x01, 0x05 "AllFunctionsRequest", // 0x01, 0x06 "CancelInvite-Request", // 0x01, 0x07 "CancelInviteUser-Request", // 0x01, 0x08 "Capability", // 0x01, 0x09 "CapabilityList", // 0x01, 0x0A "CapabilityRequest", // 0x01, 0x0B "ClientCapability-Request", // 0x01, 0x0C "ClientCapability-Response",// 0x01, 0x0D "DigestBytes", // 0x01, 0x0E "DigestSchema", // 0x01, 0x0F "Disconnect", // 0x01, 0x10 "Functions", // 0x01, 0x11 "GetSPInfo-Request", // 0x01, 0x12 "GetSPInfo-Response", // 0x01, 0x13 "InviteID", // 0x01, 0x14 "InviteNote", // 0x01, 0x15 "Invite-Request", // 0x01, 0x16 "Invite-Response", // 0x01, 0x17 "InviteType", // 0x01, 0x18 "InviteUser-Request", // 0x01, 0x19 "InviteUser-Response", // 0x01, 0x1A "KeepAlive-Request", // 0x01, 0x1B "KeepAliveTime", // 0x01, 0x1C "Login-Request", // 0x01, 0x1D "Login-Response", // 0x01, 0x1E "Logout-Request", // 0x01, 0x1F "Nonce", // 0x01, 0x20 "Password", // 0x01, 0x21 "Polling-Request", // 0x01, 0x22 "ResponseNote", // 0x01, 0x23 "SearchElement", // 0x01, 0x24 "SearchFindings", // 0x01, 0x25 "SearchID", // 0x01, 0x26 "SearchIndex", // 0x01, 0x27 "SearchLimit", // 0x01, 0x28 "KeepAlive-Response", // 0x01, 0x29 "SearchPairList", // 0x01, 0x2A "Search-Request", // 0x01, 0x2B "Search-Response", // 0x01, 0x2C "SearchResult", // 0x01, 0x2D "Service-Request", // 0x01, 0x2E "Service-Response", // 0x01, 0x2F "SessionCookie", // 0x01, 0x30 "StopSearch-Request", // 0x01, 0x31 "TimeToLive", // 0x01, 0x32 "SearchString", // 0x01, 0x33 "CompletionFlag", // 0x01, 0x34 null, // 0x01, 0x35 "ReceiveList", // 0x01, 0x36 /* WV 1.2 */ "VerifyID-Request", // 0x01, 0x37 /* WV 1.2 */ "Extended-Request", // 0x01, 0x38 /* WV 1.2 */ "Extended-Response", // 0x01, 0x39 /* WV 1.2 */ "AgreedCapabilityList", // 0x01, 0x3A /* WV 1.2 */ "Extended-Data", // 0x01, 0x3B /* WV 1.2 */ "OtherServer", // 0x01, 0x3C /* WV 1.2 */ "PresenceAttributeNSName",//0x01, 0x3D /* WV 1.2 */ "SessionNSName", // 0x01, 0x3E /* WV 1.2 */ "TransactionNSName", // 0x01, 0x3F /* WV 1.2 */ }; public static final String [] tagTablePage2 = { /* Service ... continue on Page 0x08 */ "ADDGM", // 0x02, 0x05 "AttListFunc", // 0x02, 0x06 "BLENT", // 0x02, 0x07 "CAAUT", // 0x02, 0x08 "CAINV", // 0x02, 0x09 "CALI", // 0x02, 0x0A "CCLI", // 0x02, 0x0B "ContListFunc", // 0x02, 0x0C "CREAG", // 0x02, 0x0D "DALI", // 0x02, 0x0E "DCLI", // 0x02, 0x0F "DELGR", // 0x02, 0x10 "FundamentalFeat",//0x02, 0x11 "FWMSG", // 0x02, 0x12 "GALS", // 0x02, 0x13 "GCLI", // 0x02, 0x14 "GETGM", // 0x02, 0x15 "GETGP", // 0x02, 0x16 "GETLM", // 0x02, 0x17 "GETM", // 0x02, 0x18 "GETPR", // 0x02, 0x19 "GETSPI", // 0x02, 0x1A "GETWL", // 0x02, 0x1B "GLBLU", // 0x02, 0x1C "GRCHN", // 0x02, 0x1D "GroupAuthFunc",// 0x02, 0x1E "GroupFeat", // 0x02, 0x1F "GroupMgmtFunc",// 0x02, 0x20 "GroupUseFunc", // 0x02, 0x21 "IMAuthFunc", // 0x02, 0x22 "IMFeat", // 0x02, 0x23 "IMReceiveFunc",// 0x02, 0x24 "IMSendFunc", // 0x02, 0x25 "INVIT", // 0x02, 0x26 "InviteFunc", // 0x02, 0x27 "MBRAC", // 0x02, 0x28 "MCLS", // 0x02, 0x29 "MDELIV", // 0x02, 0x2A "NEWM", // 0x02, 0x2B "NOTIF", // 0x02, 0x2C "PresenceAuthFunc",//0x02, 0x2D "PresenceDeliverFunc",//0x02, 0x2E "PresenceFeat", // 0x02, 0x2F "REACT", // 0x02, 0x30 "REJCM", // 0x02, 0x31 "REJEC", // 0x02, 0x32 "RMVGM", // 0x02, 0x33 "SearchFunc", // 0x02, 0x34 "ServiceFunc", // 0x02, 0x35 "SETD", // 0x02, 0x36 "SETGP", // 0x02, 0x37 "SRCH", // 0x02, 0x38 "STSRC", // 0x02, 0x39 "SUBGCN", // 0x02, 0x3A "UPDPR", // 0x02, 0x3B "WVCSPFeat", // 0x02, 0x3C "MF", // 0x02, 0x3D /* WV 1.2 */ "MG", // 0x02, 0x3E /* WV 1.2 */ "MM" // 0x02, 0x3F /* WV 1.2 */ }; public static final String [] tagTablePage3 = { /* Client Capability */ "AcceptedCharset", // 0x03, 0x05 "AcceptedContentLength", // 0x03, 0x06 "AcceptedContentType", // 0x03, 0x07 "AcceptedTransferEncoding", // 0x03, 0x08 "AnyContent", // 0x03, 0x09 "DefaultLanguage", // 0x03, 0x0A "InitialDeliveryMethod", // 0x03, 0x0B "MultiTrans", // 0x03, 0x0C "ParserSize", // 0x03, 0x0D "ServerPollMin", // 0x03, 0x0E "SupportedBearer", // 0x03, 0x0F "SupportedCIRMethod", // 0x03, 0x10 "TCPAddress", // 0x03, 0x11 "TCPPort", // 0x03, 0x12 "UDPPort" // 0x03, 0x13 }; public static final String [] tagTablePage4 = { /* Presence Primitive */ "CancelAuth-Request", // 0x04, 0x05 "ContactListProperties", // 0x04, 0x06 "CreateAttributeList-Request", // 0x04, 0x07 "CreateList-Request", // 0x04, 0x08 "DefaultAttributeList", // 0x04, 0x09 "DefaultContactList", // 0x04, 0x0A "DefaultList", // 0x04, 0x0B "DeleteAttributeList-Request", // 0x04, 0x0C "DeleteList-Request", // 0x04, 0x0D "GetAttributeList-Request", // 0x04, 0x0E "GetAttributeList-Response", // 0x04, 0x0F "GetList-Request", // 0x04, 0x10 "GetList-Response", // 0x04, 0x11 "GetPresence-Request", // 0x04, 0x12 "GetPresence-Response", // 0x04, 0x13 "GetWatcherList-Request", // 0x04, 0x14 "GetWatcherList-Response", // 0x04, 0x15 "ListManage-Request", // 0x04, 0x16 "ListManage-Response", // 0x04, 0x17 "UnsubscribePresence-Request", // 0x04, 0x18 "PresenceAuth-Request", // 0x04, 0x19 "PresenceAuth-User", // 0x04, 0x1A "PresenceNotification-Request", // 0x04, 0x1B "UpdatePresence-Request", // 0x04, 0x1C "SubscribePresence-Request", // 0x04, 0x1D "Auto-Subscribe", // 0x04, 0x1E /* WV 1.2 */ "GetReactiveAuthStatus-Request",// 0x04, 0x1F /* WV 1.2 */ "GetReactiveAuthStatus-Response",// 0x04, 0x20 /* WV 1.2 */ }; public static final String [] tagTablePage5 = { /* Presence Attribute */ "Accuracy", // 0x05, 0x05 "Address", // 0x05, 0x06 "AddrPref", // 0x05, 0x07 "Alias", // 0x05, 0x08 "Altitude", // 0x05, 0x09 "Building", // 0x05, 0x0A "Caddr", // 0x05, 0x0B "City", // 0x05, 0x0C "ClientInfo", // 0x05, 0x0D "ClientProducer", // 0x05, 0x0E "ClientType", // 0x05, 0x0F "ClientVersion", // 0x05, 0x10 "CommC", // 0x05, 0x11 "CommCap", // 0x05, 0x12 "ContactInfo", // 0x05, 0x13 "ContainedvCard", // 0x05, 0x14 "Country", // 0x05, 0x15 "Crossing1", // 0x05, 0x16 "Crossing2", // 0x05, 0x17 "DevManufacturer", // 0x05, 0x18 "DirectContent", // 0x05, 0x19 "FreeTextLocation", // 0x05, 0x1A "GeoLocation", // 0x05, 0x1B "Language", // 0x05, 0x1C "Latitude", // 0x05, 0x1D "Longitude", // 0x05, 0x1E "Model", // 0x05, 0x1F "NamedArea", // 0x05, 0x20 "OnlineStatus", // 0x05, 0x21 "PLMN", // 0x05, 0x22 "PrefC", // 0x05, 0x23 "PreferredContacts",// 0x05, 0x24 "PreferredLanguage",// 0x05, 0x25 "PreferredContent", // 0x05, 0x26 "PreferredvCard", // 0x05, 0x27 "Registration", // 0x05, 0x28 "StatusContent", // 0x05, 0x29 "StatusMood", // 0x05, 0x2A "StatusText", // 0x05, 0x2B "Street", // 0x05, 0x2C "TimeZone", // 0x05, 0x2D "UserAvailability", // 0x05, 0x2E "Cap", // 0x05, 0x2F "Cname", // 0x05, 0x30 "Contact", // 0x05, 0x31 "Cpriority", // 0x05, 0x32 "Cstatus", // 0x05, 0x33 "Note", // 0x05, 0x34 /* WV 1.2 */ "Zone", // 0x05, 0x35 null, "Inf_link", // 0x05, 0x37 /* WV 1.2 */ "InfoLink", // 0x05, 0x38 /* WV 1.2 */ "Link", // 0x05, 0x39 /* WV 1.2 */ "Text", // 0x05, 0x3A /* WV 1.2 */ }; public static final String [] tagTablePage6 = { /* Messaging */ "BlockList", // 0x06, 0x05 // "BlockUser-Request", // 0x06, 0x06 //This is a bug in the spec "BlockEntity-Request", // 0x06, 0x06 "DeliveryMethod", // 0x06, 0x07 "DeliveryReport", // 0x06, 0x08 "DeliveryReport-Request", // 0x06, 0x09 "ForwardMessage-Request", // 0x06, 0x0A "GetBlockedList-Request", // 0x06, 0x0B "GetBlockedList-Response", // 0x06, 0x0C "GetMessageList-Request", // 0x06, 0x0D "GetMessageList-Response", // 0x06, 0x0E "GetMessage-Request", // 0x06, 0x0F "GetMessage-Response", // 0x06, 0x10 "GrantList", // 0x06, 0x11 "MessageDelivered", // 0x06, 0x12 "MessageInfo", // 0x06, 0x13 "MessageNotification", // 0x06, 0x14 "NewMessage", // 0x06, 0x15 "RejectMessage-Request", // 0x06, 0x16 "SendMessage-Request", // 0x06, 0x17 "SendMessage-Response", // 0x06, 0x18 "SetDeliveryMethod-Request",// 0x06, 0x19 "DeliveryTime", // 0x06, 0x1A }; public static final String [] tagTablePage7 = { /* Group */ "AddGroupMembers-Request", // 0x07, 0x05 "Admin", // 0x07, 0x06 "CreateGroup-Request", // 0x07, 0x07 "DeleteGroup-Request", // 0x07, 0x08 "GetGroupMembers-Request", // 0x07, 0x09 "GetGroupMembers-Response", // 0x07, 0x0A "GetGroupProps-Request", // 0x07, 0x0B "GetGroupProps-Response", // 0x07, 0x0C "GroupChangeNotice", // 0x07, 0x0D "GroupProperties", // 0x07, 0x0E "Joined", // 0x07, 0x0F "JoinedRequest", // 0x07, 0x10 "JoinGroup-Request", // 0x07, 0x11 "JoinGroup-Response", // 0x07, 0x12 "LeaveGroup-Request", // 0x07, 0x13 "LeaveGroup-Response", // 0x07, 0x14 "Left", // 0x07, 0x15 "MemberAccess-Request", // 0x07, 0x16 "Mod", // 0x07, 0x17 "OwnProperties", // 0x07, 0x18 "RejectList-Request", // 0x07, 0x19 "RejectList-Response", // 0x07, 0x1A "RemoveGroupMembers-Request",// 0x07, 0x1B "SetGroupProps-Request", // 0x07, 0x1C "SubscribeGroupNotice-Request", // 0x07, 0x1D "SubscribeGroupNotice-Response",// 0x07, 0x1E "Users", // 0x07, 0x1F "WelcomeNote", // 0x07, 0x20 "JoinGroup", // 0x07, 0x21 "SubscribeNotification", // 0x07, 0x22 "SubscribeType", // 0x07, 0x23 "GetJoinedUsers-Request", // 0x07, 0x24 /* WV 1.2 */ "GetJoinedUsers-Response", // 0x07, 0x25 /* WV 1.2 */ "AdminMapList", // 0x07, 0x26 /* WV 1.2 */ "AdminMapping", // 0x07, 0x27 /* WV 1.2 */ "Mapping", // 0x07, 0x28 /* WV 1.2 */ "ModMapping", // 0x07, 0x29 /* WV 1.2 */ "UserMapList", // 0x07, 0x2A /* WV 1.2 */ "UserMapping", // 0x07, 0x2B /* WV 1.2 */ }; public static final String [] tagTablePage8 = { /* Service ... continued */ "MP", // 0x08, 0x05 /* WV 1.2 */ "GETAUT", // 0x08, 0x06 /* WV 1.2 */ "GETJU", // 0x08, 0x07 /* WV 1.2 */ "VRID", // 0x08, 0x08 /* WV 1.2 */ "VerifyIDFunc", // 0x08, 0x09 /* WV 1.2 */ }; public static final String [] tagTablePage9 = { /* Common ... continued */ "CIR", // 0x09, 0x05 /* WV 1.2 */ "Domain", // 0x09, 0x06 /* WV 1.2 */ "ExtBlock", // 0x09, 0x07 /* WV 1.2 */ "HistoryPeriod", // 0x09, 0x08 /* WV 1.2 */ "IDList", // 0x09, 0x09 /* WV 1.2 */ "MaxWatcherList", // 0x09, 0x0A /* WV 1.2 */ "ReactiveAuthState", // 0x09, 0x0B /* WV 1.2 */ "ReactiveAuthStatus", // 0x09, 0x0C /* WV 1.2 */ "ReactiveAuthStatusList", // 0x09, 0x0D /* WV 1.2 */ "Watcher", // 0x09, 0x0E /* WV 1.2 */ "WatcherStatus" // 0x09, 0x0F /* WV 1.2 */ }; public static final String [] tagTablePageA = { /* Access ... continued */ "WV-CSP-NSDiscovery-Request", //0x0A, 0x05 /* WV 1.2 */ "WV-CSP-NSDiscovery-Response", //0x0A, 0x06 /* WV 1.2 */ "VersionList" //0x0A, 0x07 /* WV 1.2 */ }; public static final String [] attrStartTable = { "xmlns=http://www.wireless-village.org/CSP",// 0x00, 0x05 "xmlns=http://www.wireless-village.org/PA", // 0x00, 0x06 "xmlns=http://www.wireless-village.org/TRC",// 0x00, 0x07 "xmlns=http://www.openmobilealliance.org/DTD/WV-CSP", // 0x00, 0x08 "xmlns=http://www.openmobilealliance.org/DTD/WV-PA", // 0x00, 0x09 "xmlns=http://www.openmobilealliance.org/DTD/WV-TRC", // 0x00, 0x0A }; public static final String [] attrValueTable = { "AccessType", // 0x00 /* Common value token */ "ActiveUsers", // 0x01 /* Common value token */ "Admin", // 0x02 /* Common value token */ "application/", // 0x03 /* Common value token */ "application/vnd.wap.mms-message", // 0x04 /* Common value token */ "application/x-sms", // 0x05 /* Common value token */ "AutoJoin", // 0x06 /* Common value token */ "BASE64", // 0x07 /* Common value token */ "Closed", // 0x08 /* Common value token */ "Default", // 0x09 /* Common value token */ "DisplayName", // 0x0a /* Common value token */ "F", // 0x0b /* Common value token */ "G", // 0x0c /* Common value token */ "GR", // 0x0d /* Common value token */ "http://", // 0x0e /* Common value token */ "https://", // 0x0f /* Common value token */ "image/", // 0x10 /* Common value token */ "Inband", // 0x11 /* Common value token */ "IM", // 0x12 /* Common value token */ "MaxActiveUsers", // 0x13 /* Common value token */ "Mod", // 0x14 /* Common value token */ "Name", // 0x15 /* Common value token */ "None", // 0x16 /* Common value token */ "N", // 0x17 /* Common value token */ "Open", // 0x18 /* Common value token */ "Outband", // 0x19 /* Common value token */ "PR", // 0x1a /* Common value token */ "Private", // 0x1b /* Common value token */ "PrivateMessaging", // 0x1c /* Common value token */ "PrivilegeLevel", // 0x1d /* Common value token */ "Public", // 0x1e /* Common value token */ "P", // 0x1f /* Common value token */ "Request", // 0x20 /* Common value token */ "Response", // 0x21 /* Common value token */ "Restricted", // 0x22 /* Common value token */ "ScreenName", // 0x23 /* Common value token */ "Searchable", // 0x24 /* Common value token */ "S", // 0x25 /* Common value token */ "SC", // 0x26 /* Common value token */ "text/", // 0x27 /* Common value token */ "text/plain", // 0x28 /* Common value token */ "text/x-vCalendar", // 0x29 /* Common value token */ "text/x-vCard", // 0x2a /* Common value token */ "Topic", // 0x2b /* Common value token */ "T", // 0x2c /* Common value token */ "Type", // 0x2d /* Common value token */ "U", // 0x2e /* Common value token */ "US", // 0x2f /* Common value token */ "www.wireless-village.org", // 0x30 /* Common value token */ "AutoDelete", // 0x31 /* Common value token */ /* WV 1.2 */ "GM", // 0x32 /* Common value token */ /* WV 1.2 */ "Validity", // 0x33 /* Common value token */ /* WV 1.2 */ "ShowID", // 0x34 /* Common value token */ /* WV 1.2 */ "GRANTED", // 0x35 /* Common value token */ /* WV 1.2 */ "PENDING", // 0x36 /* Common value token */ /* WV 1.2 */ null, // 0x37 null, // 0x38 null, // 0x39 null, // 0x3a null, // 0x3b null, // 0x3c "GROUP_ID", // 0x3d /* Access value token */ "GROUP_NAME", // 0x3e /* Access value token */ "GROUP_TOPIC", // 0x3f /* Access value token */ "GROUP_USER_ID_JOINED", // 0x40 /* Access value token */ "GROUP_USER_ID_OWNER", // 0x41 /* Access value token */ "HTTP", // 0x42 /* Access value token */ "SMS", // 0x43 /* Access value token */ "STCP", // 0x44 /* Access value token */ "SUDP", // 0x45 /* Access value token */ "USER_ALIAS", // 0x46 /* Access value token */ "USER_EMAIL_ADDRESS", // 0x47 /* Access value token */ "USER_FIRST_NAME", // 0x48 /* Access value token */ "USER_ID", // 0x49 /* Access value token */ "USER_LAST_NAME", // 0x4a /* Access value token */ "USER_MOBILE_NUMBER", // 0x4b /* Access value token */ "USER_ONLINE_STATUS", // 0x4c /* Access value token */ "WAPSMS", // 0x4d /* Access value token */ "WAPUDP", // 0x4e /* Access value token */ "WSP", // 0x4f /* Access value token */ "GROUP_USER_ID_AUTOJOIN", // 0x50 /* Access value token */ /* WV 1.2 */ null, // 0x51 null, // 0x52 null, // 0x53 null, // 0x54 null, // 0x55 null, // 0x56 null, // 0x57 null, // 0x58 null, // 0x59 null, // 0x5a "ANGRY", // 0x5b /* Presence value token */ "ANXIOUS", // 0x5c /* Presence value token */ "ASHAMED", // 0x5d /* Presence value token */ "AUDIO_CALL", // 0x5e /* Presence value token */ "AVAILABLE", // 0x5f /* Presence value token */ "BORED", // 0x60 /* Presence value token */ "CALL", // 0x61 /* Presence value token */ "CLI", // 0x62 /* Presence value token */ "COMPUTER", // 0x63 /* Presence value token */ "DISCREET", // 0x64 /* Presence value token */ "EMAIL", // 0x65 /* Presence value token */ "EXCITED", // 0x66 /* Presence value token */ "HAPPY", // 0x67 /* Presence value token */ "IM", // 0x68 /* Presence value token */ "IM_OFFLINE", // 0x69 /* Presence value token */ "IM_ONLINE", // 0x6a /* Presence value token */ "IN_LOVE", // 0x6b /* Presence value token */ "INVINCIBLE", // 0x6c /* Presence value token */ "JEALOUS", // 0x6d /* Presence value token */ "MMS", // 0x6e /* Presence value token */ "MOBILE_PHONE", // 0x6f /* Presence value token */ "NOT_AVAILABLE", // 0x70 /* Presence value token */ "OTHER", // 0x71 /* Presence value token */ "PDA", // 0x72 /* Presence value token */ "SAD", // 0x73 /* Presence value token */ "SLEEPY", // 0x74 /* Presence value token */ "SMS", // 0x75 /* Presence value token */ "VIDEO_CALL", // 0x76 /* Presence value token */ "VIDEO_STREAM", // 0x77 /* Presence value token */ }; } ================================================ FILE: src/main/resources/META-INF/services/org.xmlpull.v1.XmlPullParserFactory ================================================ org.kxml2.io.KXmlParser,org.kxml2.io.KXmlSerializer ================================================ FILE: src/main/resources/application-prod.yml ================================================ spring: profiles: active: prod reader: app: storagePath: 'storage' showUI: false debug: false packaged: false secure: false inviteCode: "" secureKey: "" proxy: false proxyType: "HTTP" proxyHost: "" proxyPort: "" proxyUsername: "" proxyPassword: "" cacheChapterContent: true userLimit: 50 userBookLimit: 200 debugLog: false autoClearInactiveUser: 0 server: port: 8080 webUrl: http://localhost:${reader.server.port} logging: path: "./logs" ================================================ FILE: src/main/resources/application.yml ================================================ reader: app: storagePath: storage showUI: false debug: false packaged: false secure: false inviteCode: "" secureKey: "" proxy: false proxyType: "HTTP" proxyHost: "" proxyPort: "" proxyUsername: "" proxyPassword: "" cacheChapterContent: true userLimit: 50 userBookLimit: 200 debugLog: false autoClearInactiveUser: 0 server: port: 8080 webUrl: http://localhost:${reader.server.port} logging: path: "./logs" ================================================ FILE: src/main/resources/banner.txt ================================================ ██████  ███████  █████  ██████  ███████ ██████  ██   ██ ██      ██   ██ ██   ██ ██      ██   ██  ██████  █████  ███████ ██  ██ █████  ██████   ██   ██ ██     ██   ██ ██  ██ ██     ██   ██  ██  ██ ███████ ██  ██ ██████  ███████ ██  ██                                                ================================================ FILE: src/main/resources/defaultData/txtTocRule.json ================================================ [ { "id": -1, "enable": true, "name": "目录(去空白)", "rule": "(?<=[ \\s])(?:序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$", "serialNumber": 0 }, { "id": -2, "enable": true, "name": "目录", "rule": "^[  \\t]{0,4}(?:序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$", "serialNumber": 1 }, { "id": -3, "enable": false, "name": "目录(匹配简介)", "rule": "(?<=[ \\s])(?:(?:内容|文章)?简介|文案|前言|序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$", "serialNumber": 2 }, { "id": -4, "enable": false, "name": "目录(古典、轻小说备用)", "rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|回(?![合来事去])|场(?![和合比电是])|话|篇(?!张))).{0,30}$", "serialNumber": 3 }, { "id": -5, "enable": false, "name": "数字(纯数字标题)", "rule": "(?<=[ \\s])\\d+\\.?[  \\t]{0,4}$", "serialNumber": 4 }, { "id": -6, "enable": true, "name": "数字 分隔符 标题名称", "rule": "^[  \\t]{0,4}\\d{1,5}[::,., 、_—\\-].{1,30}$", "serialNumber": 5 }, { "id": -7, "enable": true, "name": "大写数字 分隔符 标题名称", "rule": "^[  \\t]{0,4}(?:序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|[〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[ 、_—\\-].{1,30}$", "serialNumber": 6 }, { "id": -8, "enable": true, "name": "正文 标题/序号", "rule": "^[  \\t]{0,4}正文[  ]{1,4}.{0,20}$", "serialNumber": 7 }, { "id": -9, "enable": true, "name": "Chapter/Section/Part/Episode 序号 标题", "rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Nn][oO]\\.|[Ee]pisode|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)\\s{0,4}\\d{1,4}.{0,30}$", "serialNumber": 8 }, { "id": -10, "enable": false, "name": "Chapter(去简介)", "rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Nn][Oo]\\.|[Ee]pisode)\\s{0,4}\\d{1,4}.{0,30}$", "serialNumber": 9 }, { "id": -11, "enable": true, "name": "特殊符号 序号 标题", "rule": "(?<=[\\s ])[【〔〖「『〈[\\[](?:第|[Cc]hapter)[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节].{0,20}$", "serialNumber": 10 }, { "id": -12, "enable": false, "name": "特殊符号 标题(成对)", "rule": "(?<=[\\s ]{0,4})(?:[\\[〈「『〖〔《(【\\(].{1,30}[\\)】)》〕〗』」〉\\]]?|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$", "serialNumber": 11 }, { "id": -13, "enable":true, "name": "特殊符号 标题(单个)", "rule": "(?<=[\\s ]{0,4})(?:[☆★✦✧].{1,30}|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$", "serialNumber": 12 }, { "id": -14, "enable": true, "name": "章/卷 序号 标题", "rule": "^[ \\t ]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|[卷章][\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[  ]{0,4}.{0,30}$", "serialNumber": 13 }, { "id": -15, "enable":false, "name": "顶格标题", "rule": "^\\S.{1,20}$", "serialNumber": 14 }, { "id": -16, "enable":false, "name": "双标题(前向)", "rule": "(?m)(?<=[ \\t ]{0,4})第[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$(?=[\\s ]{0,8}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章)", "serialNumber": 15 }, { "id": -17, "enable":false, "name": "双标题(后向)", "rule": "(?m)(?<=[ \\t ]{0,4}第[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$[\\s ]{0,8})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$", "serialNumber": 16 }, { "id":-18, "enable": true, "name": "标题 特殊符号 序号", "rule": "^.{1,20}[((][\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[))][  \t]{0,4}$", "serialNumber": 17 } ] ================================================ FILE: src/main/resources/dtd/openebook.org/dtds/oeb-1.2/oeb12.ent ================================================ ================================================ FILE: src/main/resources/dtd/openebook.org/dtds/oeb-1.2/oebpkg12.dtd ================================================ %OEBEntities; ================================================ FILE: src/main/resources/dtd/www.daisy.org/z3986/2005/ncx-2005-1.dtd ================================================ ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/ruby/xhtml-ruby-1.mod ================================================ ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-arch-1.mod ================================================ ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-attribs-1.mod ================================================ ]]> ]]> ]]> ]]> ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-base-1.mod ================================================ ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-bdo-1.mod ================================================ ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkphras-1.mod ================================================ ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkpres-1.mod ================================================ ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkstruct-1.mod ================================================ ]]> ]]> ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-charent-1.mod ================================================ %xhtml-lat1; %xhtml-symbol; %xhtml-special; ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-csismap-1.mod ================================================ ]]> ]]> ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-datatypes-1.mod ================================================ ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-datatypes-1.mod.1 ================================================ ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-edit-1.mod ================================================ ]]> ]]> ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-events-1.mod ================================================ ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-form-1.mod ================================================ ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-framework-1.mod ================================================ %xhtml-arch.mod;]]> %xhtml-notations.mod;]]> %xhtml-datatypes.mod;]]> %xhtml-xlink.mod; %xhtml-qname.mod;]]> %xhtml-events.mod;]]> %xhtml-attribs.mod;]]> %xhtml-model.redecl; %xhtml-model.mod;]]> %xhtml-charent.mod;]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-hypertext-1.mod ================================================ ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-image-1.mod ================================================ ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlphras-1.mod ================================================ ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlpres-1.mod ================================================ ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlstruct-1.mod ================================================ ]]> ]]> ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlstyle-1.mod ================================================ ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-lat1.ent ================================================ ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-link-1.mod ================================================ ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-list-1.mod ================================================ ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-meta-1.mod ================================================ ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-notations-1.mod ================================================ ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-object-1.mod ================================================ ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-param-1.mod ================================================ ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-pres-1.mod ================================================ %xhtml-inlpres.mod;]]> %xhtml-blkpres.mod;]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-qname-1.mod ================================================ ]]> %xhtml-qname-extra.mod; ]]> ]]> ]]> %xhtml-qname.redecl; ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-script-1.mod ================================================ ]]> ]]> ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-special.ent ================================================ ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-ssismap-1.mod ================================================ ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-struct-1.mod ================================================ ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-style-1.mod ================================================ ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-symbol.ent ================================================ ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-symbol.ent.1 ================================================ ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-table-1.mod ================================================ ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-text-1.mod ================================================ %xhtml-inlstruct.mod;]]> %xhtml-inlphras.mod;]]> %xhtml-blkstruct.mod;]]> %xhtml-blkphras.mod;]]> ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml11-model-1.mod ================================================ ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent ================================================ ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml-special.ent ================================================ ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml-symbol.ent ================================================ ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd ================================================ %HTMLlat1; %HTMLsymbol; %HTMLspecial; ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd ================================================ %HTMLlat1; %HTMLsymbol; %HTMLspecial; ================================================ FILE: src/main/resources/dtd/www.w3.org/TR/xhtml11/DTD/xhtml11.dtd ================================================ ]]> %xhtml-inlstyle.mod;]]> %xhtml-framework.mod;]]> ]]> %xhtml-text.mod;]]> %xhtml-hypertext.mod;]]> %xhtml-list.mod;]]> %xhtml-edit.mod;]]> %xhtml-bdo.mod;]]> %xhtml-ruby.mod;]]> %xhtml-pres.mod;]]> %xhtml-link.mod;]]> %xhtml-meta.mod;]]> %xhtml-base.mod;]]> %xhtml-script.mod;]]> %xhtml-style.mod;]]> %xhtml-image.mod;]]> %xhtml-csismap.mod;]]> %xhtml-ssismap.mod;]]> %xhtml-param.mod;]]> %xhtml-object.mod;]]> %xhtml-table.mod;]]> %xhtml-form.mod;]]> %xhtml-legacy.mod;]]> %xhtml-struct.mod;]]> ================================================ FILE: src/main/resources/epub/chapter.html ================================================ Chapter

{title}

{content} ================================================ FILE: src/main/resources/epub/cover.html ================================================ Cover

{name}

{author} / 著
================================================ FILE: src/main/resources/epub/fonts.css ================================================ @charset "utf-8"; /*---常用---*/ @font-face { font-family: "zw"; src: local("宋体"),local("明体"),local("明朝"), local("Songti"),local("Songti SC"),local("Songti TC"), /*iOS6+iBooks3*/ local("Song S"),local("Song T"),local("STBShusong"),local("TBMincho"),local("HYMyeongJo"), /*Kindle Paperwihite*/ local("DK-SONGTI"), url(../Fonts/zw.ttf), url(res:///opt/sony/ebook/FONT/zw.ttf), url(res:///Data/FONT/zw.ttf), url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), url(res:///ebook/fonts/../../mnt/sdcard/fonts/zw.ttf), url(res:///ebook/fonts/../../mnt/extsd/fonts/zw.ttf), url(res:///ebook/fonts/zw.ttf), url(res:///ebook/fonts/DroidSansFallback.ttf), url(res:///fonts/ttf/zw.ttf), url(res:///../../media/mmcblk0p1/fonts/zw.ttf), url(file:///mnt/us/DK_System/system/fonts/zw.ttf), /*Duokan Old Path*/ url(file:///mnt/us/DK_System/xKindle/res/userfonts/zw.ttf), /*Duokan 2012 Path*/ url(res:///abook/fonts/zw.ttf), url(res:///system/fonts/zw.ttf), url(res:///system/media/sdcard/fonts/zw.ttf), url(res:///media/fonts/zw.ttf), url(res:///sdcard/fonts/zw.ttf), url(res:///system/fonts/DroidSansFallback.ttf), url(res:///mnt/MOVIFAT/font/zw.ttf), url(res:///media/flash/fonts/zw.ttf), url(res:///media/sd/fonts/zw.ttf), url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), url(res:///../../fonts/zw.ttf), url(res:///../fonts/zw.ttf), url(../../../../../zw.ttf), /*EpubReaderI*/ url(res:///mnt/sdcard/fonts/zw.ttf), /*Nook for Android: fonts in TF Card*/ url(res:///fonts/zw.ttf), /*ADE1,8, 2.0 Program Path*/ url(res:///../../../../Windows/fonts/zw.ttf); /*ADE1,8, 2.0 Windows Path*/; } @font-face { font-family: "fs"; src: local("amasis30"),local("仿宋"),local("仿宋_GB2312"), local("Yuanti"),local("Yuanti SC"),local("Yuanti TC"), /*iOS6+iBooks3*/ local("DK-FANGSONG"), url(../Fonts/fs.ttf), url(res:///opt/sony/ebook/FONT/fs.ttf), url(res:///Data/FONT/fs.ttf), url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), url(res:///ebook/fonts/../../mnt/sdcard/fonts/fs.ttf), url(res:///ebook/fonts/../../mnt/extsd/fonts/fs.ttf), url(res:///ebook/fonts/fs.ttf), url(res:///ebook/fonts/DroidSansFallback.ttf), url(res:///fonts/ttf/fs.ttf), url(res:///../../media/mmcblk0p1/fonts/fs.ttf), url(file:///mnt/us/DK_System/system/fonts/fs.ttf), /*Duokan Old Path*/ url(file:///mnt/us/DK_System/xKindle/res/userfonts/fs.ttf), /*Duokan 2012 Path*/ url(res:///abook/fonts/fs.ttf), url(res:///system/fonts/fs.ttf), url(res:///system/media/sdcard/fonts/fs.ttf), url(res:///media/fonts/fs.ttf), url(res:///sdcard/fonts/fs.ttf), url(res:///system/fonts/DroidSansFallback.ttf), url(res:///mnt/MOVIFAT/font/fs.ttf), url(res:///media/flash/fonts/fs.ttf), url(res:///media/sd/fonts/fs.ttf), url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), url(res:///../../fonts/fs.ttf), url(res:///../fonts/fs.ttf), url(../../../../../fs.ttf), /*EpubReaderI*/ url(res:///mnt/sdcard/fonts/fs.ttf), /*Nook for Android: fonts in TF Card*/ url(res:///fonts/fs.ttf), /*ADE1,8, 2.0 Program Path*/ url(res:///../../../../Windows/fonts/fs.ttf); /*ADE1,8, 2.0 Windows Path*/; } @font-face { font-family: "kt"; src: local("Caecilia"),local("楷体"),local("楷体_GB2312"), local("Kaiti"),local("Kaiti SC"),local("Kaiti TC"), /*iOS6+iBooks3*/ local("MKai PRC"),local("MKaiGB18030C-Medium"),local("MKaiGB18030C-Bold"), /*Kindle Paperwihite*/ local("DK-KAITI"), url(../Fonts/kt.ttf), url(res:///opt/sony/ebook/FONT/kt.ttf), url(res:///Data/FONT/kt.ttf), url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), url(res:///ebook/fonts/../../mnt/sdcard/fonts/kt.ttf), url(res:///ebook/fonts/../../mnt/extsd/fonts/kt.ttf), url(res:///ebook/fonts/kt.ttf), url(res:///ebook/fonts/DroidSansFallback.ttf), url(res:///fonts/ttf/kt.ttf), url(res:///../../media/mmcblk0p1/fonts/kt.ttf), url(file:///mnt/us/DK_System/system/fonts/kt.ttf), /*Duokan Old Path*/ url(file:///mnt/us/DK_System/xKindle/res/userfonts/kt.ttf), /*Duokan 2012 Path*/ url(res:///abook/fonts/kt.ttf), url(res:///system/fonts/kt.ttf), url(res:///system/media/sdcard/fonts/kt.ttf), url(res:///media/fonts/kt.ttf), url(res:///sdcard/fonts/kt.ttf), url(res:///system/fonts/DroidSansFallback.ttf), url(res:///mnt/MOVIFAT/font/kt.ttf), url(res:///media/flash/fonts/kt.ttf), url(res:///media/sd/fonts/kt.ttf), url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), url(res:///../../fonts/kt.ttf), url(res:///../fonts/kt.ttf), url(../../../../../kt.ttf), /*EpubReaderI*/ url(res:///mnt/sdcard/fonts/kt.ttf), /*Nook for Android: fonts in TF Card*/ url(res:///fonts/kt.ttf), /*ADE1,8, 2.0 Program Path*/ url(res:///../../../../Windows/fonts/kt.ttf); /*ADE1,8, 2.0 Windows Path*/; } @font-face { font-family: "ht"; src: local("黑体"),local("微软雅黑"), local("Heiti"),local("Heiti SC"),local("Heiti TC"), /*iOS6+iBooks3*/ local("MYing Hei S"),local("MYing Hei T"),local("TBGothic"), /*Kindle Paperwihite*/ local("DK-HEITI"), url(../Fonts/ht.ttf), url(res:///opt/sony/ebook/FONT/ht.ttf), url(res:///Data/FONT/ht.ttf), url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), url(res:///ebook/fonts/../../mnt/sdcard/fonts/ht.ttf), url(res:///ebook/fonts/../../mnt/extsd/fonts/ht.ttf), url(res:///ebook/fonts/ht.ttf), url(res:///ebook/fonts/DroidSansFallback.ttf), url(res:///fonts/ttf/ht.ttf), url(res:///../../media/mmcblk0p1/fonts/ht.ttf), url(file:///mnt/us/DK_System/system/fonts/ht.ttf), /*Duokan Old Path*/ url(file:///mnt/us/DK_System/xKindle/res/userfonts/ht.ttf), /*Duokan 2012 Path*/ url(res:///abook/fonts/ht.ttf), url(res:///system/fonts/ht.ttf), url(res:///system/media/sdcard/fonts/ht.ttf), url(res:///media/fonts/ht.ttf), url(res:///sdcard/fonts/ht.ttf), url(res:///system/fonts/DroidSansFallback.ttf), url(res:///mnt/MOVIFAT/font/ht.ttf), url(res:///media/flash/fonts/ht.ttf), url(res:///media/sd/fonts/ht.ttf), url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), url(res:///../../fonts/ht.ttf), url(res:///../fonts/ht.ttf), url(../../../../../ht.ttf), /*EpubReaderI*/ url(res:///mnt/sdcard/fonts/ht.ttf), /*Nook for Android: fonts in TF Card*/ url(res:///fonts/ht.ttf), /*ADE1,8, 2.0 Program Path*/ url(res:///../../../../Windows/fonts/ht.ttf); /*ADE1,8, 2.0 Windows Path*/; } @font-face { font-family:"h1"; src: local("方正兰亭特黑长_GBK"),local("方正兰亭特黑长简体"),local("方正兰亭特黑长繁体"), local("LantingTeheichang"), local("Yuanti"),local("Yuanti SC"),local("Yuanti TC"), local("DK-HEITI"), url(../Fonts/h1.ttf), url(res:///opt/sony/ebook/FONT/h1.ttf), url(res:///Data/FONT/h1.ttf), url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), url(res:///ebook/fonts/../../mnt/sdcard/fonts/h1.ttf), url(res:///ebook/fonts/../../mnt/extsd/fonts/h1.ttf), url(res:///ebook/fonts/h1.ttf), url(res:///ebook/fonts/DroidSansFallback.ttf), url(res:///fonts/ttf/h1.ttf), url(res:///../../media/mmcblk0p1/fonts/h1.ttf), url(file:///mnt/us/DK_System/system/fonts/h1.ttf), /*Duokan Old Path*/ url(file:///mnt/us/DK_System/xKindle/res/userfonts/h1.ttf), /*Duokan 2012 Path*/ url(res:///abook/fonts/h1.ttf), url(res:///system/fonts/h1.ttf), url(res:///system/media/sdcard/fonts/h1.ttf), url(res:///media/fonts/h1.ttf), url(res:///sdcard/fonts/h1.ttf), url(res:///system/fonts/DroidSansFallback.ttf), url(res:///mnt/MOVIFAT/font/h1.ttf), url(res:///media/flash/fonts/h1.ttf), url(res:///media/sd/fonts/h1.ttf), url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), url(res:///../../fonts/h1.ttf), url(res:///../fonts/h1.ttf), url(../../../../../h1.ttf), /*EpubReaderI*/ url(res:///mnt/sdcard/fonts/h1.ttf), /*Nook for Android: fonts in TF Card*/ url(res:///fonts/h1.ttf), /*ADE1,8, 2.0 Program Path*/ url(res:///../../../../Windows/fonts/h1.ttf); /*ADE1,8, 2.0 Windows Path*/ } @font-face { font-family:"h2"; src: local("方正大标宋_GBK"),local("方正大标宋简体"),local("方正大标宋繁体"), local("Dabiaosong"), local("Heiti"),local("Heiti SC"),local("Heiti TC"), local("DK-XIAOBIAOSONG"), url(../Fonts/h2.ttf), url(res:///opt/sony/ebook/FONT/h2.ttf), url(res:///Data/FONT/h2.ttf), url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), url(res:///ebook/fonts/../../mnt/sdcard/fonts/h2.ttf), url(res:///ebook/fonts/../../mnt/extsd/fonts/h2.ttf), url(res:///ebook/fonts/h2.ttf), url(res:///ebook/fonts/DroidSansFallback.ttf), url(res:///fonts/ttf/h2.ttf), url(res:///../../media/mmcblk0p1/fonts/h2.ttf), url(file:///mnt/us/DK_System/system/fonts/h2.ttf), /*Duokan Old Path*/ url(file:///mnt/us/DK_System/xKindle/res/userfonts/h2.ttf), /*Duokan 2012 Path*/ url(res:///abook/fonts/h2.ttf), url(res:///system/fonts/h2.ttf), url(res:///system/media/sdcard/fonts/h2.ttf), url(res:///media/fonts/h2.ttf), url(res:///sdcard/fonts/h2.ttf), url(res:///system/fonts/DroidSansFallback.ttf), url(res:///mnt/MOVIFAT/font/h2.ttf), url(res:///media/flash/fonts/h2.ttf), url(res:///media/sd/fonts/h2.ttf), url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), url(res:///../../fonts/h2.ttf), url(res:///../fonts/h2.ttf), url(../../../../../h2.ttf), /*EpubReaderI*/ url(res:///mnt/sdcard/fonts/h2.ttf), /*Nook for Android: fonts in TF Card*/ url(res:///fonts/h2.ttf), /*ADE1,8, 2.0 Program Path*/ url(res:///../../../../Windows/fonts/h2.ttf); /*ADE1,8, 2.0 Windows Path*/ } @font-face { font-family:"h3"; src: local("方正华隶_GBK"),local("方正行黑简体"),local("方正行黑繁体"), local("Yuanti"),local("Yuanti SC"),local("Yuanti TC"), local("DK-FANGSONG"), url(../Fonts/h3.ttf), url(res:///opt/sony/ebook/FONT/h3.ttf), url(res:///Data/FONT/h3.ttf), url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), url(res:///ebook/fonts/../../mnt/sdcard/fonts/h3.ttf), url(res:///ebook/fonts/../../mnt/extsd/fonts/h3.ttf), url(res:///ebook/fonts/h3.ttf), url(res:///ebook/fonts/DroidSansFallback.ttf), url(res:///fonts/ttf/h3.ttf), url(res:///../../media/mmcblk0p1/fonts/h3.ttf), url(file:///mnt/us/DK_System/system/fonts/h3.ttf), /*Duokan Old Path*/ url(file:///mnt/us/DK_System/xKindle/res/userfonts/h3.ttf), /*Duokan 2012 Path*/ url(res:///abook/fonts/h3.ttf), url(res:///system/fonts/h3.ttf), url(res:///system/media/sdcard/fonts/h3.ttf), url(res:///media/fonts/h3.ttf), url(res:///sdcard/fonts/h3.ttf), url(res:///system/fonts/DroidSansFallback.ttf), url(res:///mnt/MOVIFAT/font/h3.ttf), url(res:///media/flash/fonts/h3.ttf), url(res:///media/sd/fonts/h3.ttf), url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), url(res:///../../fonts/h3.ttf), url(res:///../fonts/h3.ttf), url(../../../../../h3.ttf), /*EpubReaderI*/ url(res:///mnt/sdcard/fonts/h3.ttf), /*Nook for Android: fonts in TF Card*/ url(res:///fonts/h3.ttf), /*ADE1,8, 2.0 Program Path*/ url(res:///../../../../Windows/fonts/h3.ttf); /*ADE1,8, 2.0 Windows Path*/ } @font-face { font-family:"luohua"; src:local("汉仪落花体"), url("../Fonts/hylh.ttf"); } ================================================ FILE: src/main/resources/epub/intro.html ================================================ Intro

内容简介

{intro} ================================================ FILE: src/main/resources/epub/main.css ================================================ @charset "utf-8"; @import url("../Styles/fonts.css"); body { padding: 0%; margin-top: 0%; margin-bottom: 0%; margin-left: 0.5%; margin-right: 0.5%; line-height: 130%; text-align: justify; font-family: "DK-SONGTI","st","宋体","zw",sans-serif; } p { text-align: justify; text-indent: 2em; line-height: 130%; margin-right: 0.5%; margin-left: 0.5%; font-family: "DK-SONGTI","st","宋体","zw",sans-serif; } p.kaiti { font-family: "DK-KAITI","kt","楷体","zw",serif; } p.fangsong { font-family: "DK-FANGSONG","fs","仿宋","zw",serif; } span.xinli { font-family: "DK-KAITI","kt","楷体","zw",serif; color: #4e753f; } /** 英文斜体字 **/ span.english{ font-style: italic; } div { margin: 0px; padding: 0px; line-height: 120%; text-align: justify; font-family: "zw"; } div.foot { text-indent: 2em; margin: 30% 5% 0 5%; padding: 8px 0; } p.foot { font-family: "DK-KAITI","kt","楷体","zw",serif; } /*扉页*/ .booksubtitle { padding: 10px 0 0px 0; text-indent: 0em; font-size: 75%; font-family: "ht"; } .booktitle { padding: 9% 0 0 0; font-size: 1.3em; font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG"; font-weight: normal; text-indent: 0em; color: #000; text-align: center; line-height: 1.6; } .booktitle0 { font-size: 1.2em; font-family: "fs"; text-indent: 0em; text-align: center; line-height: 1.8; } .booktitle1 { padding: 0 0 0 0; font-size: 0.85em; font-family: "fs"; text-indent: 0em; text-align: center; line-height: 1.6; } .bookauthor { font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; padding: 5% 5px 0px 5px; text-indent: 0em; text-align: center; color: #000; font-size: 90%; line-height: 1.3; } .booktranslator { padding: 1% 5px 0px 5px; text-indent: 0em; text-align: center; font-size: 85%; line-height: 1.3; } .bookpub { font-family: "DK-KAITI","kt","楷体","楷体_gb2312"; padding: 30% 5px 5px 5px; text-indent: 0em; color: #000; text-align: center; font-size: 80%; } /*标题页*/ body.head { background-repeat:no-repeat no-repeat; background-size:160px 229px; background-position:bottom right; background-attachment:fixed; } body.xhead { background-color: #FDF5E6; } h1.head { font-family: "DK-HEITI",黑体,sans-serif; font-size: 1.2em; font-weight: bold; color: #311a02; text-indent: 0em; font-weight: normal; duokan-text-indent: 0em; padding: auto; text-align: center; margin-top: -8em; } div.head { border: solid 2px #ffffff; padding: 2px; margin: 2em auto 0.7em auto; text-align: center; width: 1em; } h1.head b { font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG"; font-weight: bold; font-size: 1.2em; text-align: center; text-indent: 0em; duokan-text-indent: 0em; color: #311a02; margin: 0.5em auto; line-height: 140%; } div.back { text-align: center; text-indent: 0em; duokan-text-indent: 0em; margin: 4em auto; } img.back { width: 70%; } img.back2 { width: 40%; margin: 2em 0 0 0; } /*正文*/ /**楷体引文**/ .titou { font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; } .yinwen { font-family: "DK-KAITI","kt","楷体","zw",serif; margin-left: 2em; text-indent: 0em; } .nicename { font-family: "DK-HEITI",黑体,sans-serif; font-weight: bold; font-size: 0.9em; } body.head3 { background-color: #a7bdcc; color: #354f66; } body.head4 { background-color: #bfd19b; color: #4e753f; } h2.head { font-family: "小标宋"; text-align: left; font-weight: bold; font-size: 1.1em; margin: -3em 2em 2em 0; color: #3f83e8; line-height: 140%; } h2.head span { font-family: "仿宋"; font-size: 0.7em; background-color: #3f83e8; border-radius: 9px; padding: 4px; color: #fff; } div.logo { margin: -2em 0% 0 0; text-align: right; } img.logo { width: 40%; } .imgl { /*图片居右*/ margin: -8.8em 1em 4em 0em; width: 80%; text-align: right; } h1.head { line-height:130%; font-size:1.4em; text-align: center; color: #BA2213; font-weight: bold; margin-top: 2em; margin-bottom: 1em; font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG"; } h3 { font-family: "DK-HEITI",黑体,sans-serif; font-size: 1.1em; margin: 1em 0; border-left: 1.2em solid #00a1e9; line-height: 120%; padding-left: 3px; color: #00a1e9; } h4 { font-family: "DK-HEITI",黑体,sans-serif; font-size: 1.1em; text-align: center; margin: 1em 0; line-height: 120%; color: #000; } h1.post { font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG"; text-align: center; font-size: 1.3em; color: #026fca; margin: 3em auto 2em auto; } .banquan { font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; text-align: left; color: #000; font-size:1.1em; margin-bottom:1em; text-indent: 1em; duokan-text-indent: 1em; } p.post { font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; } p.zy { font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; margin: 1em 0 0 1em; padding: 5px 0px 5px 10px; text-indent: 0em; border-left: 5px solid #a9b5c1; } .sign { font-family: "DK-KAITI","kt","楷体","zw",serif; margin: 1em 2px 0 auto; text-align: right; font-size: 0.8em; text-indent: 0em; duokan-text-indent: 0em; } .mark { font-family: "DK-HEITI",黑体,sans-serif; font-size: 0.9em; color: #fff; text-indent: 0em; duokan-text-indent: 0em; background-color: maroon; text-align: center; padding: 0px; margin: 2em 30%; } /*出版社*/ .chubanshe img{ width:106px; height:28px; } .chubanshe { margin-top:20px; } .cr { font-size:0.9em; } /*多看画廊*/ div.duokan-image-single { text-align: center; margin: 0.5em auto; /*插图盒子上下外边距为0.5em,左右设置auto是为了水平居中这个盒子*/ } img.picture-80 { margin: 0; /*清除img元素的外边距*/ width: 80%; /*预览窗口的宽度*/ box-shadow: 3px 3px 10px #bfbfbf; /*给图片添加阴影效果*/ } p.duokan-image-maintitle { margin: 1em 0 0; /*图片说明的段间距*/ font-family: "楷体"; /*图片说明使用的字体*/ font-size: 0.9em; /*字体大小*/ text-indent: 0; /*首行缩进为零,当你使用单标签p来指定首行缩进为2em时,记得在需要居中的文本中清除缩进,因为样式是叠加的*/ text-align: center; /*图片说明水平居中*/ color: #a52a2a; /*字体颜色*/ line-height: 1.25em; /*行高,防止有很长的图片说明*/ } /*制作说明页*/ body.description { background-image: url(../Images/001.png); background-position: bottom center; background-repeat: no-repeat; background-size: cover; padding: 25% 10% 0; font-size: 0.9em; } div.description-body { width: 55%; padding: 2em 1.3em; border-radius: 0.5em; font-size: 0.9em; border-style: solid; border-color: #393939; border-width: 0.3em; border-radius: 5em; background-color: #5a5a5a; box-shadow: 2px 2px 3px #828281; } h1.description-title { text-align: center; font-family: "黑体"; font-size: 1.2em; margin: 0 0 1em 0; color: #FF9; text-shadow: 1px 1px 0 black; } p.description-text { color: #f9ddd2; font-family: "准圆"; margin: 0; text-align: justify; text-indent: 0; duokan-text-indent: 0; } hr.description-hr { margin: 0.5em -1em; border-style: dotted; border-color: #9C9; border-width: 0.05em 0 0 0; } p.tips { text-align: justify; text-indent: 0; duokan-text-indent: 0; font-family: "楷体"; font-size: 0.7em; color: #FFC; margin: 0; } /*版本说明页*/ .ver { font-family: "DK-CODE","DK-XIHEITI",细黑体,"xihei",sans-serif; font-weight: bold; font-size: 100%; color: #000; margin: 1em 0 1em 0; text-align: center; } .vertitle { font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; font-size: 100%; text-indent: 0em; text-align: left; duokan-text-indent: 0em; } .vertxt { font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; line-height: 100%; font-size: 85%; text-indent: 0em; text-align: left; duokan-text-indent: 0em; } .verchar { font-family: "DK-KAITI","kt","楷体","楷体_gb2312"; text-align: left; text-indent: 1em; duokan-text-indent: 1em; margin-bottom: 1em; margin-top: 1em; } .vernote { font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; font-size: 75%; color: #686d70; text-indent: 0em; text-align: left; duokan-text-indent: 0em; padding-bottom: 15px; } .line { border: dotted #A2906A; border-width: 1px 0 0 0; } .entry { margin-left: 18px; font-size: 83%; color: #8fe0a3; text-indent: 0em; duokan-text-indent: 0em; } /*版权信息*/ .vol { text-indent: 0em; text-align: center; padding: 0.8em; margin: 0 auto 3px auto; color: #000; font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG"; font-size: 130%; text-shadow: none; } .cp { font-family: "DK-CODE","DK-XIHEITI",细黑体,"xihei",sans-serif; color: #412938; font-size: 70%; text-align: left; text-indent: 0em; duokan-text-indent: 0em; } .xchar { font-family: "DK-KAITI","kt","楷体","楷体_gb2312"; text-indent: 0em; duokan-text-indent: 0em; } /*多看弹注*/ sup img { line-height: 100%; width: auto; height: 1.0em; margin: 0em; padding: 0em; vertical-align: text-top; } ol { margin-bottom:0; padding:0 auto; list-style-type: decimal; } .hr { width:50%; margin:2em 0 0 0.5em; padding:0; height:2px; background-color: #F3221D; } .duokan-footnote-content{ padding:0 auto; text-align: left; } .duokan-footnote-item { font-family:"DK-XIHEITI",细黑体,"xihei",sans-serif; text-align: left; font-size: 80%; line-height: 100%; clear: both; color:#000; list-style-type:decimal; } li.duokan-footnote-item a { font-family:"DK-HEITI"; text-align: left; } a{ text-decoration: none; color: #222; } a:hover {background: #81caf9} a:active {background: yellow} .duokan-image-maintitle { font-family:"DK-HEITI",黑体,"hei",sans-serif; text-align: center; text-indent: 0em; duokan-text-indent: 0em; font-size:90%; color: #1F4150; margin-top: 1em; } .duokan-image-subtitle { font-family:"DK-XIHEITI",细黑体,"xihei",sans-serif; text-align: center; text-indent: 0em; duokan-text-indent: 0em; font-size:70%; color: #3A3348; margin-top: 1em; } ================================================ FILE: src/main/resources/logback-spring.xml ================================================ %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n ${logPath}/reader-%d{yyyy-MM-dd}.%i.log 100MB [%thread] %d{HH:mm:ss.SSS} %-5level %logger{0} - %msg%n ================================================ FILE: src/test/java/com/htmake/reader/ReaderApplicationTests.java ================================================ package com.htmake.reader; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class ReaderApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: vetur.config.js ================================================ // vetur.config.js /** @type {import('vls').VeturConfig} */ module.exports = { // **optional** default: `{}` // override vscode settings // Notice: It only affects the settings used by Vetur. settings: { "vetur.useWorkspaceDependencies": true, "vetur.experimental.templateInterpolationService": true }, // **optional** default: `[{ root: './' }]` projects: [ './web' ] } ================================================ FILE: web/.browserslistrc ================================================ > 0.5% last 2 chrome version last 2 firefox version last 2 edge version last 2 opera version last 22 ios versions last 65 android versions last 4 ie versions ================================================ FILE: web/.eslintrc.js ================================================ module.exports = { root: true, env: { node: true }, extends: ["plugin:vue/essential", "@vue/prettier"], globals: { workbox: "writable" }, rules: { "no-console": process.env.NODE_ENV === "production" ? "error" : "off", "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" }, parserOptions: { parser: "babel-eslint" } }; ================================================ FILE: web/.gitignore ================================================ .DS_Store node_modules /dist # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? #Carlo build output /dist_carlo /.profile dist*.zip yarn.lock pnpm-lock.yaml ================================================ FILE: web/LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: web/README.md ================================================ # 「阅读3.0」 web 端(可设置IP) 本程序为「阅读3.0」的配套 web 端,需要保证手机和电脑在同一局域网内,然后手机端打开 web 服务。 在线地址 http://alanskycn.gitee.io/vip/reader/ ## 具体实现 使用 Vue2 开发 ## 功能特性 - 本地存储阅读记录与设置 - 阅读主题切换 - 夜间模式 - 字号调节 - 字体调节 - 阅读宽度调节 ## 使用方法 ```shell yarn install #安装项目 yarn serve #开发模式 yarn build #打包 yarn lint #格式化代码 ``` - ~~点击`Star`自动编译,可在Actions查看~~ - ~~编译失败,可先点击`Unstar`,再点击`Star`重新开始~~ ================================================ FILE: web/babel.config.js ================================================ module.exports = { presets: [ [ "@vue/app", { polyfills: ["es.promise", "es.symbol"] } ] ], plugins: [ [ "component", { libraryName: "element-ui", styleLibraryName: "theme-chalk" } ] ] }; ================================================ FILE: web/jsconfig.json ================================================ { "include": [ "./src/**/*" ] } ================================================ FILE: web/package.json ================================================ { "name": "reader", "version": "2.5.4", "private": true, "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint", "sync": "yarn build && rm -rf ../src/main/resources/web && mv dist ../src/main/resources/web" }, "dependencies": { "axios": "^0.21.1", "codejar": "^3.5.0", "core-js": "^3.3.2", "element-ui": "^2.15.9", "localforage": "^1.10.0", "prismjs": "^1.25.0", "register-service-worker": "^1.7.1", "sortablejs": "^1.15.0", "stylus": "^0.54.7", "stylus-loader": "^3.0.2", "vue": "^2.6.10", "vue-lazyload": "^1.3.3", "vue-router": "^3.1.3", "vuex": "^3.1.1" }, "devDependencies": { "@vue/cli-plugin-babel": "^4.0.0", "@vue/cli-plugin-eslint": "^4.0.0", "@vue/cli-plugin-pwa": "^4.0.0", "@vue/cli-plugin-router": "^4.0.0", "@vue/cli-service": "^4.0.0", "@vue/eslint-config-prettier": "^5.0.0", "babel-eslint": "^10.0.3", "babel-plugin-component": "^1.1.1", "eslint": "^5.16.0", "eslint-plugin-prettier": "^3.1.1", "eslint-plugin-vue": "^5.0.0", "prettier": "^1.18.2", "vue-cli-plugin-element": "^1.0.1", "vue-template-compiler": "^2.6.10" } } ================================================ FILE: web/postcss.config.js ================================================ module.exports = { plugins: { autoprefixer: {} } }; ================================================ FILE: web/public/bookSourceDebug/index.css ================================================ body { margin: 0; } .editor { display: flex; align-items: stretch; } .setbox, .menu, .outbox { flex: 1; display: flex; flex-flow: column; max-height: 100vh; overflow-y: auto; } .menu { justify-content: center; max-width: 90px; margin: 0 5px; } .menu .button { width: 90px; height: 30px; min-height: 30px; margin: 5px 0px; cursor: pointer; } @keyframes stroker { 0% { stroke-dashoffset: 0; } 100% { stroke-dashoffset: -240; } } .button rect { width: 100%; height: 100%; fill: transparent; stroke: #666; stroke-width: 2px; } .button rect.busy { stroke: #fd1850; stroke-dasharray: 30 90; animation: stroker 1s linear infinite; } .button text { text-anchor: middle; dominant-baseline: middle; } .setbox { min-width: 40em; } .rules { overflow: auto; } .tabbox { flex: 1; display: flex; flex-flow: column; } .rules > * { display: flex; margin: 2px 0; } .rules textarea { flex: 1; margin-left: 5px; } .rules > *, .rules > * > div, .rules textarea { min-height: 1.2em; } textarea { word-break: break-all; } .tabtitle { display: flex; z-index: 1; justify-content: flex-end; } .tabtitle > div { cursor: pointer; padding: 1px 10px 0 10px; border-bottom: 3px solid transparent; font-weight: bold; } .tabtitle > .this { color: #4f9da6; border-bottom-color: #4ebbe4; } .tabbody { flex: 1; display: flex; margin-top: -1px; border: 1px solid #a9a9a9; height: 0; } .tabbody > * { flex: 1; flex-flow: column; display: none; } .tabbody > .this { display: flex; } .tabbody > * > .titlebar { display: flex; } .tabbody > * > .titlebar > * { flex: 1; margin: 1px 1px 1px 1px; } .tabbody > * > .context { flex: 1; flex-flow: column; border: 0; padding: 5px; overflow-y: auto; } .tabbody > * > .inputbox { border: 0; border-bottom: #a9a9a9 solid 1px; height: 15px; text-align: center; } .link > * { display: flex; margin: 5px; border-bottom: 1px solid; text-decoration: none; } #RuleList > label > * { background: #eee; padding-left: 3px; margin: 2px 0; cursor: pointer; } #RuleList input[type="radio"] { display: none; } #RuleList input[type="radio"]:checked + * { background: #15cda8; } .isError { color: #ff0000; } ================================================ FILE: web/public/bookSourceDebug/index.html ================================================ Reader书源编辑器
←主页 书源
基本
源域名 :
源类型 :
源名称 :
源分组 :
源注释 :
登录地址:
登录界面:
登录检测:
并发率 :
请求头 :
链接验证:

搜索
搜索地址:
校验文字:
列表规则:
书名规则:
作者规则:
分类规则:
字数规则:
最新章节:
简介规则:
封面规则:
详情地址:

发现
发现地址:
列表规则:
书名规则:
作者规则:
分类规则:
字数规则:
最新章节:
简介规则:
封面规则:
详情地址:

详情
预处理 :
书名规则:
作者规则:
分类规则:
字数规则:
最新章节:
简介规则:
封面规则:
目录地址:

目录
列表规则:
章节名称:
章节地址:
卷名标识:
收费标识:
购买标识:
章节信息:
翻页规则:

正文
脚本注入:
正文规则:
翻页规则:
资源正则:
替换规则:
图片样式:
购买操作:

其它规则
启用搜索:
启用发现:
搜索权重:
排序编号:
更新时间:
编辑源
调试源
源列表
帮助信息
================================================ FILE: web/public/bookSourceDebug/index.js ================================================ /* eslint-disable no-inner-declarations */ /* eslint-disable no-case-declarations */ // 简化js原生选择器 function $(selector) { return document.querySelector(selector); } function $$(selector) { return document.querySelectorAll(selector); } // 读写Hash值(val未赋值时为读取) function hashParam(key, val) { let hashstr = decodeURIComponent(window.location.hash); let regKey = new RegExp(`${key}=([^&]*)`); let getVal = regKey.test(hashstr) ? hashstr.match(regKey)[1] : null; if (val == undefined) return getVal; if (hashstr == "" || hashstr == "#") { window.location.hash = `#${key}=${val}`; } else { if (getVal) window.location.hash = hashstr.replace(getVal, val); else { window.location.hash = hashstr.indexOf(key) > -1 ? hashstr.replace(regKey, `${key}=${val}`) : `${hashstr}&${key}=${val}`; } } } // 创建源规则容器对象 function Container() { let ruleJson = {}; let searchJson = {}; let exploreJson = {}; let bookInfoJson = {}; let tocJson = {}; let contentJson = {}; // 基本以及其他 $$(".rules .base").forEach((item) => (ruleJson[item.title] = "")); ruleJson.lastUpdateTime = 0; ruleJson.customOrder = 0; ruleJson.weight = 0; ruleJson.enabled = true; ruleJson.enabledExplore = true; // 搜索规则 $$(".rules .ruleSearch").forEach((item) => (searchJson[item.title] = "")); ruleJson.ruleSearch = searchJson; // 发现规则 $$(".rules .ruleExplore").forEach((item) => (exploreJson[item.title] = "")); ruleJson.ruleExplore = exploreJson; // 详情页规则 $$(".rules .ruleBookInfo").forEach((item) => (bookInfoJson[item.title] = "")); ruleJson.ruleBookInfo = bookInfoJson; // 目录规则 $$(".rules .ruleToc").forEach((item) => (tocJson[item.title] = "")); ruleJson.ruleToc = tocJson; // 正文规则 $$(".rules .ruleContent").forEach((item) => (contentJson[item.title] = "")); ruleJson.ruleContent = contentJson; return ruleJson; } // 选项卡Tab切换事件处理 function showTab(tabName) { $$(".tabtitle>*").forEach((node) => { node.className = node.className.replace(" this", ""); }); $$(".tabbody>*").forEach((node) => { node.className = node.className.replace(" this", ""); }); $(`.tabbody>.${$(`.tabtitle>*[name=${tabName}]`).className}`).className += " this"; $(`.tabtitle>*[name=${tabName}]`).className += " this"; hashParam("tab", tabName); } // 源列表列表标签构造函数 function newRule(rule) { return ``; } // 缓存规则列表 var RuleSources = []; if (localStorage.getItem("debug@BookSources")) { RuleSources = JSON.parse(localStorage.getItem("debug@BookSources")); RuleSources.forEach((item) => ($("#RuleList").innerHTML += newRule(item))); } // 页面加载完成事件 window.onload = () => { $$(".tabtitle>*").forEach((item) => { item.addEventListener("click", () => { showTab(item.innerHTML); }); }); if (hashParam("tab")) showTab(hashParam("tab")); }; // 获取数据 function HttpGet(url) { return fetch(hashParam("domain") ? hashParam("domain") + url : url, { mode: "cors", credentials: "include" }) .then((res) => res.json()) .catch((err) => console.error("Error:", err)); } // 提交数据 function HttpPost(url, data) { return fetch(hashParam("domain") ? hashParam("domain") + url : url, { body: JSON.stringify(data), method: "POST", mode: "cors", credentials: "include", headers: new Headers({ "Content-Type": "application/json;charset=utf-8" }) }) .then((res) => res.json()) .catch((err) => console.error("Error:", err)); } // 将源表单转化为源对象 function rule2json() { let RuleJSON = Container(); // 转换base Object.keys(RuleJSON).forEach((key) => { if (!key.startsWith("rule")) { RuleJSON[key] = $("#" + key).value; } }); // 转换搜索规则 let searchJson = {}; Object.keys(RuleJSON.ruleSearch).forEach((key) => { if ($("#" + "ruleSearch_" + key).value) searchJson[key] = $("#" + "ruleSearch_" + key).value; }); RuleJSON.ruleSearch = searchJson; // 转换发现规则 let exploreJson = {}; Object.keys(RuleJSON.ruleExplore).forEach((key) => { if ($("#" + "ruleExplore_" + key).value) exploreJson[key] = $("#" + "ruleExplore_" + key).value; }); RuleJSON.ruleExplore = exploreJson; // 转换详情页规则 let bookInfoJson = {}; Object.keys(RuleJSON.ruleBookInfo).forEach((key) => { if ($("#" + "ruleBookInfo_" + key).value) bookInfoJson[key] = $("#" + "ruleBookInfo_" + key).value; }); RuleJSON.ruleBookInfo = bookInfoJson; // 转换目录规则 let tocJson = {}; Object.keys(RuleJSON.ruleToc).forEach((key) => { if ($("#" + "ruleToc_" + key).value) tocJson[key] = $("#" + "ruleToc_" + key).value; }); RuleJSON.ruleToc = tocJson; // 转换正文规则 let contentJson = {}; Object.keys(RuleJSON.ruleContent).forEach((key) => { if ($("#" + "ruleContent_" + key).value) contentJson[key] = $("#" + "ruleContent_" + key).value; }); RuleJSON.ruleContent = contentJson; RuleJSON.lastUpdateTime = new Date().getTime(); RuleJSON.customOrder = RuleJSON.customOrder == "" ? 0 : parseInt(RuleJSON.customOrder); RuleJSON.weight = RuleJSON.weight == "" ? 0 : parseInt(RuleJSON.weight); RuleJSON.bookSourceType = RuleJSON.bookSourceType == "" ? 0 : parseInt(RuleJSON.bookSourceType); RuleJSON.enabled = RuleJSON.enabled == "" || String(RuleJSON.enabled) .toLocaleLowerCase() .replace(/^\s*|\s*$/g, "") == "true"; RuleJSON.enabledExplore = RuleJSON.enabledExplore == "" || String(RuleJSON.enabledExplore) .toLocaleLowerCase() .replace(/^\s*|\s*$/g, "") == "true"; return RuleJSON; } // 将源对象填充到源表单 function json2rule(RuleEditor) { let RuleJSON = Container(); // 转换base Object.keys(RuleJSON).forEach((key) => { if (!key.startsWith("rule")) { let val = RuleEditor[key]; if (typeof val == "number") { $("#" + key).value = val ? String(val) : "0"; } else if (typeof val == "boolean") { $("#" + key).value = val ? String(val) : "false"; } else { $("#" + key).value = val ? String(val) : ""; } } }); // 转换搜索规则 if (RuleEditor.ruleSearch) { let searchJson = RuleEditor.ruleSearch; Object.keys(RuleJSON.ruleSearch).forEach((key) => { $("#" + "ruleSearch_" + key).value = searchJson[key] ? searchJson[key] : ""; }); } // 转换发现规则 if (RuleEditor.ruleExplore) { let exploreJson = RuleEditor.ruleExplore; Object.keys(RuleJSON.ruleExplore).forEach((key) => { $("#" + "ruleExplore_" + key).value = exploreJson[key] ? exploreJson[key] : ""; }); } // 转换详情页规则 if (RuleEditor.ruleBookInfo) { let bookInfoJson = RuleEditor.ruleBookInfo; Object.keys(RuleJSON.ruleBookInfo).forEach((key) => { $("#" + "ruleBookInfo_" + key).value = bookInfoJson[key] ? bookInfoJson[key] : ""; }); } // 转换目录规则 if (RuleEditor.ruleToc) { let tocJson = RuleEditor.ruleToc; Object.keys(RuleJSON.ruleToc).forEach((key) => { $("#" + "ruleToc_" + key).value = tocJson[key] ? tocJson[key] : ""; }); } // 转换正文规则 if (RuleEditor.ruleContent) { let contentJson = RuleEditor.ruleContent; Object.keys(RuleJSON.ruleContent).forEach((key) => { $("#" + "ruleContent_" + key).value = contentJson[key] ? contentJson[key] : ""; }); } } // 记录操作过程 var course = { old: [], now: {}, new: [] }; if (localStorage.getItem("debug@bookSourceRecord")) { course = JSON.parse(localStorage.getItem("debug@bookSourceRecord")); json2rule(course.now); } else { course.now = rule2json(); window.localStorage.setItem("debug@bookSourceRecord", JSON.stringify(course)); } function todo() { course.old.push(Object.assign({}, course.now)); course.now = rule2json(); course.new = []; if (course.old.length > 50) course.old.shift(); // 限制历史记录堆栈大小 localStorage.setItem("debug@bookSourceRecord", JSON.stringify(course)); } function undo() { course = JSON.parse(localStorage.getItem("debug@bookSourceRecord")); if (course.old.length > 0) { course.new.push(course.now); course.now = course.old.pop(); localStorage.setItem("debug@bookSourceRecord", JSON.stringify(course)); json2rule(course.now); } } function redo() { course = JSON.parse(localStorage.getItem("debug@bookSourceRecord")); if (course.new.length > 0) { course.old.push(course.now); course.now = course.new.pop(); localStorage.setItem("debug@bookSourceRecord", JSON.stringify(course)); json2rule(course.now); } } function setRule(editRule) { let checkRule = RuleSources.find( (x) => x.bookSourceUrl == editRule.bookSourceUrl ); if ($(`input[id="${editRule.bookSourceUrl}"]`)) { Object.keys(checkRule).forEach((key) => { checkRule[key] = editRule[key]; }); $( `input[id="${editRule.bookSourceUrl}"]+*` ).innerHTML = `${editRule.bookSourceName}
${editRule.bookSourceUrl}`; } else { RuleSources.push(editRule); $("#RuleList").innerHTML += newRule(editRule); } } $$("input").forEach((item) => { item.addEventListener("change", () => { todo(); }); }); $$("textarea").forEach((item) => { item.addEventListener("change", () => { todo(); }); }); // 处理按钮点击事件 $(".menu").addEventListener("click", (e) => { let thisNode = e.target; thisNode = thisNode.parentNode.nodeName == "svg" ? thisNode.parentNode.querySelector("rect") : thisNode.nodeName == "svg" ? thisNode.querySelector("rect") : null; if (!thisNode) return; if (thisNode.getAttribute("class") == "busy") return; thisNode.setAttribute("class", "busy"); switch (thisNode.id) { case "push": $$("#RuleList>label>div").forEach((item) => { item.className = ""; }); (async () => { await HttpPost(`/saveBookSources`, RuleSources) .then((json) => { if (json.isSuccess) { let okData = json.data; if (Array.isArray(okData)) { let failMsg = ``; if (RuleSources.length > okData.length) { RuleSources.forEach((item) => { if ( okData.find((x) => x.bookSourceUrl == item.bookSourceUrl) ) { // } else { $(`#RuleList #${item.bookSourceUrl}+*`).className += "isError"; } }); failMsg = "\n推送失败的源将用红色字体标注!"; } alert( `批量推送源到「Reader」\n共计: ${ RuleSources.length } 条\n成功: ${okData.length} 条\n失败: ${RuleSources.length - okData.length} 条${failMsg}` ); } else { alert( `批量推送源到「Reader」成功!\n共计: ${RuleSources.length} 条` ); } } else { alert(`批量推送源失败!\nErrorMsg: ${json.errorMsg}`); } }) .catch((err) => { alert(`批量推送源失败,无法连接到「Reader」!\n${err}`); }); thisNode.setAttribute("class", ""); })(); return; case "pull": showTab("源列表"); (async () => { await HttpGet(`/getBookSources`) .then((json) => { if (json.isSuccess) { $("#RuleList").innerHTML = ""; localStorage.setItem( "debug@BookSources", JSON.stringify((RuleSources = json.data)) ); RuleSources.forEach((item) => { $("#RuleList").innerHTML += newRule(item); }); alert(`成功拉取 ${RuleSources.length} 条源`); } else { alert(`批量拉取源失败!\nErrorMsg: ${json.errorMsg}`); } }) .catch((err) => { alert(`批量拉取源失败,无法连接到「Reader」!\n${err}`); }); thisNode.setAttribute("class", ""); })(); return; case "editor": if ($("#RuleJsonString").value == "") break; try { json2rule(JSON.parse($("#RuleJsonString").value)); todo(); } catch (error) { console.log(error); alert(error); } break; case "conver": showTab("编辑源"); $("#RuleJsonString").value = JSON.stringify(rule2json(), null, 4); break; case "initial": $$(".rules textarea").forEach((item) => { item.value = ""; }); todo(); break; case "undo": undo(); break; case "redo": redo(); break; case "debug": showTab("调试源"); // let wsOrigin = (hashParam("domain") || location.origin) // .replace(/^.*?:/, "ws:") // .replace(/\d+$/, port => parseInt(port) + 1); let DebugInfos = $("#DebugConsole"); function DebugPrint(msg) { DebugInfos.value += `\n${msg}`; DebugInfos.scrollTop = DebugInfos.scrollHeight; } let saveRule = [rule2json()]; HttpPost(`/saveBookSources`, saveRule) .then((sResult) => { if (sResult.isSuccess) { let sKey = $("#DebugKey").value ? $("#DebugKey").value : "我的"; $( "#DebugConsole" ).value = `源《${saveRule[0].bookSourceName}》保存成功!使用搜索关键字“${sKey}”开始调试...`; let url = (hashParam("domain") || location.origin) + "/bookSourceDebugSSE?bookSourceUrl=" + encodeURIComponent(saveRule[0].bookSourceUrl) + "&keyword=" + encodeURIComponent(sKey); const tryClose = () => { try { thisNode.setAttribute("class", ""); if ( window.searchEventSource && window.searchEventSource.readyState != window.searchEventSource.CLOSED ) { window.searchEventSource.close(); } window.searchEventSource = null; } catch (error) { // } }; window.searchEventSource = new EventSource(url, { withCredentials: true }); window.searchEventSource.addEventListener("error", e => { tryClose(); try { if (e.data) { const result = JSON.parse(e.data); if (result && result.errorMsg) { DebugPrint(result.errorMsg); } } } catch (error) { // } }); window.searchEventSource.addEventListener("end", () => { tryClose(); }); window.searchEventSource.addEventListener("message", e => { try { if (e.data) { const result = JSON.parse(e.data); if (result.msg) { DebugPrint(result.msg); } } } catch (error) { // } }); // let ws = new WebSocket(`${wsOrigin}/bookSourceDebug`); // ws.onopen = () => { // ws.send( // `{"tag":"${saveRule[0].bookSourceUrl}", "key":"${sKey}"}` // ); // }; // ws.onmessage = msg => { // console.log("[调试]", msg); // DebugPrint(msg.data); // }; // ws.onerror = err => { // throw `${err.data}`; // }; // ws.onclose = () => { // thisNode.setAttribute("class", ""); // DebugPrint(`\n调试服务已关闭!`); // }; } else throw `${sResult.errorMsg}`; }) .catch((err) => { DebugPrint(`调试过程意外中止,以下是详细错误信息:\n${err}`); thisNode.setAttribute("class", ""); }); return; case "accept": (async () => { let saveRule = [rule2json()]; await HttpPost(`/saveBookSource`, saveRule[0]) .then((json) => { alert( json.isSuccess ? `源《${saveRule[0].bookSourceName}》已成功保存到「Reader」` : `源《${saveRule[0].bookSourceName}》保存失败!\nErrorMsg: ${json.errorMsg}` ); setRule(saveRule[0]); }) .catch((err) => { alert(`保存源失败,无法连接到「Reader」!\n${err}`); }); thisNode.setAttribute("class", ""); })(); return; default: } setTimeout(() => { thisNode.setAttribute("class", ""); }, 500); }); $("#DebugKey").addEventListener("keydown", (e) => { if (e.keyCode == 13) { let clickEvent = document.createEvent("MouseEvents"); clickEvent.initEvent("click", true, false); $("#debug").dispatchEvent(clickEvent); } }); $("#Filter").addEventListener("keydown", (e) => { if (e.keyCode == 13) { let cashList = []; $("#RuleList").innerHTML = ""; let sKey = $("#Filter").value ? $("#Filter").value : ""; if (sKey == "") { cashList = RuleSources; } else { let patt = new RegExp(sKey); RuleSources.forEach((source) => { if ( patt.test(source.bookSourceUrl) || patt.test(source.bookSourceName) || patt.test(source.bookSourceGroup) ) { cashList.push(source); } }); } cashList.forEach((source) => { $("#RuleList").innerHTML += newRule(source); }); } }); // 列表规则更改事件 $("#RuleList").addEventListener("click", (e) => { let editRule = null; if (e.target && e.target.getAttribute("name") == "rule") { editRule = rule2json(); json2rule(RuleSources.find((x) => x.bookSourceUrl == e.target.id)); } else return; if (editRule.bookSourceUrl == "") return; if (editRule.bookSourceName == "") editRule.bookSourceName = editRule.bookSourceUrl.replace( /.*?\/\/|\/.*/g, "" ); setRule(editRule); localStorage.setItem("debug@BookSources", JSON.stringify(RuleSources)); }); // 处理列表按钮事件 $(".tab3>.titlebar").addEventListener("click", (e) => { let thisNode = e.target; if (thisNode.nodeName != "BUTTON") return; switch (thisNode.id) { case "Import": let fileImport = document.createElement("input"); fileImport.type = "file"; fileImport.accept = ".json"; fileImport.addEventListener( "change", () => { let file = fileImport.files[0]; let reader = new FileReader(); reader.onloadend = function(evt) { if (evt.target.readyState == FileReader.DONE) { let fileText = evt.target.result; try { let fileJson = JSON.parse(fileText); let newSources = []; newSources.push(...fileJson); if ( window.confirm( `如何处理导入的源?\n"确定": 覆盖当前列表(不会删除APP源)\n"取消": 插入列表尾部(自动忽略重复源)` ) ) { localStorage.setItem( "debug@BookSources", JSON.stringify((RuleSources = newSources)) ); $("#RuleList").innerHTML = ""; RuleSources.forEach((item) => { $("#RuleList").innerHTML += newRule(item); }); } else { newSources = newSources.filter( (item) => !JSON.stringify(RuleSources).includes(item.bookSourceUrl) ); RuleSources.push(...newSources); localStorage.setItem( "debug@BookSources", JSON.stringify(RuleSources) ); newSources.forEach((item) => { $("#RuleList").innerHTML += newRule(item); }); } alert(`成功导入 ${newSources.length} 条源`); } catch (err) { alert(`导入源文件失败!\n${err}`); } } }; reader.readAsText(file); }, false ); fileImport.click(); break; case "Export": let fileExport = document.createElement("a"); fileExport.download = `Rules${Date() .replace(/.*?\s(\d+)\s(\d+)\s(\d+:\d+:\d+).*/, "$2$1$3") .replace(/:/g, "")}.json`; let myBlob = new Blob([JSON.stringify(RuleSources, null, 4)], { type: "application/json", }); fileExport.href = window.URL.createObjectURL(myBlob); fileExport.click(); break; case "Delete": let selectRule = $("#RuleList input:checked"); if (!selectRule) { alert(`没有源被选中!`); return; } if (confirm(`确定要删除选定源吗?\n(同时删除APP内源)`)) { let selectRuleUrl = selectRule.id; let deleteSources = RuleSources.filter( (item) => item.bookSourceUrl == selectRuleUrl ); // 提取待删除的源 let laveSources = RuleSources.filter( (item) => !(item.bookSourceUrl == selectRuleUrl) ); // 提取待留下的源 HttpPost(`/deleteBookSources`, deleteSources) .then((json) => { if (json.isSuccess) { let selectNode = document.getElementById(selectRuleUrl) .parentNode; selectNode.parentNode.removeChild(selectNode); localStorage.setItem( "debug@BookSources", JSON.stringify((RuleSources = laveSources)) ); if ($("#bookSourceUrl").value == selectRuleUrl) { $$(".rules textarea").forEach((item) => { item.value = ""; }); todo(); } console.log(deleteSources); console.log(`以上源已删除!`); } }) .catch((err) => { alert(`删除源失败,无法连接到「Reader」!\n${err}`); }); } break; case "ClrAll": if (confirm(`确定要清空当前源列表吗?\n(不会删除APP内源)`)) { localStorage.setItem( "debug@BookSources", JSON.stringify((RuleSources = [])) ); $("#RuleList").innerHTML = ""; } break; default: } }); ================================================ FILE: web/public/browsertest.html ================================================ ES5测试

ES5 特性测试结果:

================================================ FILE: web/public/index.html ================================================ 阅读
================================================ FILE: web/public/robots.txt ================================================ User-agent: * Disallow: ================================================ FILE: web/public/sw.js ================================================ self.addEventListener("message", event => { if (event.data && event.data.type === "CLEAR_HOME_CACHE") { self.caches.delete("home"); } }); workbox.routing.setDefaultHandler(async ({ event }) => { let { request, target } = event; // 处理首页请求 networkFirst try { const url = new URL(request.url); if ( url.origin === target.origin && (!url.pathname || url.pathname === "/") ) { const homeCache = await self.caches.open("home"); return fetch(request) .then(fetchRes => { if (fetchRes.type !== "opaque") { let resClone = fetchRes.clone(); homeCache.put(request, fetchRes); return resClone; } }) .catch(() => { return homeCache.match(request); }); } } catch (error) { // } // 判断是否有 precache const precache = await self.caches.open(workbox.core.cacheNames.precache); const precacheRes = await precache.match( workbox.precaching.getCacheKeyForURL(request.url) ); if (precacheRes) { return precacheRes; } // 处理api请求 networkOnly if (request.url.indexOf("/reader3/") !== -1) { return fetch(request); } const siteCache = await self.caches.open("SITE_CAHCE"); const opaqueCache = await self.caches.open("OPAQUE_CAHCE"); /** * * @param {Response} res */ const isImage = function(res) { const contentType = res.headers.get("Content-Type"); if (contentType && contentType.indexOf("image/") !== -1) { return true; } return false; }; /** * * @param {Response} res */ const isFont = function(res) { const contentType = res.headers.get("Content-Type"); if (contentType && contentType.indexOf("application/x-font") !== -1) { return true; } return false; }; // 通用请求,缓存 opaque 资源 和 图片资源 const doRequest = function(request) { var originRequest = request; // 站外资源去掉 referrer if ( request.mode !== "navigate" && request.url.indexOf(request.referrer) === -1 ) { // 站外资源强制缓存 request = new Request(request, { referrer: "" }); } // 对于不在 caches 中的资源进行请求 return fetch(request).then(fetchRes => { if (fetchRes.type === "opaque") { let resClone = fetchRes.clone(); opaqueCache.put(originRequest, fetchRes); return resClone; } // 这里只缓存成功 && 请求是 GET 方式的结果,对于 POST 等请求,可把 indexDB 给用上 if (!fetchRes || fetchRes.status !== 200 || request.method !== "GET") { return fetchRes; } // 只能缓存同源的图片、字体,跨域的资源都访问不了 let resClone = fetchRes.clone(); if (isImage(fetchRes) || isFont(fetchRes)) { siteCache.put(originRequest, fetchRes); } return resClone; }); }; // 先从 caches 中寻找是否有匹配 return opaqueCache.match(request).then(res => { if (res) { return res; } return siteCache.match(request).then(res => { if (res) { return res; } return doRequest(request); }); }); }); ================================================ FILE: web/src/App.vue ================================================ ================================================ FILE: web/src/assets/fonts/iconfont.css ================================================ @charset "UTF-8"; @font-face { font-family: "iconfont"; src: url("./iconfont.woff") format("woff"); } @font-face { font-family: "reader-iconfont"; /* Project id 2841133 */ src: url('./reader-iconfont.woff2?t=1657702223137') format('woff2'), url('./reader-iconfont.woff?t=1657702223137') format('woff'), url('./reader-iconfont.ttf?t=1657702223137') format('truetype'); } .reader-iconfont { font-family: "reader-iconfont" !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .reader-icon-shezhi:before { content: "\e654"; } .reader-icon-volume-off:before { content: "\e631"; } .reader-icon-volume:before { content: "\e632"; } .reader-icon-15s:before { content: "\e61f"; } .reader-icon-jian15s:before { content: "\e620"; } .reader-icon-player-pause:before { content: "\ea2b"; } .reader-icon-player-forward-step:before { content: "\ea2c"; } .reader-icon-player-backward-step:before { content: "\ea2d"; } .reader-icon-player-play:before { content: "\ea2e"; } ================================================ FILE: web/src/components/AddUser.vue ================================================ ================================================ FILE: web/src/components/BookGroup.vue ================================================ ================================================ FILE: web/src/components/BookInfo.vue ================================================ ================================================ FILE: web/src/components/BookManage.vue ================================================ ================================================ FILE: web/src/components/BookShelf.vue ================================================ ================================================ FILE: web/src/components/BookSource.vue ================================================ ================================================ FILE: web/src/components/Bookmark.vue ================================================ ================================================ FILE: web/src/components/BookmarkForm.vue ================================================ ================================================ FILE: web/src/components/Content.vue ================================================ ================================================ FILE: web/src/components/Explore.vue ================================================ ================================================ FILE: web/src/components/LocalStore.vue ================================================ ================================================ FILE: web/src/components/MPCode.vue ================================================ ================================================ FILE: web/src/components/PopCatalog.vue ================================================ ================================================ FILE: web/src/components/ReadSettings.vue ================================================ ================================================ FILE: web/src/components/ReplaceRule.vue ================================================ ================================================ FILE: web/src/components/ReplaceRuleForm.vue ================================================ ================================================ FILE: web/src/components/RssArticle.vue ================================================ ================================================ FILE: web/src/components/RssArticleList.vue ================================================ ================================================ FILE: web/src/components/RssSourceList.vue ================================================ ================================================ FILE: web/src/components/SearchBookContent.vue ================================================ ================================================ FILE: web/src/components/UserManage.vue ================================================ ================================================ FILE: web/src/components/WebDAV.vue ================================================ ================================================ FILE: web/src/main.js ================================================ import Vue from "vue"; import App from "./App.vue"; import router from "./router"; import "./plugins/element.js"; import store from "./plugins/vuex.js"; import "./plugins/md5.js"; import { registerServiceWorker } from "./registerServiceWorker"; import noCover from "./assets/imgs/noCover.jpeg"; import noImage from "./assets/imgs/noImage.png"; import VueLazyload from "vue-lazyload"; import { jsonEncode } from "./plugins/safe-json-stringify"; import localforage from "localforage"; try { // 设置全局错误收集 if (window.location.href.indexOf("errorAlert") > 0) { window.errorAlert = true; } window.onerror = function(event, source, lineno, colno, error) { if (window.errorAlert) { window.alert( jsonEncode({ event: event, source, lineno, colno, error: error }) ); } }; window.addEventListener("unhandledrejection", e => { if (window.errorAlert) { window.alert(jsonEncode(e)); } }); Vue.config.errorHandler = e => { if (window.errorAlert) { window.alert(jsonEncode(e)); } }; window.$cacheStorage = localforage.createInstance({ name: "cacheStorage" }); registerServiceWorker(); Vue.config.productionTip = false; Vue.use(VueLazyload, { observer: true }); Vue.mixin({ computed: { api() { return this.$store.getters.api; }, isWebApp() { return window.navigator.standalone; }, isPWA() { return ["fullscreen", "standalone", "minimal-ui"].some( displayMode => window.matchMedia("(display-mode: " + displayMode + ")").matches ); }, isNightTheme() { return this.$store.getters.isNight; }, currentUserName() { return this.$store.getters.currentUserName; } }, methods: { getImagePath(url, useSW) { if ( url && (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//")) ) { if (useSW && window.serviceWorkerReady) { return url; } return this.api + "/cover?path=" + url; } if (!url) return false; // 默认是接口服务器上的资源 return this.$store.getters.apiRoot + url; }, getCover(coverUrl, normal, useSW) { coverUrl = this.getImagePath(coverUrl, useSW); if (coverUrl) { return normal ? coverUrl : { src: coverUrl, error: noCover }; } return noCover; }, getImage(imageUrl, normal, useSW) { imageUrl = this.getImagePath(imageUrl, useSW); if (imageUrl) { return normal ? imageUrl : { src: imageUrl, error: noCover }; } return noImage; } } }); new Vue({ router, store, render: h => h(App) }).$mount("#app"); } catch (error) { alert(error.stack); } ================================================ FILE: web/src/plugins/animate.js ================================================ if (!window.requestAnimationFrame) { window.requestAnimationFrame = function(callback) { return setTimeout(callback, 1000 / 60); }; } // 动画执行函数 function Animate(options) { var start = Date.now(); window.requestAnimationFrame(function _animate() { // timeFraction 从 0 逐渐增加到 1 var timeFraction = (Date.now() - start) / options.duration; if (timeFraction > 1) timeFraction = 1; var progress = options.timing(timeFraction); // 动画当前进度 options.draw(progress); // 绘制动画 if (timeFraction < 1) { window.requestAnimationFrame(_animate); } else { options.onEnd && options.onEnd(); } }); } // 时序函数 Animate.Timings = { // 线性函数 linear: function(timeFraction) { return timeFraction; }, // 圆弧函数 circle: function(timeFraction) { return 1 - Math.sin(Math.acos(timeFraction)); }, // 圆弧函数(与上一个相同) circle2: function(timeFraction) { return 1 - (1 - timeFraction ** 2) ** 0.5; }, // 反-弹跳函数 bounce: function(timeFraction) { for (var a = 0, b = 1; (a += b), (b /= 2); ) { if (timeFraction >= (7 - 4 * a) / 11) { return ( -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2) ); } } }, // 幂函数,x 为指数 power: function(x, timeFraction) { return Math.pow(timeFraction, x); }, // 反弹函数,x 为弹性系数 back: function(x, timeFraction) { return Math.pow(timeFraction, 2) * ((x + 1) * timeFraction - x); }, // 伸缩函数,x 为初始范围 elastic: function(x, timeFraction) { return ( Math.pow(2, 10 * (timeFraction - 1)) * Math.cos(((20 * Math.PI * x) / 3) * timeFraction) ); } }; // 工具函数 Animate.Utils = { // 接受时序函数,返回时序函数的反函数 makeEaseOut: function(timing) { return function easeOut(timeFraction) { return 1 - timing(1 - timeFraction); }; }, // 接受时序函数,返回时序函数的 easeInOut 变体 makeEaseInOut: function(timing) { return function easeInOut(timeFraction) { if (timeFraction < 0.5) return timing(2 * timeFraction) / 2; else return 1 - timing(2 * (1 - timeFraction)) / 2; }; } }; export default Animate; ================================================ FILE: web/src/plugins/axios.js ================================================ import Axios from "axios"; import { Message, MessageBox } from "element-ui"; import { errorTypeList } from "./config"; import store from "./vuex"; const service = Axios.create({ baseURL: store.getters.api, withCredentials: true, timeout: 5 * 60 * 1000 }); store.watch( state => state.api, () => { service.defaults.baseURL = store.getters.api; } ); service.interceptors.request.use( config => config, error => { // console.log(error); // for debug return Promise.reject(error); } ); service.interceptors.response.use( response => { return response; }, error => { return Promise.reject(error); } ); let isShowLoginTip = false; export const request = async ({ url, method = "get", params = {}, data = {}, headers = {}, options = {}, alert }) => { // post 默认显示返回的信息 if (alert === undefined) { alert = method === "post"; } if (store.state.token) { params.accessToken = store.state.token; } if (store.state.isManagerMode && store.state.secureKey) { params.secureKey = store.state.secureKey; params.userNS = store.state.userNS; } // 防止 ie 缓存 GET 请求 params.v = new Date().getTime(); const query = { url, method, headers, params, data, ...options }; const response = await service(query).catch(e => { if (params.bookSourceUrl && store.state.failureIncludeTimeout) { // 判断是否失效书源 const errorMsg = e.toString(); window.errorMsgList = window.errorMsgList || []; window.errorMsgList.push(errorMsg); if (errorMsg.indexOf("timeout") >= 0) { store.commit("addFailureBookSource", { bookSourceUrl: params.bookSourceUrl, errorMsg }); } } throw e; }); if (!response) { return; } const res = response.data; const { isSuccess, errorMsg } = res; if (!isSuccess) { let result; switch (res.data) { case "NEED_LOGIN": // 需要登录 store.commit("setShowLogin", true); if (!isShowLoginTip) { isShowLoginTip = true; setTimeout(() => { errorMsg && Message.error({ message: errorMsg, duration: 2000 }); setTimeout(() => { isShowLoginTip = false; }, 2000); }, 200); } break; case "NEED_SECURE_KEY": result = await MessageBox.prompt( "请输入管理密码后继续操作", "操作确认" ); if (result && result.action === "confirm" && result.value) { params.secureKey = result.value; store.commit("setSecureKey", result.value); return await request({ url, method, params, data, headers, alert }); } break; default: if (params.bookSourceUrl) { // 判断是否失效书源 if (errorMsg) { window.errorMsgList = window.errorMsgList || []; window.errorMsgList.push(errorMsg); for (let index = 0; index < errorTypeList.length; index++) { if (errorMsg.indexOf(errorTypeList[index]) >= 0) { store.commit("addFailureBookSource", { bookSourceUrl: params.bookSourceUrl, errorMsg }); break; } } } } if (!options.silent) { errorMsg && Message.error({ message: errorMsg, duration: 2000 }); } break; } } else { alert && errorMsg && Message.success({ message: errorMsg, duration: 1500 }); } return response; }; request.get = async (url, options) => { options = options || {}; return await request({ url, method: "get", params: options.params || {}, options }); }; request.post = async (url, data, options) => { return await request({ url, data, method: "post", options }); }; export default request; ================================================ FILE: web/src/plugins/cache.js ================================================ export const setCache = (key, value) => { value = typeof value === "string" ? value : JSON.stringify(value); window.localStorage && window.localStorage.setItem(key, value); }; export const getCache = (key, defaultVal = null) => { let val = defaultVal; try { val = window.localStorage && window.localStorage.getItem(key); if (val === null) { return defaultVal; } if (val) { const parseVal = JSON.parse(val); if (parseVal !== null) { return parseVal; } } return val; } catch (error) { return val; } }; ================================================ FILE: web/src/plugins/chinese.js ================================================ let scStr = "皑蔼碍爱翱袄奥坝罢摆败颁办绊帮绑镑谤剥饱宝报鲍辈贝钡狈备惫绷笔毕毙闭边编贬变辩辫鳖瘪濒滨宾摈饼拨钵铂驳卜补参蚕残惭惨灿苍舱仓沧厕侧册测层诧搀掺蝉馋谗缠铲产阐颤场尝长偿肠厂畅钞车彻尘陈衬撑称惩诚骋痴迟驰耻齿炽冲虫宠畴踌筹绸丑橱厨锄雏础储触处传疮闯创锤纯绰辞词赐聪葱囱从丛凑窜错达带贷担单郸掸胆惮诞弹当挡党荡档捣岛祷导盗灯邓敌涤递缔点垫电淀钓调迭谍叠钉顶锭订东动栋冻斗犊独读赌镀锻断缎兑队对吨顿钝夺鹅额讹恶饿儿尔饵贰发罚阀珐矾钒烦范贩饭访纺飞废费纷坟奋愤粪丰枫锋风疯冯缝讽凤肤辐抚辅赋复负讣妇缚该钙盖干赶秆赣冈刚钢纲岗皋镐搁鸽阁铬个给龚宫巩贡钩沟构购够蛊顾剐关观馆惯贯广规硅归龟闺轨诡柜贵刽辊滚锅国过骇韩汉阂鹤贺横轰鸿红后壶护沪户哗华画划话怀坏欢环还缓换唤痪焕涣黄谎挥辉毁贿秽会烩汇讳诲绘荤浑伙获货祸击机积饥讥鸡绩缉极辑级挤几蓟剂济计记际继纪夹荚颊贾钾价驾歼监坚笺间艰缄茧检碱硷拣捡简俭减荐槛鉴践贱见键舰剑饯渐溅涧浆蒋桨奖讲酱胶浇骄娇搅铰矫侥脚饺缴绞轿较秸阶节茎惊经颈静镜径痉竞净纠厩旧驹举据锯惧剧鹃绢杰洁结诫届紧锦仅谨进晋烬尽劲荆觉决诀绝钧军骏开凯颗壳课垦恳抠库裤夸块侩宽矿旷况亏岿窥馈溃扩阔蜡腊莱来赖蓝栏拦篮阑兰澜谰揽览懒缆烂滥捞劳涝乐镭垒类泪篱离里鲤礼丽厉励砾历沥隶俩联莲连镰怜涟帘敛脸链恋炼练粮凉两辆谅疗辽镣猎临邻鳞凛赁龄铃凌灵岭领馏刘龙聋咙笼垄拢陇楼娄搂篓芦卢颅庐炉掳卤虏鲁赂禄录陆驴吕铝侣屡缕虑滤绿峦挛孪滦乱抡轮伦仑沦纶论萝罗逻锣箩骡骆络妈玛码蚂马骂吗买麦卖迈脉瞒馒蛮满谩猫锚铆贸么霉没镁门闷们锰梦谜弥觅绵缅庙灭悯闽鸣铭谬谋亩钠纳难挠脑恼闹馁腻撵捻酿鸟聂啮镊镍柠狞宁拧泞钮纽脓浓农疟诺欧鸥殴呕沤盘庞国爱赔喷鹏骗飘频贫苹凭评泼颇扑铺朴谱脐齐骑岂启气弃讫牵扦钎铅迁签谦钱钳潜浅谴堑枪呛墙蔷强抢锹桥乔侨翘窍窃钦亲轻氢倾顷请庆琼穷趋区躯驱龋颧权劝却鹊让饶扰绕热韧认纫荣绒软锐闰润洒萨鳃赛伞丧骚扫涩杀纱筛晒闪陕赡缮伤赏烧绍赊摄慑设绅审婶肾渗声绳胜圣师狮湿诗尸时蚀实识驶势释饰视试寿兽枢输书赎属术树竖数帅双谁税顺说硕烁丝饲耸怂颂讼诵擞苏诉肃虽绥岁孙损笋缩琐锁獭挞抬摊贪瘫滩坛谭谈叹汤烫涛绦腾誊锑题体屉条贴铁厅听烃铜统头图涂团颓蜕脱鸵驮驼椭洼袜弯湾顽万网韦违围为潍维苇伟伪纬谓卫温闻纹稳问瓮挝蜗涡窝呜钨乌诬无芜吴坞雾务误锡牺袭习铣戏细虾辖峡侠狭厦锨鲜纤咸贤衔闲显险现献县馅羡宪线厢镶乡详响项萧销晓啸蝎协挟携胁谐写泻谢锌衅兴汹锈绣虚嘘须许绪续轩悬选癣绚学勋询寻驯训讯逊压鸦鸭哑亚讶阉烟盐严颜阎艳厌砚彦谚验鸯杨扬疡阳痒养样瑶摇尧遥窑谣药爷页业叶医铱颐遗仪彝蚁艺亿忆义诣议谊译异绎荫阴银饮樱婴鹰应缨莹萤营荧蝇颖哟拥佣痈踊咏涌优忧邮铀犹游诱舆鱼渔娱与屿语吁御狱誉预驭鸳渊辕园员圆缘远愿约跃钥岳粤悦阅云郧匀陨运蕴酝晕韵杂灾载攒暂赞赃脏凿枣灶责择则泽贼赠扎札轧铡闸诈斋债毡盏斩辗崭栈战绽张涨帐账胀赵蛰辙锗这贞针侦诊镇阵挣睁狰帧郑证织职执纸挚掷帜质钟终种肿众诌轴皱昼骤猪诸诛烛瞩嘱贮铸筑驻专砖转赚桩庄装妆壮状锥赘坠缀谆浊兹资渍踪综总纵邹诅组钻致钟么为只凶准启板里雳余链泄"; let tcStr = "皚藹礙愛翺襖奧壩罷擺敗頒辦絆幫綁鎊謗剝飽寶報鮑輩貝鋇狽備憊繃筆畢斃閉邊編貶變辯辮鼈癟瀕濱賓擯餅撥缽鉑駁蔔補參蠶殘慚慘燦蒼艙倉滄廁側冊測層詫攙摻蟬饞讒纏鏟産闡顫場嘗長償腸廠暢鈔車徹塵陳襯撐稱懲誠騁癡遲馳恥齒熾沖蟲寵疇躊籌綢醜櫥廚鋤雛礎儲觸處傳瘡闖創錘純綽辭詞賜聰蔥囪從叢湊竄錯達帶貸擔單鄲撣膽憚誕彈當擋黨蕩檔搗島禱導盜燈鄧敵滌遞締點墊電澱釣調叠諜疊釘頂錠訂東動棟凍鬥犢獨讀賭鍍鍛斷緞兌隊對噸頓鈍奪鵝額訛惡餓兒爾餌貳發罰閥琺礬釩煩範販飯訪紡飛廢費紛墳奮憤糞豐楓鋒風瘋馮縫諷鳳膚輻撫輔賦複負訃婦縛該鈣蓋幹趕稈贛岡剛鋼綱崗臯鎬擱鴿閣鉻個給龔宮鞏貢鈎溝構購夠蠱顧剮關觀館慣貫廣規矽歸龜閨軌詭櫃貴劊輥滾鍋國過駭韓漢閡鶴賀橫轟鴻紅後壺護滬戶嘩華畫劃話懷壞歡環還緩換喚瘓煥渙黃謊揮輝毀賄穢會燴彙諱誨繪葷渾夥獲貨禍擊機積饑譏雞績緝極輯級擠幾薊劑濟計記際繼紀夾莢頰賈鉀價駕殲監堅箋間艱緘繭檢堿鹼揀撿簡儉減薦檻鑒踐賤見鍵艦劍餞漸濺澗漿蔣槳獎講醬膠澆驕嬌攪鉸矯僥腳餃繳絞轎較稭階節莖驚經頸靜鏡徑痙競淨糾廄舊駒舉據鋸懼劇鵑絹傑潔結誡屆緊錦僅謹進晉燼盡勁荊覺決訣絕鈞軍駿開凱顆殼課墾懇摳庫褲誇塊儈寬礦曠況虧巋窺饋潰擴闊蠟臘萊來賴藍欄攔籃闌蘭瀾讕攬覽懶纜爛濫撈勞澇樂鐳壘類淚籬離裏鯉禮麗厲勵礫曆瀝隸倆聯蓮連鐮憐漣簾斂臉鏈戀煉練糧涼兩輛諒療遼鐐獵臨鄰鱗凜賃齡鈴淩靈嶺領餾劉龍聾嚨籠壟攏隴樓婁摟簍蘆盧顱廬爐擄鹵虜魯賂祿錄陸驢呂鋁侶屢縷慮濾綠巒攣孿灤亂掄輪倫侖淪綸論蘿羅邏鑼籮騾駱絡媽瑪碼螞馬罵嗎買麥賣邁脈瞞饅蠻滿謾貓錨鉚貿麽黴沒鎂門悶們錳夢謎彌覓綿緬廟滅憫閩鳴銘謬謀畝鈉納難撓腦惱鬧餒膩攆撚釀鳥聶齧鑷鎳檸獰甯擰濘鈕紐膿濃農瘧諾歐鷗毆嘔漚盤龐國愛賠噴鵬騙飄頻貧蘋憑評潑頗撲鋪樸譜臍齊騎豈啓氣棄訖牽扡釺鉛遷簽謙錢鉗潛淺譴塹槍嗆牆薔強搶鍬橋喬僑翹竅竊欽親輕氫傾頃請慶瓊窮趨區軀驅齲顴權勸卻鵲讓饒擾繞熱韌認紉榮絨軟銳閏潤灑薩鰓賽傘喪騷掃澀殺紗篩曬閃陝贍繕傷賞燒紹賒攝懾設紳審嬸腎滲聲繩勝聖師獅濕詩屍時蝕實識駛勢釋飾視試壽獸樞輸書贖屬術樹豎數帥雙誰稅順說碩爍絲飼聳慫頌訟誦擻蘇訴肅雖綏歲孫損筍縮瑣鎖獺撻擡攤貪癱灘壇譚談歎湯燙濤縧騰謄銻題體屜條貼鐵廳聽烴銅統頭圖塗團頹蛻脫鴕馱駝橢窪襪彎灣頑萬網韋違圍爲濰維葦偉僞緯謂衛溫聞紋穩問甕撾蝸渦窩嗚鎢烏誣無蕪吳塢霧務誤錫犧襲習銑戲細蝦轄峽俠狹廈鍁鮮纖鹹賢銜閑顯險現獻縣餡羨憲線廂鑲鄉詳響項蕭銷曉嘯蠍協挾攜脅諧寫瀉謝鋅釁興洶鏽繡虛噓須許緒續軒懸選癬絢學勳詢尋馴訓訊遜壓鴉鴨啞亞訝閹煙鹽嚴顔閻豔厭硯彥諺驗鴦楊揚瘍陽癢養樣瑤搖堯遙窯謠藥爺頁業葉醫銥頤遺儀彜蟻藝億憶義詣議誼譯異繹蔭陰銀飲櫻嬰鷹應纓瑩螢營熒蠅穎喲擁傭癰踴詠湧優憂郵鈾猶遊誘輿魚漁娛與嶼語籲禦獄譽預馭鴛淵轅園員圓緣遠願約躍鑰嶽粵悅閱雲鄖勻隕運蘊醞暈韻雜災載攢暫贊贓髒鑿棗竈責擇則澤賊贈紮劄軋鍘閘詐齋債氈盞斬輾嶄棧戰綻張漲帳賬脹趙蟄轍鍺這貞針偵診鎮陣掙睜猙幀鄭證織職執紙摯擲幟質鍾終種腫衆謅軸皺晝驟豬諸誅燭矚囑貯鑄築駐專磚轉賺樁莊裝妝壯狀錐贅墜綴諄濁茲資漬蹤綜總縱鄒詛組鑽緻鐘麼為隻兇準啟闆裡靂餘鍊洩"; // 构建映射字典 const scToTcMap = {}; const tcToScMap = {}; for (let i = 0; i < scStr.length; i++) { scToTcMap[scStr[i]] = tcStr[i]; tcToScMap[tcStr[i]] = scStr[i]; } // 释放内存 scStr = undefined; tcStr = undefined; export const traditionalized = function(orgStr) { let str = ""; for (let i = 0; i < orgStr.length; i++) { const char = orgStr[i]; if (char.charCodeAt(0) > 10000) { str += scToTcMap[char] || char; } else { str += char; } } return str; }; export const simplized = function(orgStr) { let str = ""; for (let i = 0; i < orgStr.length; i++) { const char = orgStr[i]; if (char.charCodeAt(0) > 10000) { str += tcToScMap[char] || char; } else { str += char; } } return str; }; ================================================ FILE: web/src/plugins/config.js ================================================ import body_0 from "../assets/imgs/themes/body_0.png"; import content_0 from "../assets/imgs/themes/content_0.png"; import popup_0 from "../assets/imgs/themes/popup_0.png"; import body_1 from "../assets/imgs/themes/body_1.png"; import content_1 from "../assets/imgs/themes/content_1.png"; import popup_1 from "../assets/imgs/themes/popup_1.png"; import body_2 from "../assets/imgs/themes/body_2.png"; import content_2 from "../assets/imgs/themes/content_2.png"; import popup_2 from "../assets/imgs/themes/popup_2.png"; import body_3 from "../assets/imgs/themes/body_3.png"; import content_3 from "../assets/imgs/themes/content_3.png"; import popup_3 from "../assets/imgs/themes/popup_3.png"; import body_5 from "../assets/imgs/themes/body_5.png"; import content_5 from "../assets/imgs/themes/content_5.png"; import popup_5 from "../assets/imgs/themes/popup_5.png"; import body_6 from "../assets/imgs/themes/body_6.png"; import content_6 from "../assets/imgs/themes/content_6.png"; // import popup_6 from "../assets/imgs/themes/popup_6.png"; const defaultDayConfig = { configDefaultType: "白天默认", name: "内置白天", theme: 0, font: 0, chineseFont: "简体", fontSize: 18, fontWeight: 400, fontColor: "#262626", bodyColor: "#eadfca", contentColor: "#fff", popupColor: "#ede7da", themeType: "day", readMethod: "上下滑动", clickMethod: "自动", animateMSTime: 300, // 翻页动画时长 readWidth: 800, lineHeight: 1.8, // 行高 paragraphSpace: 0.2, // 段间距 autoReadingMethod: "像素滚动", autoReadingPixel: 1, autoReadingLineTime: 1000, pageMode: "自适应", selectionAction: "操作弹窗" }; const defaultNightConfig = { configDefaultType: "黑夜默认", name: "内置黑夜", theme: 6, font: 0, chineseFont: "简体", fontSize: 18, fontWeight: 400, fontColor: "#666666", bodyColor: "#121212", contentColor: "#171717", popupColor: "#121212", themeType: "night", readMethod: "上下滑动", clickMethod: "自动", animateMSTime: 300, // 翻页动画时长 readWidth: 800, lineHeight: 1.8, // 行高 paragraphSpace: 0.2, // 段间距 autoReadingMethod: "像素滚动", autoReadingPixel: 1, autoReadingLineTime: 1000, pageMode: "自适应", selectionAction: "操作弹窗" }; const settings = { shelfConfig: { showBookGroup: -1 }, searchConfig: { searchType: "multi", bookSourceGroup: "", bookSourceUrl: "", concurrentCount: 24 }, customConfigList: [defaultDayConfig, defaultNightConfig], config: { ...defaultDayConfig, customConfig: "内置白天", autoTheme: true, // 自动切换主题 pageType: "正常" }, speechVoiceConfig: { voiceName: "", speechRate: 1, speechPitch: 1 }, defaultNightTheme: 6, themes: [ { body: "url(" + body_0 + ") repeat", content: "url(" + content_0 + ") repeat", popup: "url(" + popup_0 + ") repeat" }, { body: "url(" + body_1 + ") repeat", content: "url(" + content_1 + ") repeat", popup: "url(" + popup_1 + ") repeat" }, { body: "url(" + body_2 + ") repeat", content: "url(" + content_2 + ") repeat", popup: "url(" + popup_2 + ") repeat" }, { body: "url(" + body_3 + ") repeat", content: "url(" + content_3 + ") repeat", popup: "url(" + popup_3 + ") repeat" }, { body: "#ebcece repeat", content: "#f5e4e4 repeat", popup: "#faeceb repeat" }, { body: "url(" + body_5 + ") repeat", content: "url(" + content_5 + ") repeat", popup: "url(" + popup_5 + ") repeat" }, { body: "url(" + body_6 + ") repeat", content: "url(" + content_6 + ") repeat", popup: "#121212" }, { body: "#f7f7f7 repeat", content: "#fff repeat", popup: "#f7f7f7 repeat" } ], fonts: [ { fontFamily: "custom-system" }, // 黑体 { // fontFamily: // '-apple-system, "Noto Sans", "Helvetica Neue", Helvetica, "Nimbus Sans L", Arial, "Liberation Sans", "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", "Source Han Sans SC", "Source Han Sans CN", "Microsoft YaHei", "Wenquanyi Micro Hei", "WenQuanYi Zen Hei", "ST Heiti", SimHei, "WenQuanYi Zen Hei Sharp", sans-serif' fontFamily: "custom-ht, reader-ht" }, // 楷体 { // fontFamily: // 'Baskerville, Georgia, "Liberation Serif", "Kaiti SC", STKaiti, "AR PL UKai CN", "AR PL UKai HK", "AR PL UKai TW", "AR PL UKai TW MBE", "AR PL KaitiM GB", KaiTi, KaiTi_GB2312, DFKai-SB, "TW-Kai", serif', fontFamily: "custom-kt, reader-kt" // fontFamily: "STKaiti", // "-fx-font-family": "STKaiti" }, // 宋体 { // fontFamily: // 'Georgia, "Nimbus Roman No9 L", "Songti SC", "Noto Serif CJK SC", "Source Han Serif SC", "Source Han Serif CN", STSong, "AR PL New Sung", "AR PL SungtiL GB", NSimSun, SimSun, "TW-Sung", "WenQuanYi Bitmap Song", "AR PL UMing CN", "AR PL UMing HK", "AR PL UMing TW", "AR PL UMing TW MBE", PMingLiU, MingLiU, serif', fontFamily: "custom-st, reader-st" // fontFamily: "'Source Han Serif CN'", // "-fx-font-family": "'Source Han Serif CN'" }, // 仿宋 { // fontFamily: // 'Baskerville, "Times New Roman", "Liberation Serif", STFangsong, FangSong, FangSong_GB2312, "CWTEX-F", serif', fontFamily: "custom-fs, reader-fs" // fontFamily: "STFangsong", // "-fx-font-family": "STFangsong" } ] }; export const errorTypeList = [ "UnknownHostException", "ConnectException: Failed to connect", "SocketException: Connection reset", "SSLHandshakeException", "responseCode: 307", "responseCode: 400", "responseCode: 403", "responseCode: 404", "responseCode: 500", "responseCode: 502", "responseCode: 503", "responseCode: 504", "responseCode: 513" ]; export const defaultReplaceRule = { name: "", pattern: "", replacement: "", scope: "", isRegex: false, isEnabled: true }; export const defaultBookmark = { bookName: "", bookAuthor: "", chapterIndex: 0, chapterPos: 0, chapterName: "", bookText: "", content: "" }; export const syncConfigFiled = Object.keys(defaultDayConfig).concat([ "contentBGImg" ]); export const customFonts = [ "custom-system", "custom-ht", "custom-kt", "custom-st", "custom-fs" ]; export default settings; ================================================ FILE: web/src/plugins/element.js ================================================ import Vue from "vue"; import { Button, Divider, MessageBox, Message, Breadcrumb, BreadcrumbItem, Table, TableColumn, Popover, Loading, Input, Select, Option, Tag, Collapse, CollapseItem, Dialog, Checkbox, CheckboxGroup, ColorPicker, Slider, Form, FormItem, Switch, Link, RadioGroup, RadioButton, Pagination, InputNumber, Image, Badge, Tabs, TabPane, Dropdown, DropdownItem, DropdownMenu } from "element-ui"; Vue.use(Button); Vue.use(Divider); Vue.use(Breadcrumb); Vue.use(BreadcrumbItem); Vue.use(Table); Vue.use(TableColumn); Vue.use(Popover); Vue.use(Input); Vue.use(Select); Vue.use(Option); Vue.use(Tag); Vue.use(Loading.directive); Vue.use(Collapse); Vue.use(CollapseItem); Vue.use(Dialog); Vue.use(Checkbox); Vue.use(CheckboxGroup); Vue.use(ColorPicker); Vue.use(Slider); Vue.use(Form); Vue.use(FormItem); Vue.use(Switch); Vue.use(Link); Vue.use(RadioGroup); Vue.use(RadioButton); Vue.use(Pagination); Vue.use(InputNumber); Vue.use(Image); Vue.use(Badge); Vue.use(Tabs); Vue.use(TabPane); Vue.use(Dropdown); Vue.use(DropdownItem); Vue.use(DropdownMenu); Vue.prototype.$msgbox = MessageBox; Vue.prototype.$message = Object.assign({}, Message, { info(message, duration) { const options = typeof message === "string" ? { message } : message; options.duration = duration || 1000; Message.info(options); }, error(message, duration) { const options = typeof message === "string" ? { message } : message; options.duration = duration || 2000; Message.error(options); }, success(message, duration) { const options = typeof message === "string" ? { message } : message; options.duration = duration || 1000; Message.success(options); } }); Vue.prototype.$ELEMENT = { zIndex: 2100 }; Vue.prototype.$alert = MessageBox.alert; Vue.prototype.$confirm = MessageBox.confirm; Vue.prototype.$prompt = MessageBox.prompt; Vue.prototype.$loading = Loading.service; ================================================ FILE: web/src/plugins/eventBus.js ================================================ import Vue from "vue"; export default new Vue(); ================================================ FILE: web/src/plugins/helper.js ================================================ // import { Message } from "element-ui"; import { getCache } from "../plugins/cache"; export const formatSize = function(value, scale) { if (value == null || value == "") { return "0 Bytes"; } var unitArr = new Array( "Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" ); var index = 0; index = Math.floor(Math.log(value) / Math.log(1024)); var size = value / Math.pow(1024, index); size = size.toFixed(scale || 2); return size + " " + unitArr[index]; }; export const LimitResquest = function(limit, process) { let currentSum = 0; let requests = []; async function run() { let err, result; try { ++currentSum; handler.leftCount = requests.length; const fn = requests.shift(); result = await fn(); } catch (error) { err = error; // console.log("Error", err); handler.errorCount++; } finally { --currentSum; handler.requestCount++; handler.leftCount = requests.length; process && process(handler, result, err); if (requests.length > 0) { run(); } } } const handler = reqFn => { if (!reqFn || !(reqFn instanceof Function)) { return; } requests.push(reqFn); handler.leftCount = requests.length; if (currentSum < limit) { run(); } }; handler.requestCount = 0; handler.leftCount = 0; handler.errorCount = 0; handler.cancel = () => { requests = []; }; handler.isEnd = () => { return !handler.leftCount && !currentSum; }; return handler; }; export const networkFirstRequest = async function(requestFunc, cacheKey) { cacheKey = "localCache@" + cacheKey; const res = await requestFunc().catch(() => { // 请求出错,使用缓存 // 使用新的异步存储 return window.$cacheStorage .getItem(cacheKey) .then(cacheResponse => { if (cacheResponse) { return { data: cacheResponse }; } }) .catch(err => { // 兼容旧逻辑 const cacheResponse = getCache(cacheKey); if (cacheResponse) { return { data: cacheResponse }; } throw err; }); }); if (res.data && res.data.isSuccess) { // 使用新的异步存储 window.$cacheStorage.setItem(cacheKey, res.data).catch(() => {}); } return res; }; export const cacheFirstRequest = async function( requestFunc, cacheKey, validateCache ) { cacheKey = "localCache@" + cacheKey; // validateCache === true 时,直接刷新缓存 if (validateCache !== true) { let cacheResponse = await window.$cacheStorage .getItem(cacheKey) .then(cacheResponse => { if (cacheResponse) { return cacheResponse; } // console.log("Cache not found in new cache"); throw new Error("Cache not found"); }) .catch(() => { // 兼容旧逻辑 const cacheResponse = getCache(cacheKey); return cacheResponse; }); if (cacheResponse) { if (!validateCache || (validateCache && validateCache(cacheResponse))) { return { data: cacheResponse }; } } } const res = await requestFunc(); if (res.data && res.data.isSuccess) { // 使用新的异步存储 window.$cacheStorage.setItem(cacheKey, res.data).catch(() => {}); } return res; }; export const isMiniInterface = () => window.innerWidth <= 750; export const editDistance = function(strA, strB) { // Levenshtein Edit Distance if (strA === strB) { return 1.0; } if (!strA || !strB) { return 0.0; } const arr = new Array(strA.length + 1); for (let i1 = 0; i1 <= strA.length; i1++) { arr[i1] = new Array(strB.length + 1); } for (let i1 = 0; i1 <= strA.length; i1++) { for (let i2 = 0; i2 <= strB.length; i2++) { if (i1 === 0) { arr[0][i2] = i2; } else if (i2 === 0) { arr[i1][0] = i1; } else if (strA.charAt(i1 - 1) === strB.charAt(i2 - 1)) { arr[i1][i2] = arr[i1 - 1][i2 - 1]; } else { arr[i1][i2] = 1 + Math.min( arr[i1 - 1][i2 - 1], Math.min(arr[i1][i2 - 1], arr[i1 - 1][i2]) ); } } } return 1 - arr[strA.length][strB.length] / Math.max(strA.length, strB.length); }; export const loadFont = function(fontName, fontUrl) { window.customFonts = window.customFonts || {}; if ( !window.customFonts[fontName] || window.customFonts[fontName] !== fontUrl ) { // 动态插入CSS const style = document.createElement("style"); style.textContent = ` @font-face { font-family: "${fontName}"; src: url("${fontUrl}"); }`; style.id = "custom-font-" + fontName; document.head.appendChild(style); window.customFonts[fontName] = fontUrl; } }; export const removeFont = function(fontName) { window.customFonts = window.customFonts || {}; delete window.customFonts[fontName]; const nodeList = document.querySelectorAll("#custom-font-" + fontName); for (let i = 0; i < nodeList.length; i++) { const node = nodeList[i]; node.remove(); } }; ================================================ FILE: web/src/plugins/jump.js ================================================ const easeInOutQuad = (t, b, c, d) => { t /= d / 2; if (t < 1) return (c / 2) * t * t + b; t--; return (-c / 2) * (t * (t - 2) - 1) + b; }; const jumper = () => { // private variable cache // no variables are created during a jump, preventing memory leaks let container; // container element to be scrolled (node) let element; // element to scroll to (node) let start; // where scroll starts (px) let stop; // where scroll stops (px) let offset; // adjustment from the stop position (px) let easing; // easing function (function) let a11y; // accessibility support flag (boolean) let distance; // distance of scroll (px) let duration; // scroll duration (ms) let timeStart; // time scroll started (ms) let timeElapsed; // time spent scrolling thus far (ms) let next; // next scroll position (px) let callback; // to call when done scrolling (function) // scroll position helper function location() { let top = container.scrollTop || container.scrollY || container.pageYOffset; top = typeof top === "undefined" ? 0 : top; return top; } // element offset helper function top(element) { const elementTop = element.getBoundingClientRect().top; const containerTop = container.getBoundingClientRect ? container.getBoundingClientRect().top : 0; return elementTop - containerTop + start; } // scrollTo helper function scrollTo(top) { container.scrollTo ? container.scrollTo(0, top) // window : (container.scrollTop = top); // custom container } // rAF loop helper function loop(timeCurrent) { // store time scroll started, if not started already if (!timeStart) { timeStart = timeCurrent; } // determine time spent scrolling so far timeElapsed = timeCurrent - timeStart; // calculate next scroll position next = easing(timeElapsed, start, distance, duration); // scroll to it scrollTo(next); // check progress timeElapsed < duration ? requestAnimationFrame(loop) // continue scroll loop : done(); // scrolling is done } // scroll finished helper function done() { // account for rAF time rounding inaccuracies scrollTo(start + distance); // if scrolling to an element, and accessibility is enabled if (element && a11y) { // add tabindex indicating programmatic focus element.setAttribute("tabindex", "-1"); // focus the element element.focus(); } // if it exists, fire the callback if (typeof callback === "function") { callback(); } // reset time for next jump timeStart = false; } // API function jump(target, options = {}) { // resolve options, or use defaults duration = options.duration || 1000; offset = options.offset || 0; callback = options.callback; // "undefined" is a suitable default, and won't be called easing = options.easing || easeInOutQuad; a11y = options.a11y || false; // resolve container switch (typeof options.container) { case "object": // we assume container is an HTML element (Node) container = options.container; break; case "string": container = document.querySelector(options.container); break; default: container = window; } // cache starting position start = location(); // resolve target switch (typeof target) { // scroll from current position case "number": element = undefined; // no element to scroll to a11y = false; // make sure accessibility is off stop = start + target; break; // scroll to element (node) // bounding rect is relative to the viewport case "object": element = target; stop = top(element); break; // scroll to element (selector) // bounding rect is relative to the viewport case "string": element = document.querySelector(target); stop = top(element); break; } // resolve scroll distance, accounting for offset distance = stop - start + offset; // resolve duration switch (typeof options.duration) { // number in ms case "number": duration = options.duration; break; // function passed the distance of the scroll case "function": duration = options.duration(distance); break; } // start the loop requestAnimationFrame(loop); } // expose only the jump method return jump; }; // export singleton const singleton = jumper(); export default singleton; ================================================ FILE: web/src/plugins/md5.js ================================================ /* md5.js */ String.prototype.MD5 = function(bit) { var sMessage = this; function RotateLeft(lValue, iShiftBits) { return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits)); } function AddUnsigned(lX, lY) { var lX4, lY4, lX8, lY8, lResult; lX8 = lX & 0x80000000; lY8 = lY & 0x80000000; lX4 = lX & 0x40000000; lY4 = lY & 0x40000000; lResult = (lX & 0x3fffffff) + (lY & 0x3fffffff); if (lX4 & lY4) return lResult ^ 0x80000000 ^ lX8 ^ lY8; if (lX4 | lY4) { if (lResult & 0x40000000) return lResult ^ 0xc0000000 ^ lX8 ^ lY8; else return lResult ^ 0x40000000 ^ lX8 ^ lY8; } else return lResult ^ lX8 ^ lY8; } function F(x, y, z) { return (x & y) | (~x & z); } function G(x, y, z) { return (x & z) | (y & ~z); } function H(x, y, z) { return x ^ y ^ z; } function I(x, y, z) { return y ^ (x | ~z); } function FF(a, b, c, d, x, s, ac) { a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac)); return AddUnsigned(RotateLeft(a, s), b); } function GG(a, b, c, d, x, s, ac) { a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac)); return AddUnsigned(RotateLeft(a, s), b); } function HH(a, b, c, d, x, s, ac) { a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac)); return AddUnsigned(RotateLeft(a, s), b); } function II(a, b, c, d, x, s, ac) { a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac)); return AddUnsigned(RotateLeft(a, s), b); } function ConvertToWordArray(sMessage) { var lWordCount; var lMessageLength = sMessage.length; var lNumberOfWords_temp1 = lMessageLength + 8; var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64; var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16; var lWordArray = Array(lNumberOfWords - 1); var lBytePosition = 0; var lByteCount = 0; while (lByteCount < lMessageLength) { lWordCount = (lByteCount - (lByteCount % 4)) / 4; lBytePosition = (lByteCount % 4) * 8; lWordArray[lWordCount] = lWordArray[lWordCount] | (sMessage.charCodeAt(lByteCount) << lBytePosition); lByteCount++; } lWordCount = (lByteCount - (lByteCount % 4)) / 4; lBytePosition = (lByteCount % 4) * 8; lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition); lWordArray[lNumberOfWords - 2] = lMessageLength << 3; lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29; return lWordArray; } function WordToHex(lValue) { var WordToHexValue = "", WordToHexValue_temp = "", lByte, lCount; for (lCount = 0; lCount <= 3; lCount++) { lByte = (lValue >>> (lCount * 8)) & 255; WordToHexValue_temp = "0" + lByte.toString(16); WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length - 2, 2); } return WordToHexValue; } var x = Array(); var k, AA, BB, CC, DD, a, b, c, d; var S11 = 7, S12 = 12, S13 = 17, S14 = 22; var S21 = 5, S22 = 9, S23 = 14, S24 = 20; var S31 = 4, S32 = 11, S33 = 16, S34 = 23; var S41 = 6, S42 = 10, S43 = 15, S44 = 21; // Steps 1 and 2. Append padding bits and length and convert to words x = ConvertToWordArray(sMessage); // Step 3. Initialise a = 0x67452301; b = 0xefcdab89; c = 0x98badcfe; d = 0x10325476; // Step 4. Process the message in 16-word blocks for (k = 0; k < x.length; k += 16) { AA = a; BB = b; CC = c; DD = d; a = FF(a, b, c, d, x[k + 0], S11, 0xd76aa478); d = FF(d, a, b, c, x[k + 1], S12, 0xe8c7b756); c = FF(c, d, a, b, x[k + 2], S13, 0x242070db); b = FF(b, c, d, a, x[k + 3], S14, 0xc1bdceee); a = FF(a, b, c, d, x[k + 4], S11, 0xf57c0faf); d = FF(d, a, b, c, x[k + 5], S12, 0x4787c62a); c = FF(c, d, a, b, x[k + 6], S13, 0xa8304613); b = FF(b, c, d, a, x[k + 7], S14, 0xfd469501); a = FF(a, b, c, d, x[k + 8], S11, 0x698098d8); d = FF(d, a, b, c, x[k + 9], S12, 0x8b44f7af); c = FF(c, d, a, b, x[k + 10], S13, 0xffff5bb1); b = FF(b, c, d, a, x[k + 11], S14, 0x895cd7be); a = FF(a, b, c, d, x[k + 12], S11, 0x6b901122); d = FF(d, a, b, c, x[k + 13], S12, 0xfd987193); c = FF(c, d, a, b, x[k + 14], S13, 0xa679438e); b = FF(b, c, d, a, x[k + 15], S14, 0x49b40821); a = GG(a, b, c, d, x[k + 1], S21, 0xf61e2562); d = GG(d, a, b, c, x[k + 6], S22, 0xc040b340); c = GG(c, d, a, b, x[k + 11], S23, 0x265e5a51); b = GG(b, c, d, a, x[k + 0], S24, 0xe9b6c7aa); a = GG(a, b, c, d, x[k + 5], S21, 0xd62f105d); d = GG(d, a, b, c, x[k + 10], S22, 0x2441453); c = GG(c, d, a, b, x[k + 15], S23, 0xd8a1e681); b = GG(b, c, d, a, x[k + 4], S24, 0xe7d3fbc8); a = GG(a, b, c, d, x[k + 9], S21, 0x21e1cde6); d = GG(d, a, b, c, x[k + 14], S22, 0xc33707d6); c = GG(c, d, a, b, x[k + 3], S23, 0xf4d50d87); b = GG(b, c, d, a, x[k + 8], S24, 0x455a14ed); a = GG(a, b, c, d, x[k + 13], S21, 0xa9e3e905); d = GG(d, a, b, c, x[k + 2], S22, 0xfcefa3f8); c = GG(c, d, a, b, x[k + 7], S23, 0x676f02d9); b = GG(b, c, d, a, x[k + 12], S24, 0x8d2a4c8a); a = HH(a, b, c, d, x[k + 5], S31, 0xfffa3942); d = HH(d, a, b, c, x[k + 8], S32, 0x8771f681); c = HH(c, d, a, b, x[k + 11], S33, 0x6d9d6122); b = HH(b, c, d, a, x[k + 14], S34, 0xfde5380c); a = HH(a, b, c, d, x[k + 1], S31, 0xa4beea44); d = HH(d, a, b, c, x[k + 4], S32, 0x4bdecfa9); c = HH(c, d, a, b, x[k + 7], S33, 0xf6bb4b60); b = HH(b, c, d, a, x[k + 10], S34, 0xbebfbc70); a = HH(a, b, c, d, x[k + 13], S31, 0x289b7ec6); d = HH(d, a, b, c, x[k + 0], S32, 0xeaa127fa); c = HH(c, d, a, b, x[k + 3], S33, 0xd4ef3085); b = HH(b, c, d, a, x[k + 6], S34, 0x4881d05); a = HH(a, b, c, d, x[k + 9], S31, 0xd9d4d039); d = HH(d, a, b, c, x[k + 12], S32, 0xe6db99e5); c = HH(c, d, a, b, x[k + 15], S33, 0x1fa27cf8); b = HH(b, c, d, a, x[k + 2], S34, 0xc4ac5665); a = II(a, b, c, d, x[k + 0], S41, 0xf4292244); d = II(d, a, b, c, x[k + 7], S42, 0x432aff97); c = II(c, d, a, b, x[k + 14], S43, 0xab9423a7); b = II(b, c, d, a, x[k + 5], S44, 0xfc93a039); a = II(a, b, c, d, x[k + 12], S41, 0x655b59c3); d = II(d, a, b, c, x[k + 3], S42, 0x8f0ccc92); c = II(c, d, a, b, x[k + 10], S43, 0xffeff47d); b = II(b, c, d, a, x[k + 1], S44, 0x85845dd1); a = II(a, b, c, d, x[k + 8], S41, 0x6fa87e4f); d = II(d, a, b, c, x[k + 15], S42, 0xfe2ce6e0); c = II(c, d, a, b, x[k + 6], S43, 0xa3014314); b = II(b, c, d, a, x[k + 13], S44, 0x4e0811a1); a = II(a, b, c, d, x[k + 4], S41, 0xf7537e82); d = II(d, a, b, c, x[k + 11], S42, 0xbd3af235); c = II(c, d, a, b, x[k + 2], S43, 0x2ad7d2bb); b = II(b, c, d, a, x[k + 9], S44, 0xeb86d391); a = AddUnsigned(a, AA); b = AddUnsigned(b, BB); c = AddUnsigned(c, CC); d = AddUnsigned(d, DD); } if (bit == 32) { return WordToHex(a) + WordToHex(b) + WordToHex(c) + WordToHex(d); } else { return WordToHex(b) + WordToHex(c); } }; ================================================ FILE: web/src/plugins/safe-json-stringify.js ================================================ var hasProp = Object.prototype.hasOwnProperty; function throwsMessage(err) { return "[Throws: " + (err ? err.message : "?") + "]"; } function safeGetValueFromPropertyOnObject(obj, property) { if (hasProp.call(obj, property)) { try { return obj[property]; } catch (err) { return throwsMessage(err); } } return obj[property]; } function ensureProperties(obj) { var seen = []; // store references to objects we have seen before function visit(obj) { if (obj === null || typeof obj !== "object") { return obj; } if (seen.indexOf(obj) !== -1) { return "[Circular]"; } seen.push(obj); if (typeof obj.toJSON === "function") { try { var fResult = visit(obj.toJSON()); seen.pop(); return fResult; } catch (err) { return throwsMessage(err); } } if (Array.isArray(obj)) { var aResult = obj.map(visit); seen.pop(); return aResult; } var result = Object.keys(obj).reduce(function(result, prop) { // prevent faulty defined getter properties result[prop] = visit(safeGetValueFromPropertyOnObject(obj, prop)); return result; }, {}); seen.pop(); return result; } return visit(obj); } export const jsonEncode = function(data, replacer, space) { return JSON.stringify(ensureProperties(data), replacer, space); }; ================================================ FILE: web/src/plugins/vuex.js ================================================ import Vue from "vue"; import Vuex from "vuex"; import settings, { customFonts, syncConfigFiled } from "./config"; import { setCache, getCache } from "../plugins/cache"; import { Message } from "element-ui"; const defaultNS = [{ username: "默认", userNS: "default" }]; const builtInBookGroup = [ { groupId: -1, groupName: "全部", order: -10, show: true }, { groupId: -2, groupName: "本地", order: -9, show: true }, { groupId: -3, groupName: "音频", order: -8, show: true }, { groupId: -4, groupName: "未分组", order: -7, show: true } ]; Vue.use(Vuex); const getCurrentUserName = state => { return state.isManagerMode ? state.userNS : (state.userInfo || {}).username || "default"; }; export default new Vuex.Store({ state: { connected: false, api: getCache("api_prefix") || location.host + "/reader3", shelfBooks: [], readingBook: {}, config: { ...settings.config }, miniInterface: false, windowSize: { width: window.innerWidth, height: window.innerHeight }, touchable: "ontouchstart" in document, showLogin: false, loginAuth: true, token: getCache("api_token") || "", bookSourceList: [], isSecureMode: false, isManagerMode: false, secureKey: "", userInfo: {}, userList: [].concat(defaultNS), userNS: "default", showManagerMode: false, version: process.env.VUE_APP_BUILD_VERSION, filterRules: [], speechVoiceConfig: { ...settings.speechVoiceConfig }, safeArea: { top: 0, bottom: 0, left: 0, right: 0 }, autoPlay: false, failureIncludeTimeout: false, failureBookSource: [], bookGroupList: [], rssSourceList: [], shelfConfig: { ...settings.shelfConfig }, showImageViewer: false, previewImageIndex: 0, previewImgList: [], searchConfig: { ...settings.searchConfig }, txtTocRules: [], customConfigList: [].concat(settings.customConfigList), showBookInfo: {}, cachingBookList: [], bookmarks: [] }, mutations: { setShelfBooks(state, books) { // 过滤一下不用的字段,省点内存 window.shelfBooks = books; state.shelfBooks = books.map(v => { return { author: v.author, bookUrl: v.bookUrl, coverUrl: v.coverUrl, tocUrl: v.tocUrl, charset: v.charset, customCoverUrl: v.customCoverUrl, canUpdate: v.canUpdate, durChapterIndex: v.durChapterIndex, durChapterPos: v.durChapterPos, durChapterTime: v.durChapterTime, durChapterTitle: v.durChapterTitle, kind: v.kind, intro: v.intro, lastCheckTime: v.lastCheckTime, latestChapterTitle: v.latestChapterTitle, name: v.name, origin: v.origin, originName: v.originName, totalChapterNum: v.totalChapterNum, type: v.type, group: v.group }; }); }, updateShelfBook(state, book) { const index = state.shelfBooks.findIndex(v => v.bookUrl === book.bookUrl); if (index >= 0) { state.shelfBooks[index] = { ...state.shelfBooks[index], ...{ author: book.author || state.shelfBooks[index].author, bookUrl: book.bookUrl || state.shelfBooks[index].bookUrl, coverUrl: book.coverUrl || state.shelfBooks[index].coverUrl, tocUrl: book.tocUrl || state.shelfBooks[index].tocUrl, charset: book.charset || state.shelfBooks[index].charset, customCoverUrl: book.customCoverUrl || state.shelfBooks[index].customCoverUrl, canUpdate: typeof book.canUpdate === "undefined" ? state.shelfBooks[index].canUpdate : book.canUpdate, durChapterIndex: book.durChapterIndex || state.shelfBooks[index].durChapterIndex, durChapterPos: book.durChapterPos || state.shelfBooks[index].durChapterPos, durChapterTime: book.durChapterTime || state.shelfBooks[index].durChapterTime, durChapterTitle: book.durChapterTitle || state.shelfBooks[index].durChapterTitle, kind: book.kind || state.shelfBooks[index].kind, intro: book.intro || state.shelfBooks[index].intro, lastCheckTime: book.lastCheckTime || state.shelfBooks[index].lastCheckTime, latestChapterTitle: book.latestChapterTitle || state.shelfBooks[index].latestChapterTitle, name: book.name || state.shelfBooks[index].name, origin: book.origin || state.shelfBooks[index].origin, originName: book.originName || state.shelfBooks[index].originName, totalChapterNum: book.totalChapterNum || state.shelfBooks[index].totalChapterNum, type: book.type || state.shelfBooks[index].type, group: book.group || state.shelfBooks[index].group } }; state.shelfBooks = [].concat(state.shelfBooks); } }, setReadingBook(state, readingBook) { state.readingBook = readingBook; // 更新书架信息 setTimeout(() => { for (let i = 0; i < state.shelfBooks.length; i++) { if (state.shelfBooks[i].bookUrl === readingBook.bookUrl) { const title = ((readingBook.catalog || [])[readingBook.index] || {}) .title; state.shelfBooks[i] = { ...state.shelfBooks[i], durChapterTime: new Date().getTime(), durChapterIndex: readingBook.index, ...(title ? { durChapterTitle: title } : {}) }; break; } } state.shelfBooks = [].concat(state.shelfBooks); }, 100); // eslint-disable-next-line no-unused-vars const { catalog, latestChapterTitle, intro, ...info } = readingBook; setCache( getCurrentUserName(state) + "@readingRecent", JSON.stringify(info) ); }, setConfig(state, config) { delete config.name; delete config.configDefaultType; if ( config.theme !== settings.defaultNightTheme && config.theme !== "custom" ) { config.themeType = "day"; } else if (config.theme === settings.defaultNightTheme) { config.themeType = "night"; } state.config = config; // 同步设置到 customConfig if (config.customConfig) { const index = state.customConfigList.findIndex( v => v.name === config.customConfig ); if (index >= 0) { const oldCustomConfig = { ...state.customConfigList[index] }; syncConfigFiled.forEach(field => { if ( typeof config[field] !== "undefined" && field !== "name" && field !== "configDefaultType" ) { oldCustomConfig[field] = config[field]; } }); state.customConfigList[index] = oldCustomConfig; state.customConfigList = [].concat(state.customConfigList); setCache("customConfigList", JSON.stringify(state.customConfigList)); } } setCache("config", JSON.stringify(config)); }, setMiniInterface(state, mini) { if (state.config.pageMode === "自适应") { state.miniInterface = mini; } else { state.miniInterface = true; } }, setWindowSize(state, size) { state.windowSize = size; }, setTouchable(state, touchable) { state.touchable = touchable; }, setApi(state, api) { state.api = api; }, setConnected(state, connected) { state.connected = connected; }, setShowLogin(state, showLogin) { state.showLogin = showLogin; if (showLogin) { state.loginAuth = false; } }, setLoginAuth(state, loginAuth) { state.loginAuth = loginAuth; }, setToken(state, token) { state.token = token; setCache("api_token", token); }, setBookSourceList(state, list) { // 过滤一下不用的字段,省点内存 state.bookSourceList = list.map(v => { return { bookSourceGroup: v.bookSourceGroup, bookSourceName: v.bookSourceName, bookSourceType: v.bookSourceType, bookSourceUrl: v.bookSourceUrl, exploreUrl: v.exploreUrl }; }); }, setUserNS(state, userNS) { state.userNS = userNS; }, setIsSecureMode(state, isSecureMode) { state.isSecureMode = isSecureMode; }, setSecureKey(state, secureKey) { state.secureKey = secureKey; }, setIsManagerMode(state, isManagerMode) { state.isManagerMode = isManagerMode; }, setShowManagerMode(state, showManagerMode) { state.showManagerMode = showManagerMode; }, setUserInfo(state, userInfo) { state.userInfo = userInfo; }, setUserList(state, userList) { if (userList.length) { state.userList = [] .concat([{ username: "系统默认", userNS: "default" }]) .concat(userList); } else { state.userList = [].concat(defaultNS); } }, setFilterRules(state, filterRules) { state.filterRules = filterRules; setCache("filterRules", JSON.stringify(filterRules)); }, addFilterRule(state, rule) { let filterRules = [].concat(state.filterRules); if (typeof rule.index !== "undefined" && rule.index >= 0) { filterRules[rule.index] = rule; state.filterRules = filterRules; } else { filterRules = filterRules.concat([rule]); state.filterRules = filterRules; } // setCache("filterRules", JSON.stringify(filterRules)); }, setNightTheme(state, isNight) { let config = { ...state.config }; let themeConfig; if (isNight) { // 设置为默认黑夜方案 themeConfig = state.customConfigList.find( v => v.configDefaultType === "黑夜默认" ); } else { // 设置为默认白天方案 themeConfig = state.customConfigList.find( v => v.configDefaultType === "白天默认" ); } if (!themeConfig) { Message.error("未配置" + (isNight ? "黑夜默认" : "白天默认") + "方案"); return; } config = { ...config, ...themeConfig }; config.customConfig = themeConfig.name; // let config = { ...state.config }; // if (config.theme !== "custom") { // config.theme = parseInt(config.theme); // } // if (isNight) { // if ( // config.theme !== settings.defaultNightTheme && // config.themeType !== "night" // ) { // setCache("lastDayTheme", config.theme); // } // const lastNightTheme = getCache("lastNightTheme") || 6; // config.themeType = "night"; // config.theme = lastNightTheme; // } else if ( // config.theme === settings.defaultNightTheme || // config.themeType === "night" // ) { // setCache("lastNightTheme", config.theme); // const lastDayTheme = getCache("lastDayTheme") || 0; // config.themeType = "day"; // config.theme = lastDayTheme; // } // if (config.theme !== "custom") { // config.theme = parseInt(config.theme); // } state.config = config; setCache("config", JSON.stringify(config)); }, setSpeechVoiceConfig(state, voiceConfig) { state.speechVoiceConfig = voiceConfig; setCache("speechVoiceConfig", JSON.stringify(voiceConfig)); }, setSafeArea(state, safeArea) { state.safeArea = { ...state.safeArea, ...safeArea }; }, setAutoPlay(state, autoPlay) { state.autoPlay = autoPlay; }, addFailureBookSource(state, { bookSourceUrl, errorMsg }) { const index = state.failureBookSource.findIndex( v => v.bookSourceUrl === bookSourceUrl ); if (index >= 0) { return; } const bookSource = state.bookSourceList.find( v => v.bookSourceUrl === bookSourceUrl ); if (bookSource) { state.failureBookSource = state.failureBookSource.concat([ { ...bookSource, errorMsg } ]); } }, removeFailureBookSource(state, bookSourceList) { for (let i = 0; i < bookSourceList.length; i++) { const index = state.failureBookSource.findIndex( v => v.bookSourceUrl === bookSourceList[i].bookSourceUrl ); if (index >= 0) { state.failureBookSource.splice(index, 1); } } }, setFailureIncludeTimeout(state, failureIncludeTimeout) { state.failureIncludeTimeout = failureIncludeTimeout; }, setBookGroupList(state, bookGroupList) { const _bookGroupList = []; builtInBookGroup.forEach(group => { if (!bookGroupList.some(v => v.groupId === group.groupId)) { _bookGroupList.push(group); } }); state.bookGroupList = _bookGroupList .concat(bookGroupList) .sort((a, b) => a.order - b.order); }, setRssSourceList(state, rssSources) { state.rssSourceList = [].concat(rssSources); }, setShelfConfig(state, shelfConfig) { state.shelfConfig = shelfConfig; setCache("shelfConfig", JSON.stringify(shelfConfig)); }, setPreviewImageIndex(state, previewImageIndex) { state.previewImageIndex = previewImageIndex; }, setPreviewImgList(state, previewImgList) { if (previewImgList === false) { state.showImageViewer = false; state.previewImgList = []; state.previewImageIndex = 0; } else { state.previewImgList = previewImgList; state.showImageViewer = true; } }, setSearchConfig(state, searchConfig) { state.searchConfig = searchConfig; setCache("searchConfig", JSON.stringify(searchConfig)); }, setTxtTocRules(state, tocRules) { state.txtTocRules = [].concat(tocRules); }, setCustomConfigList(state, customConfigList) { state.customConfigList = [].concat(customConfigList); setCache("customConfigList", JSON.stringify(customConfigList)); }, setShowBookInfo(state, book) { state.showBookInfo = book; }, setCachingBookList(state, cachingBookList) { state.cachingBookList = [].concat(cachingBookList); }, setBookmarks(state, bookmarks) { state.bookmarks = bookmarks; } }, getters: { api: state => { if ( state.api.startsWith("http://") || state.api.startsWith("https://") || state.api.startsWith("//") ) { return state.api; } return "//" + state.api; }, apiRoot: (state, getters) => { return getters.api.replace(/\/reader3\/?/, ""); }, isSlideRead: state => { return state.miniInterface && state.config.readMethod === "左右滑动"; }, isSystemNight: state => { return state.config.theme === settings.defaultNightTheme; }, isNight: state => { return state.config.themeType === "night"; }, isKindlePage: state => { return state.config.pageType === "Kindle"; }, isNormalPage: state => { return state.config.pageType === "正常"; }, currentContentBGImg: (state, getters) => { if (state.config.contentBGImg) { return state.config.contentBGImg.startsWith("bg/") || state.config.contentBGImg.startsWith("http://") || state.config.contentBGImg.startsWith("https://") || state.config.contentBGImg.startsWith("//") ? state.config.contentBGImg : getters.apiRoot + state.config.contentBGImg; } return undefined; }, customCSSUrl: (_, getters) => { if (getters.api) { return getters.apiRoot + "/assets/reader.css"; } return false; }, currentFontFamily: state => { return settings.fonts[state.config.font]; }, currentCustomFontFamily: (state, getters) => { const customFontName = customFonts[state.config.font]; if ( state.config.customFontsMap && state.config.customFontsMap[customFontName] ) { let url = state.config.customFontsMap[customFontName]; return { name: customFontName, url: url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//") ? url : getters.apiRoot + url }; } return null; }, currentThemeConfig: (state, getters) => { if (state.config.theme === "custom") { return { body: (state.miniInterface && state.config.isNormalPage ? "linear-gradient(to bottom,rgba(0,0,0,.2) 0,transparent 56px), " : "") + (state.config.bodyColor || "#eadfca"), content: { backgroundImage: getters.currentContentBGImg ? `${ state.miniInterface && state.config.isNormalPage ? "linear-gradient(to bottom,rgba(0,0,0,.2) 0,transparent 56px), " : "" }url(${getters.currentContentBGImg})` : null, backgroundPosition: "center", backgroundRepeat: "no-repeat", backgroundAttachment: "fixed", backgroundColor: state.config.contentColor || "#ede7da", backgroundSize: "cover" }, popupPure: state.config.popupColor || "#ede7da", popup: (state.miniInterface && state.config.isNormalPage ? "linear-gradient(to bottom,rgba(0,0,0,.2) 0,transparent 36px), " : "") + (state.config.popupColor || "#ede7da") }; } else { const config = { ...settings.themes[state.config.theme] }; config.popupPure = config.popup; if (state.miniInterface && state.config.isNormalPage) { config.body = "linear-gradient(to bottom,rgba(0,0,0,.2) 0,transparent 36px), " + config.body; config.content = "linear-gradient(to bottom,rgba(0,0,0,.2) 0,transparent 36px), " + config.content; config.popup = "linear-gradient(to bottom,rgba(0,0,0,.2) 0,transparent 36px), " + config.popup; } return config; } }, shelfBooks: state => { return state.shelfBooks.sort(function(a, b) { var x = a["durChapterTime"] || 0; var y = b["durChapterTime"] || 0; return y - x; }); }, bookSourceGroupList: state => { const groupsMap = {}; state.bookSourceList.forEach(v => { if (v.bookSourceGroup) { groupsMap[v.bookSourceGroup] = (groupsMap[v.bookSourceGroup] | 0) + 1; } }); const groups = [ { name: "全部分组", value: "", count: state.bookSourceList.length } ]; for (const i in groupsMap) { if (Object.hasOwnProperty.call(groupsMap, i)) { groups.push({ name: i, value: i, count: groupsMap[i] }); } } return groups; }, builtInBookGroupMap: () => { return builtInBookGroup.reduce((c, v) => { c[v.groupId] = v.groupName; return c; }, {}); }, config: state => { return state.config; }, collapseMenu: state => { return state.miniInterface; }, dialogWidth: state => { return state.miniInterface ? "85%" : Math.min(Math.max(state.windowSize.width * 0.7, 750), 1000) + "px"; }, dialogSmallWidth: state => { return state.miniInterface ? "85%" : "500px"; }, dialogTop: (state, getters) => { return ( (state.windowSize.height - getters.dialogContentHeight - 70 - 54 - 60) / 2 + "px" ); }, dialogContentHeight: state => { if (state.miniInterface) { return state.windowSize.height - 54 - 60 - 70; } return Math.min(0.7 * state.windowSize.height - 70 - 54 - 60, 400); }, popupWidth: state => { return state.miniInterface ? state.windowSize.width : "600"; }, currentUserName: state => { return getCurrentUserName(state); }, currentChapter: state => { return state.readingBook && state.readingBook.catalog ? state.readingBook.catalog[state.readingBook.index] : {}; }, readingBook: state => { return { ...state.readingBook, ...(state.shelfBooks.find( v => v.bookUrl === state.readingBook.bookUrl ) || {}) }; } }, actions: { syncFromLocalStorage({ commit, getters }) { try { // 获取配置 const config = getCache("config"); if (config && typeof config === "object") { commit("setConfig", { ...settings.config, ...config }); } } catch (error) { // } try { // 获取最近阅读书籍 const readingRecent = getCache( getters.currentUserName + "@readingRecent" ); if (readingRecent && typeof readingRecent === "object") { if (typeof readingRecent.index == "undefined") { readingRecent.index = 0; } commit("setReadingBook", readingRecent); } } catch (error) { // } // try { // // 获取过滤规则 // const filterRules = getCache("filterRules"); // if (filterRules && Array.isArray(filterRules)) { // commit("setFilterRules", filterRules); // } // } catch (error) { // // // } try { // 获取自定义配置方案 const customConfigList = getCache("customConfigList"); if (customConfigList && Array.isArray(customConfigList)) { commit("setCustomConfigList", customConfigList); } } catch (error) { // } try { // 获取听书配置 const speechVoiceConfig = getCache("speechVoiceConfig"); if (speechVoiceConfig && typeof speechVoiceConfig === "object") { commit("setSpeechVoiceConfig", { ...settings.speechVoiceConfig, ...speechVoiceConfig }); } } catch (error) { // } try { // 获取书架设置 const shelfConfig = getCache("shelfConfig"); if (shelfConfig && typeof shelfConfig === "object") { commit("setShelfConfig", { ...settings.shelfConfig, ...shelfConfig }); } } catch (error) { // } try { // 获取搜索设置 const searchConfig = getCache("searchConfig"); if (searchConfig && typeof searchConfig === "object") { commit("setSearchConfig", { ...settings.searchConfig, ...searchConfig }); } } catch (error) { // } } } }); ================================================ FILE: web/src/registerServiceWorker.js ================================================ /* eslint-disable no-console */ import { register } from "register-service-worker"; export function registerServiceWorker() { try { if ( process.env.NODE_ENV === "production" && !window.getQueryString("nopwa") ) { register(`${process.env.BASE_URL}service-worker.js`, { ready() { // console.log( // "App is being served from cache by a service worker.\n" + // "For more details, visit https://goo.gl/AFskqB" // ); window.serviceWorkerReady = true; }, registered(registration) { // console.log("Service worker has been registered."); if (window.localStorage) { const currentVersion = window.localStorage.getItem( "READER_APP_BUILD_VERSION" ); const newVersion = process.env.VUE_APP_BUILD_VERSION; if (currentVersion !== newVersion) { registration.active.postMessage({ type: "SKIP_WAITING" }); window.localStorage.setItem( "READER_APP_BUILD_VERSION", newVersion ); } } } // cached() { // console.log("Content has been cached for offline use."); // }, // updatefound() { // console.log("New content is downloading."); // }, // updated() { // console.log("New content is available; please refresh."); // }, // offline() { // console.log( // "No internet connection found. App is running in offline mode." // ); // }, // error(error) { // console.error("Error during service worker registration:", error); // } }); } } catch (error) { // } } ================================================ FILE: web/src/router/index.js ================================================ import Vue from "vue"; import VueRouter from "vue-router"; Vue.use(VueRouter); const originalPush = VueRouter.prototype.push; VueRouter.prototype.push = function push(location) { return originalPush.call(this, location).catch(err => err); }; const routes = [ { path: "/", name: "index", component: () => import(/* webpackChunkName: "index" */ "../views/Index.vue") }, { path: "/reader", name: "Reader", component: () => import(/* webpackChunkName: "reader" */ "../views/Reader.vue") } ]; const router = new VueRouter({ // mode: "history", base: process.env.BASE_URL, routes }); export default router; ================================================ FILE: web/src/views/Index.vue ================================================ ================================================ FILE: web/src/views/Reader.vue ================================================ ================================================ FILE: web/vue.config.js ================================================ // vue.config.js var packageInfo = require("./package.json"); function buildVersion() { const now = new Date(); const pad = v => (v >= 10 ? "" + v : "0" + v); return ( pad(now.getMonth() + 1) + pad(now.getDate()) + pad(now.getHours()) + pad(now.getMinutes()) ); } process.env.VUE_APP_BUILD_VERSION = process.env.VUE_APP_BUILD_VERSION || "v" + packageInfo.version + "-" + buildVersion(); function customWorkboxPlugin(generateCacheKey, checkResponse) { return { generateCacheKey, checkResponse, // Return `response`, a different `Response` object, or `null`. cacheWillUpdate: async function cacheWillUpdate({ request, response, event, state }) { // console.log({ request, response, event, state }); const resCopy = response.clone(); if (this.checkResponse) { return await this.checkResponse({ request, response: resCopy, event, state }); } const body = await resCopy.json().catch(() => false); if (body && body.isSuccess) { // 请求成功 return response; } else { // 请求失败 return null; } }, cacheKeyWillBeUsed: async function cacheKeyWillBeUsed({ request, mode, params, event, state }) { // `request` is the `Request` object that would otherwise be used as the cache key. // `mode` is either 'read' or 'write'. // Return either a string, or a `Request` whose `url` property will be used as the cache key. // Returning the original `request` will make this a no-op. // 只使用 url 参数 作为缓存key // console.log({ // request, // mode, // params, // event, // state // }); if (this.generateCacheKey) { const cacheKey = this.generateCacheKey({ request, mode, params, event, state }); return cacheKey || request; } else { return request; } } }; } module.exports = { publicPath: "./", productionSourceMap: false, devServer: { port: 8081 }, // 编译依赖为 es5 transpileDependencies: ["element-ui", "codejar", "vue-lazyload"], pwa: { name: "阅读", themeColor: "#ffffff", msTileColor: "#000000", appleMobileWebAppCapable: "yes", appleMobileWebAppStatusBarStyle: "black-translucent", manifestOptions: { // display: "standalone" display: "fullscreen" }, // configure the workbox plugin // workboxPluginMode: "InjectManifest", workboxOptions: { // swSrc is required in InjectManifest mode. // swSrc: "src/service-worker.js" // ignoreURLParametersMatching: [new RegExp("accessToken")], exclude: ["index.html"], importScripts: ["sw.js"], cleanupOutdatedCaches: true, // skipWaiting: true, runtimeCaching: [ { // 首页 urlPattern: new RegExp("^https?://[^/]*/?$"), handler: "networkFirst", options: { cacheName: "home", cacheableResponse: { statuses: [200] } } }, // { // // 获取书架 // urlPattern: new RegExp("^https?://[^/]*/reader3/getBookshelf"), // handler: "networkFirst", // options: { // cacheName: "bookshelf", // cacheableResponse: { // statuses: [200] // }, // plugins: [ // customWorkboxPlugin(({ request, mode }) => { // const searchParams = new URL(request.url).searchParams; // if (mode === "read" && searchParams.get("refresh")) { // // 刷新时不读取缓存 // return false; // } // const accessToken = searchParams.get("accessToken"); // if (!accessToken) { // return request; // } // return "getBookshelf@" + accessToken.split(":")[0]; // }) // ] // } // }, // 书源手动缓存在 localStorage // { // // 获取书源 // urlPattern: new RegExp("^https?://[^/]*/reader3/getBookSources"), // handler: "networkFirst", // options: { // cacheName: "bookSources", // cacheableResponse: { // statuses: [200] // } // } // }, // { // // 获取书籍章节列表 // urlPattern: new RegExp("^https?://[^/]*/reader3/getChapterList"), // handler: "networkFirst", // options: { // cacheName: "bookChapterList", // networkTimeoutSeconds: 5, // cacheableResponse: { // statuses: [200] // }, // plugins: [ // customWorkboxPlugin(({ request, mode }) => { // const searchParams = new URL(request.url).searchParams; // if (mode === "read" && searchParams.get("refresh")) { // // 刷新时不读取缓存 // return false; // } // return searchParams.get("url") + "@chapterList"; // }) // ] // } // }, // { // // 获取书籍内容 // urlPattern: new RegExp("^https?://[^/]*/reader3/getBookContent"), // handler: "cacheFirst", // options: { // cacheName: "bookContent", // cacheableResponse: { // statuses: [200] // }, // expiration: { // maxAgeSeconds: 86400 * 30, // maxEntries: 10000 // }, // plugins: [ // customWorkboxPlugin(({ request, mode }) => { // const searchParams = new URL(request.url).searchParams; // if (mode === "read" && searchParams.get("refresh")) { // // 刷新时不读取缓存 // return false; // } // return ( // searchParams.get("url") + // "@chapterContent-" + // searchParams.get("index") // ); // }) // ] // } // }, { // 获取书籍封面 urlPattern: new RegExp("^https?://[^/]*/reader3/cover"), handler: "cacheFirst", options: { cacheName: "bookCover", cacheableResponse: { statuses: [200] }, expiration: { maxAgeSeconds: 86400 * 30, maxEntries: 1000 }, plugins: [ customWorkboxPlugin( ({ request }) => { const searchParams = new URL(request.url).searchParams; return searchParams.get("path"); }, ({ response }) => { if (response.status === 200) { return response; } return null; } ) ] } } ] } } };