├── .gitattributes ├── .gitignore ├── .mvn ├── jvm.config └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── CONTRIBUTING.md ├── DIRECTORIES_AND_FILES.md ├── LICENSE ├── Makefile ├── README.md ├── RELEASE_NOTES.md ├── docs ├── about_thorough_testing.md ├── acknowledgements.md ├── dead_code_file.txt ├── definition_of_done.txt ├── development_handbook.md ├── getting_started │ ├── README.md │ ├── getting_started.md │ ├── getting_started_part_2.md │ ├── getting_started_part_3.md │ ├── getting_started_part_4.md │ ├── test_error.png │ ├── ui_finished.png │ └── ui_in_part_4.png ├── how_to_tell_if_well_tested_code.txt ├── howto │ ├── add_a_new_endpoint.md │ └── jlink.md ├── http_protocol │ ├── http_status_codes_mdn.txt │ ├── mime_types.txt │ └── password_storage_cheat_sheet_owasp.txt ├── maven │ ├── README.md │ ├── gnupg.tar.gz.encrypted │ └── pom.xml ├── migration_to_v3.md ├── parable_two_programmers.md ├── perf_data │ ├── README.md │ ├── database_speed_test.md │ ├── datestamp_perf.md │ ├── framework_perf_comparison.md │ ├── loom_perf.md │ └── response_speed_test.md ├── quick_start.md ├── release_messages │ ├── version1.md │ └── version3.md ├── simple_minum_program.jpg ├── simplify_then_add_lightness.md ├── size_comparisons.md ├── todo │ └── done │ │ ├── BUG_inconsistencies_in_DDPS_code.md │ │ ├── BUG_tests_added_to_wrong_suite.txt │ │ ├── a_user_should_be_able_to_add_photo_and_description.txt │ │ ├── docs_paradigm_of_the_database.txt │ │ ├── docs_recommended_code_pattern.txt │ │ ├── docs_test_examining_logs.txt │ │ ├── docs_testing_an_endpoint.txt │ │ ├── feature_cache_of_static_responses.txt │ │ ├── feature_index_page.txt │ │ ├── remove_static_variables.md │ │ └── virtual_tests_lock_up_after_reboot │ │ ├── tests_lock_up_after_reboot.txt │ │ └── thread_dump.json └── user_story_checklist.txt ├── minum.config ├── mvnw ├── mvnw.cmd ├── pom.xml ├── src ├── README.md ├── main │ ├── java │ │ ├── com │ │ │ └── renomad │ │ │ │ └── minum │ │ │ │ ├── README.md │ │ │ │ ├── database │ │ │ │ ├── Db.java │ │ │ │ ├── DbData.java │ │ │ │ ├── DbException.java │ │ │ │ ├── README.md │ │ │ │ └── package-info.java │ │ │ │ ├── htmlparsing │ │ │ │ ├── HtmlParseNode.java │ │ │ │ ├── HtmlParser.java │ │ │ │ ├── ParseNodeType.java │ │ │ │ ├── ParsingException.java │ │ │ │ ├── README.md │ │ │ │ ├── TagInfo.java │ │ │ │ ├── TagName.java │ │ │ │ └── package-info.java │ │ │ │ ├── logging │ │ │ │ ├── ILogger.java │ │ │ │ ├── Logger.java │ │ │ │ ├── LoggingActionQueue.java │ │ │ │ ├── LoggingLevel.java │ │ │ │ ├── README.md │ │ │ │ ├── TestLogger.java │ │ │ │ ├── TestLoggerException.java │ │ │ │ ├── TestLoggerQueue.java │ │ │ │ ├── ThrowingSupplier.java │ │ │ │ └── package-info.java │ │ │ │ ├── package-info.java │ │ │ │ ├── queue │ │ │ │ ├── AbstractActionQueue.java │ │ │ │ ├── ActionQueue.java │ │ │ │ ├── ActionQueueKiller.java │ │ │ │ ├── ActionQueueState.java │ │ │ │ └── package-info.java │ │ │ │ ├── security │ │ │ │ ├── ForbiddenUseException.java │ │ │ │ ├── ITheBrig.java │ │ │ │ ├── Inmate.java │ │ │ │ ├── MinumSecurityException.java │ │ │ │ ├── README.md │ │ │ │ ├── TheBrig.java │ │ │ │ ├── UnderInvestigation.java │ │ │ │ └── package-info.java │ │ │ │ ├── state │ │ │ │ ├── Constants.java │ │ │ │ ├── Context.java │ │ │ │ └── package-info.java │ │ │ │ ├── templating │ │ │ │ ├── README.md │ │ │ │ ├── RenderingResult.java │ │ │ │ ├── TemplateParseException.java │ │ │ │ ├── TemplateProcessor.java │ │ │ │ ├── TemplateRenderException.java │ │ │ │ ├── TemplateSection.java │ │ │ │ └── package-info.java │ │ │ │ ├── testing │ │ │ │ ├── README.md │ │ │ │ ├── RegexUtils.java │ │ │ │ ├── StopwatchUtils.java │ │ │ │ ├── TestFailureException.java │ │ │ │ ├── TestFramework.java │ │ │ │ └── package-info.java │ │ │ │ ├── utils │ │ │ │ ├── ByteUtils.java │ │ │ │ ├── ConcurrentSet.java │ │ │ │ ├── CryptoUtils.java │ │ │ │ ├── FileReader.java │ │ │ │ ├── FileUtils.java │ │ │ │ ├── IFileReader.java │ │ │ │ ├── InvariantException.java │ │ │ │ ├── Invariants.java │ │ │ │ ├── LRUCache.java │ │ │ │ ├── MyThread.java │ │ │ │ ├── README.md │ │ │ │ ├── RingBuffer.java │ │ │ │ ├── RunnableWithDescription.java │ │ │ │ ├── SearchUtils.java │ │ │ │ ├── SerializationUtils.java │ │ │ │ ├── StacktraceUtils.java │ │ │ │ ├── StringUtils.java │ │ │ │ ├── ThrowingRunnable.java │ │ │ │ ├── TimeUtils.java │ │ │ │ ├── UtilsException.java │ │ │ │ └── package-info.java │ │ │ │ └── web │ │ │ │ ├── Body.java │ │ │ │ ├── BodyProcessor.java │ │ │ │ ├── BodyType.java │ │ │ │ ├── ContentDisposition.java │ │ │ │ ├── CountBytesRead.java │ │ │ │ ├── FullSystem.java │ │ │ │ ├── FunctionalTesting.java │ │ │ │ ├── Headers.java │ │ │ │ ├── HttpServerType.java │ │ │ │ ├── HttpVersion.java │ │ │ │ ├── IBodyProcessor.java │ │ │ │ ├── IInputStreamUtils.java │ │ │ │ ├── IRequest.java │ │ │ │ ├── IResponse.java │ │ │ │ ├── IServer.java │ │ │ │ ├── ISocketWrapper.java │ │ │ │ ├── InputStreamUtils.java │ │ │ │ ├── InvalidRangeException.java │ │ │ │ ├── LastMinuteHandlerInputs.java │ │ │ │ ├── Partition.java │ │ │ │ ├── PathDetails.java │ │ │ │ ├── PreHandlerInputs.java │ │ │ │ ├── README.md │ │ │ │ ├── Range.java │ │ │ │ ├── Request.java │ │ │ │ ├── RequestLine.java │ │ │ │ ├── Response.java │ │ │ │ ├── Server.java │ │ │ │ ├── SetOfSws.java │ │ │ │ ├── SocketWrapper.java │ │ │ │ ├── StatusLine.java │ │ │ │ ├── StreamingMultipartPartition.java │ │ │ │ ├── ThrowingConsumer.java │ │ │ │ ├── ThrowingFunction.java │ │ │ │ ├── UrlEncodedDataGetter.java │ │ │ │ ├── UrlEncodedKeyValue.java │ │ │ │ ├── WebEngine.java │ │ │ │ ├── WebFramework.java │ │ │ │ ├── WebServerException.java │ │ │ │ └── package-info.java │ │ └── module-info.java │ └── resources │ │ └── certs │ │ ├── README.txt │ │ └── keystore └── test │ ├── java │ └── com │ │ └── renomad │ │ └── minum │ │ ├── EqualsTests.java │ │ ├── FunctionalTests.java │ │ ├── SearchHelpers.java │ │ ├── TheRegister.java │ │ ├── database │ │ └── DbTests.java │ │ ├── htmlparsing │ │ ├── HtmlParseNodeTests.java │ │ ├── HtmlParserTests.java │ │ └── TagInfoTests.java │ │ ├── logging │ │ ├── CustomLoggingLevel.java │ │ ├── DescendantLogger.java │ │ ├── LoggerTests.java │ │ ├── LoggingActionQueueTests.java │ │ └── TestLoggerTests.java │ │ ├── sampledomain │ │ ├── ListPhotos.java │ │ ├── LruCacheTests.java │ │ ├── PersonName.java │ │ ├── README.md │ │ ├── SampleDomain.java │ │ ├── UploadPhoto.java │ │ ├── auth │ │ │ ├── AuthResult.java │ │ │ ├── AuthUtils.java │ │ │ ├── LoginResult.java │ │ │ ├── LoginResultStatus.java │ │ │ ├── LoopingSessionReviewing.java │ │ │ ├── README.md │ │ │ ├── RegisterResult.java │ │ │ ├── RegisterResultStatus.java │ │ │ ├── SessionId.java │ │ │ ├── User.java │ │ │ └── package-info.java │ │ ├── package-info.java │ │ └── photo │ │ │ ├── Photograph.java │ │ │ ├── README.md │ │ │ ├── Video.java │ │ │ └── package-info.java │ │ ├── security │ │ └── TheBrigTests.java │ │ ├── state │ │ └── ConstantsTests.java │ │ ├── templating │ │ ├── Stock.java │ │ ├── TemplateSectionTests.java │ │ └── TemplatingTests.java │ │ ├── testing │ │ ├── README.md │ │ ├── RegexUtilsTests.java │ │ └── TestFrameworkTests.java │ │ ├── utils │ │ ├── ActionQueueKillerTests.java │ │ ├── ActionQueueTests.java │ │ ├── ByteUtilsTests.java │ │ ├── CryptoUtilsTests.java │ │ ├── FileReaderTests.java │ │ ├── FileUtilsTests.java │ │ ├── GzipTests.java │ │ ├── InvariantsTests.java │ │ ├── MyThreadTests.java │ │ ├── RingBufferTests.java │ │ ├── RunnableWithDescriptionTests.java │ │ ├── SearchUtilsTests.java │ │ ├── SerializationUtilsTests.java │ │ ├── StackTraceUtilsTests.java │ │ ├── StringUtilsTests.java │ │ ├── ThrowingRunnableTests.java │ │ └── TimeUtilsTests.java │ │ └── web │ │ ├── BodyProcessorTests.java │ │ ├── BodyTests.java │ │ ├── EndpointTests.java │ │ ├── FakeBodyProcessor.java │ │ ├── FakeRequest.java │ │ ├── FakeSocketWrapper.java │ │ ├── FullSystemTests.java │ │ ├── FunctionalTestingTests.java │ │ ├── HeadersTests.java │ │ ├── InputStreamUtilsTests.java │ │ ├── PathDetailsTests.java │ │ ├── RangeTests.java │ │ ├── RequestLineTests.java │ │ ├── RequestTests.java │ │ ├── ResponseTests.java │ │ ├── ServerTests.java │ │ ├── SetOfSwsTests.java │ │ ├── SocketWrapperTests.java │ │ ├── WebEngineTests.java │ │ ├── WebFrameworkTests.java │ │ └── WebTests.java │ ├── resources │ ├── gettysburg_address.txt │ ├── html_fuzzer.html │ ├── kitty.jpg │ └── video_poster.jpg │ └── webapp │ ├── README.md │ ├── static │ ├── Foo │ ├── index.html │ ├── index.js │ ├── listphotos │ │ └── list_photos.css │ ├── main.css │ ├── moon.png │ ├── moon.webp │ └── uploadphoto │ │ ├── upload_photo.css │ │ └── upload_photo.js │ └── templates │ ├── auth │ ├── login_page_template.html │ ├── logout_page_template.html │ └── register_page_template.html │ ├── listphotos │ ├── list_photos_template.html │ └── video_element_template.html │ ├── sampledomain │ ├── auth_homepage.html │ ├── name_entry.html │ └── unauth_homepage.html │ ├── templatebenchmarks │ ├── expected_stock_output.html │ ├── expected_stock_output_parsed.txt │ ├── individual_stock.html │ └── stock_prices.html │ └── uploadphoto │ ├── upload_photo_template.html │ └── upload_video_template.html └── utils └── build_manifest.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=lf 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.java text 7 | *.sh text 8 | *.txt text 9 | 10 | # Denote all files that are truly binary and should not be modified. 11 | *.png binary 12 | *.jpg binary 13 | *.jar binary 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # don't add any built Java files 2 | *.class 3 | 4 | # ignore the built stuff 5 | out/ 6 | target/ 7 | 8 | # don't want to commit any Intellij config files 9 | .idea 10 | *.iml 11 | 12 | # Don't want any of those Mac finder junk files scattered all over 13 | .DS_Store 14 | 15 | # Don't store the minum.database we create with our application 16 | # that is in the root directory (there is a sample in the docs directory 17 | # that we *will* store for use in local testing) 18 | db/ 19 | 20 | # This file is just a flag to show that the system is running. 21 | SYSTEM_RUNNING 22 | 23 | # Sometimes the Java compiler will fail and output these files, like javac.20230430_000504.args 24 | *.args 25 | 26 | # One of the functional tests puts a file in the static directory that is very large. 27 | # we don't want to add this to our repo. 28 | src/test/webapp/static/largefile.txt 29 | 30 | # Eclipse config files 31 | *.prefs 32 | .classpath 33 | .project 34 | -------------------------------------------------------------------------------- /.mvn/jvm.config: -------------------------------------------------------------------------------- 1 | --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED 2 | --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED 3 | --add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED 4 | --add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED 5 | --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED 6 | --add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED 7 | --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED 8 | --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED 9 | --add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED 10 | --add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byronka/minum/7e1d83fb4927e5ca2f1d3231a21284c8de58f0f4/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.8/apache-maven-3.9.8-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to the Minum web framework 2 | ======================================= 3 | 4 | This is an opinionated minimalist web framework to develop 5 | server-side-rendered monolith hypermedia html-first web applications. 6 | 7 | The thinking behind this project is described in the 8 | [development handbook](docs/development_handbook.md/#on-minimalism). 9 | 10 | Emphasis is on developing the leanest interface with the fewest 11 | technologies, written as frugally and plainly as possible. 12 | Performance, security, and maintainability are vital. 13 | 14 | What is needed 15 | -------------- 16 | 17 | * Clear documentation 18 | * Examples 19 | * Refinements and refactorings 20 | * Fresh ideas (but staying minimalist) 21 | * Reporting security vulnerabilities 22 | * Performance tuning 23 | * Bug removal 24 | * Expanded testing 25 | 26 | 27 | Please create a discussion to explain the situation before getting into coding. 28 | 29 | If desiring to add a new capability to the system, describe how it 30 | will benefit the user, with sufficient examples. New features will 31 | require a compelling explanation of the anticipated benefits. 32 | 33 | The system will stay at 100% statement and branch coverage. While it is not absolutely necessary 34 | that your code get to that level, it is appreciated to provide good testing for 35 | any new functionality. This is a good time to repeat that any new functionality should 36 | start with a discussion, so that your effort is not wasted. 37 | -------------------------------------------------------------------------------- /DIRECTORIES_AND_FILES.md: -------------------------------------------------------------------------------- 1 | 2 | Directories: 3 | ------------ 4 | 5 | - docs: documentation for the project 6 | - .git: necessary files for Git source-code management. 7 | - .mvn: necessary files for the Maven wrapper 8 | - src: All the source code 9 | - utils: scripts and utilities for the system 10 | - out: artifacts from the publishing process 11 | - target: the default Maven build directory, built after it runs 12 | 13 | 14 | Root-level files: 15 | ----------------- 16 | 17 | - DIRECTORIES_AND_FILES.md: this file 18 | - .gitignore: files we want Git to ignore. 19 | - .gitattributes: configuration for Git 20 | - LICENSE: the license that applies to this code 21 | - Makefile: the configuration for Gnu Make, which is part of our build tools 22 | - minum.config: a configuration file for the running app (a local / test-oriented version) 23 | - mvnw and mvnw.cmd: the maven wrapper - provides an ability to run Maven commands without needing to install Maven 24 | - pom.xml: Maven configuration 25 | - README.md: A surface-level explainer of the project 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Byron Katz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docs/acknowledgements.md: -------------------------------------------------------------------------------- 1 | Acknowledgements 2 | ================ 3 | 4 | I appreciate people taking the time to give advice: 5 | 6 | - Matthew Taylor - Thanks for the reviews 7 | - Elliott Frisch - Thanks for the advice 8 | - Chris Carroll - Appreciate you taking the time for a run-through 9 | - Shawn McManus - Thanks for the encouragement 10 | - Matt Grasberger, Adam Hamrick (https://github.com/kalverra), Max Saperstone - Thanks for the encouragement and advice -------------------------------------------------------------------------------- /docs/dead_code_file.txt: -------------------------------------------------------------------------------- 1 | The following is a record of commits where significant reduction of dead code took place. 2 | 3 | Some of this dead code may come in handy in the future, but was not used at the time. 4 | 5 | SHA Description 6 | fb685570 simple ORM for external db, r3z-style minum.database 7 | 84c24363 Crypto utilities - hashing 8 | 27d1f189 HTTP/2 9 | v7.0.0 http transfer by chunked encoding -------------------------------------------------------------------------------- /docs/definition_of_done.txt: -------------------------------------------------------------------------------- 1 | 1. coding is complete 2 | 2. code reviewed 3 | 3. user story checklist considered 4 | 4. automation runs successfully 5 | 5. manual careful review 6 | 6. validation by customer/user (if available) 7 | -------------------------------------------------------------------------------- /docs/getting_started/README.md: -------------------------------------------------------------------------------- 1 | Getting Started tutorial 2 | ======================== 3 | 4 | A tutorial for getting familiar with use of the framework from the ground up. 5 | 6 | Start [here](getting_started.md) -------------------------------------------------------------------------------- /docs/getting_started/test_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byronka/minum/7e1d83fb4927e5ca2f1d3231a21284c8de58f0f4/docs/getting_started/test_error.png -------------------------------------------------------------------------------- /docs/getting_started/ui_finished.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byronka/minum/7e1d83fb4927e5ca2f1d3231a21284c8de58f0f4/docs/getting_started/ui_finished.png -------------------------------------------------------------------------------- /docs/getting_started/ui_in_part_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byronka/minum/7e1d83fb4927e5ca2f1d3231a21284c8de58f0f4/docs/getting_started/ui_in_part_4.png -------------------------------------------------------------------------------- /docs/how_to_tell_if_well_tested_code.txt: -------------------------------------------------------------------------------- 1 | how to tell if code is well tested? 2 | 3 | run a test. Count the number of lines run. Fewer lines being run by a test is better than more lines. 4 | for a given line, how many times is it touched when all the tests run - overall, being called more may correlate to better testing 5 | for a given function, how many times is it called? overall, being called more may correlate to better testing 6 | for a given statement, how many mutations will cause its test to break? 7 | for a given predicate, what percentage of MC/DC coverage are we hitting? 8 | what is the overall code coverage 9 | what is the mutation-affected code coverage? 10 | for a parameter's type, how many of the basic boundary values are hit by the tests? How many of the edges of the boundaries? if nullable, is a null sent? if a container, is an empty container sent? if a string, is an empty string sent? and so on. Consider basic negative tests for various types. 11 | on top of several statistics, how to counter gaming (creating loops to call methods / lines many times to raise the value). Perhaps it is a matter of weighing each dimension properly, to prevent any one dimension from controlling. 12 | 13 | -------------------------------------------------------------------------------- /docs/howto/jlink.md: -------------------------------------------------------------------------------- 1 | # Creating Application images with Jlink and Minum 2 | 3 | ## 1. Copy all your dependencies to `target/modules` 4 | 5 | The following plugin configuration will copy the application jar and all the runtime dependencies to `target/modules`. 6 | ```xml 7 | 8 | maven-dependency-plugin 9 | 10 | 11 | copy-modules 12 | package 13 | 14 | copy-dependencies 15 | 16 | 17 | ${project.build.directory}/modules 18 | runtime 19 | 20 | 21 | 22 | 23 | 24 | org.apache.maven.plugins 25 | maven-jar-plugin 26 | 27 | ${project.build.directory}/modules 28 | 29 | 30 | ``` 31 | 32 | There is a practical example of this in the [pom file of memoria_project](https://github.com/byronka/memoria_project/blob/master/pom.xml) . 33 | Search that file for "maven-dependency-plugin". 34 | 35 | ## 2. Use Jlink to create a slim Java Runtime (JRT) with only the modules required to run the application 36 | 37 | ```shell 38 | jlink --add-modules --module-path target/modules --output /target/jrt 39 | ``` 40 | 41 | See the "jlink" target in the [Makefile for memoria_project](https://github.com/byronka/memoria_project/blob/master/Makefile) to 42 | see a practical example of this. To see it in action, run `make jlink` after cloning that project. 43 | 44 | ## 3. Run the application 45 | 46 | ```shell 47 | ./target/jrt/bin/java -m / 48 | ``` 49 | 50 | See the "runjlink" target in the [Makefile for memoria_project](https://github.com/byronka/memoria_project/blob/master/Makefile) for 51 | a practical example. To see if in action, run `make runjlink` -------------------------------------------------------------------------------- /docs/maven/README.md: -------------------------------------------------------------------------------- 1 | Maven 2 | ===== 3 | 4 | These are files necessary for publishing Minum to the Maven Central repository 5 | 6 | How to deploy to Maven Central 7 | ------------------------------ 8 | 9 | Run `make mvnprep` 10 | 11 | There will now be a file `out/bundle.jar`, which you will use in 12 | the process explained [here](https://central.sonatype.org/publish/publish-manual/#bundle-creation) 13 | 14 | In case the web page is down, here's the gist of it: 15 | 16 | 1. Once bundle.jar has been produced, log into [OSSRH](https://s01.oss.sonatype.org/), and select 17 | Staging Upload in the Build Promotion menu on the left. 18 | 2. From the Staging Upload tab, select Artifact Bundle from the Upload Mode dropdown. 19 | 3. Then click the Select Bundle to Upload button, and select the bundle you just created. 20 | 4. Click the Upload Bundle button. If the upload is successful, a staging repository will be 21 | created, and you can proceed with [releasing](https://central.sonatype.org/publish/release/). 22 | 23 | Deployment checklist 24 | -------------------- 25 | - [ ] local testing with new version number 26 | - [ ] ensure release notes are properly updated 27 | - [ ] spot-check test logs 28 | - [ ] confirm howtos 29 | - [ ] uncomment linting tools in maven-compiler-plugin and examine the output during build. Note this will be 30 | full of false positives, so examine with a critical eye. 31 | - [ ] adjust code in sample programs as needed, run their test programs 32 | - [ ] review generated bundle.jar, particularly its pom and manifest 33 | - [ ] update versions in quick start, tutorial, and example projects 34 | - [ ] squash the changes to a single commit 35 | - [ ] generate and examine reports: site page, pitest report, javadoc 36 | - [ ] publish to Maven central 37 | - [ ] push to Github, add Release 38 | 39 | Gnupg - GNU privacy guard 40 | ------------------------- 41 | 42 | Gnupg [gpg](https://gnupg.org/) is used to sign the bundle for shipping to Maven central. Its entire 43 | configuration directory is encrypted and stored here, as `gnupg.tar.gz.encrypted`. 44 | 45 | The passphrase to decrypt this file is the same as the one used when creating a signed bundle. 46 | 47 | To encrypt the directory: 48 | 49 | ```shell 50 | gpg --output gnupg.tar.gz.encrypted --symmetric --cipher-algo AES256 gnupg.tar.gz 51 | ``` 52 | 53 | To decrypt the directory: 54 | 55 | ```shell 56 | gpg --output gnupg.tar.gz --decrypt gnupg.tar.gz.encrypted 57 | ``` 58 | 59 | to untar the result: 60 | 61 | ```shell 62 | tar zxf gnupg.tar.gz 63 | ``` -------------------------------------------------------------------------------- /docs/maven/gnupg.tar.gz.encrypted: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byronka/minum/7e1d83fb4927e5ca2f1d3231a21284c8de58f0f4/docs/maven/gnupg.tar.gz.encrypted -------------------------------------------------------------------------------- /docs/maven/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | com.renomad 4 | minum 5 | {{VERSION}} 6 | jar 7 | 8 | Minum web framework 9 | A minimalist web framework 10 | https://github.com/byronka/minum 11 | 12 | 13 | 14 | MIT License 15 | http://www.opensource.org/licenses/mit-license.php 16 | 17 | 18 | 19 | 20 | scm:git:git://github.com/byronka/minum.git 21 | scm:git:ssh://github.com:byronka/minum.git 22 | https://github.com/byronka/minum/tree/master 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Byron Katz 32 | byronka@msn.com 33 | Renomad 34 | http://www.renomad.com 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/perf_data/README.md: -------------------------------------------------------------------------------- 1 | System Performance Data 2 | ======================= 3 | 4 | This directory contains data that helps us determine whether there are slow parts 5 | of our program. 6 | 7 | Performance testing was run with these specifications: 8 | 9 | * Processor: Intel(R) Core(TM) i5-4590 CPU @ 3.30GHz, 3301 Mhz, 4 Core(s), 4 Logical Processor(s) 10 | * System Type: x64-based PC 11 | * Installed Physical Memory (RAM): 16.0 GB 12 | * Operating System: Microsoft Windows 10 Pro 10.0.19045 Build 19045 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/perf_data/database_speed_test.md: -------------------------------------------------------------------------------- 1 | Database Speed Test 2 | =================== 3 | 4 | The distinction of our database is that the data values are of a particular type, 5 | DbData, which provide support for disk persistence. Otherwise, they 6 | are treated just like any ordinary data and can be arranged in any collection 7 | shape you could wish - trees, lists, whatever. 8 | 9 | Summary of the code below: we can make two million adjustments to our database in 10 | one second. The persistence to disk will happen over the ensuing minutes - this 11 | is not an ACID-compliant database. That risk, however, is allowable for many 12 | use cases. 13 | 14 | ```java 15 | 16 | /** 17 | * When this is looped a hundred thousand times, it takes 500 milliseconds to finish 18 | * making the updates in memory. It takes several minutes later for it to 19 | * finish getting those changes persisted to disk. 20 | * 21 | * a million writes in 500 milliseconds means 2 million writes in one sec. 22 | */ 23 | logger.test("Just how fast is our minum.database?");{ 24 | // clear out the directory to start 25 | FileUtils.deleteDirectoryRecursivelyIfExists(foosDirectory, logger); 26 | final var db = new Db(foosDirectory, context); 27 | MyThread.sleep(10); 28 | 29 | final var foos = new ArrayList(); 30 | 31 | // write the foos 32 | for (int i = 0; i < 10; i++) { 33 | final var newFoo = new Foo(i, i + 1, "original"); 34 | foos.add(newFoo); 35 | db.persistToDisk(newFoo); 36 | } 37 | 38 | // change the foos 39 | final var outerTimer = new StopwatchUtils().startTimer(); 40 | final var innerTimer = new StopwatchUtils().startTimer(); 41 | for (var i = 1; i < 10; i++) { 42 | final var newFoos = new ArrayList(); 43 | /* 44 | loop through the old foos and update them to new values, 45 | creating a new list in the process. There should only 46 | ever be 10 foos. 47 | */ 48 | for (var foo : foos) { 49 | final var newFoo = new Foo(foo.index, foo.a + 1, foo.b + "_updated"); 50 | newFoos.add(newFoo); 51 | db.persistToDisk(newFoo); 52 | } 53 | } 54 | logger.logDebug(() -> "It took " + innerTimer.stopTimer() + " milliseconds to make the updates in memory"); 55 | db.stop(10, 20); 56 | logger.logDebug(() -> "It took " + outerTimer.stopTimer() + " milliseconds to finish writing everything to disk"); 57 | } 58 | ``` -------------------------------------------------------------------------------- /docs/perf_data/datestamp_perf.md: -------------------------------------------------------------------------------- 1 | Performance experiments 2 | ======================= 3 | 4 | Experiment 1 - date and time stamp during HTTP response 5 | --------------------------------------------------------------------- 6 | 7 | It was my impression that getting the current date and time might slow down the HTTP response 8 | a bit, so I ran an experiment. The following code took 686 milliseconds to run in a million loops, 9 | so it is crunching about 1.5 million times per second. I am therefore 10 | not concerned with its performance. 11 | 12 | ```java 13 | 14 | for(var i = 0; i < 1_000_000; i++) { 15 | ZonedDateTime.now(ZoneId.of("UTC")).format(DateTimeFormatter.RFC_1123_DATE_TIME); 16 | } 17 | 18 | ``` -------------------------------------------------------------------------------- /docs/perf_data/framework_perf_comparison.md: -------------------------------------------------------------------------------- 1 | Framework performance comparison 2 | ================================= 3 | 4 | In summary: Minum is as fast, sometimes faster than Spring in these statistics. 5 | 6 | | | Minum | Spring | 7 | |--------------------------------------------------------------------------------------------------------------------------------------------|--------|--------| 8 | | Compile time
\> mvn clean compile
\> (minum) make clean jar | 3.4 | 1.5 | 9 | | Start Time (Sec)
\> java -jar app.jar | 0.3 | 1.6 | 10 | | Requests Per Second
\> ab -k -c 20 -n 1000000 http://localhost/8080/hello/John
Single thread | 19k | 18k | 11 | | Requests Per Second with -Xmx16m
\> ab -k -c 20 -n 1000000 http://localhost:8080/hello/John
Single thread | 19k | 10k | 12 | | Requests Per Second with -Xmx64m
\> wrk -t12 -c400 -d30s --latency http://localhost:8080/hello/John
12 threads and 400 connections | 8.9k | 4.2k | 13 | | Memory Consumption - Heap Usage (Mb) | 50 | 95 | 14 | | Memory Consumption - Heap usage with -Xmx16m (Mb) | 8.6 | 10.5 | 15 | | Jar Size With Dependencies (Mb) | 0.2 | 19 | -------------------------------------------------------------------------------- /docs/perf_data/loom_perf.md: -------------------------------------------------------------------------------- 1 | Project Loom 2 | ============ 3 | 4 | "Project Loom" is the name given to the work being done to enable green threads in the Java VM. 5 | This will allow us to have millions of threads, which is perfect for our use case. 6 | 7 | Here is some code to sample this. While running this code, have Java Mission Control running 8 | and keep an eye on the live threads. In the first chunk of code (which uses ordinary threads), 9 | each thread will take up an OS thread and about 2 megabytes of memory - about 8 gigabytes gets used. 10 | 11 | In the next chunk, it's using virtual threads, and you will see it use maybe 30 threads and maybe 12 | 50 megabytes. Quite a difference! 13 | 14 | ```java 15 | System.out.println("Starting lots of threads"); { 16 | 17 | var threadFactory = Thread.ofPlatform().factory(); 18 | try (var executor = Executors.newThreadPerTaskExecutor(threadFactory)) { 19 | IntStream.range(0, 10_000).forEach(i -> { 20 | executor.submit(() -> { 21 | Thread.sleep(Duration.ofSeconds(100)); 22 | return i; 23 | }); 24 | }); 25 | } 26 | } 27 | 28 | System.out.println("Starting virtual threads");{ 29 | 30 | try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { 31 | IntStream.range(0, 10_000).forEach(i -> { 32 | executor.submit(() -> { 33 | Thread.sleep(Duration.ofSeconds(100)); 34 | return i; 35 | }); 36 | }); 37 | } 38 | } 39 | ``` 40 | 41 | Still, the issue remains: When I try using this virtual thread executor, my tests fail 42 | in seemingly capricious ways. Not good. Still waiting on this to stabilize. 43 | -------------------------------------------------------------------------------- /docs/perf_data/response_speed_test.md: -------------------------------------------------------------------------------- 1 | Response time test 2 | ================== 3 | 4 | #### In short: 19,500 responses per second 5 | 6 | This performance test was run on a Mac M2 7 | 8 | ```shell 9 | $ ab -k -c20 -n 1000000 "http://localhost:8080/hello?name=byron" 10 | This is ApacheBench, Version 2.3 <$Revision: 1843412 $> 11 | Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ 12 | Licensed to The Apache Software Foundation, http://www.apache.org/ 13 | 14 | Benchmarking localhost (be patient) 15 | Completed 100000 requests 16 | Completed 200000 requests 17 | Completed 300000 requests 18 | Completed 400000 requests 19 | Completed 500000 requests 20 | Completed 600000 requests 21 | Completed 700000 requests 22 | Completed 800000 requests 23 | Completed 900000 requests 24 | Completed 1000000 requests 25 | Finished 1000000 requests 26 | 27 | 28 | Server Software: minum 29 | Server Hostname: localhost 30 | Server Port: 8080 31 | 32 | Document Path: /hello?name=byron 33 | Document Length: 11 bytes 34 | 35 | Concurrency Level: 20 36 | Time taken for tests: 51.167 seconds 37 | Complete requests: 1000000 38 | Failed requests: 0 39 | Keep-Alive requests: 1000000 40 | Total transferred: 150000000 bytes 41 | HTML transferred: 11000000 bytes 42 | Requests per second: 19543.92 [#/sec] (mean) 43 | Time per request: 1.023 [ms] (mean) 44 | Time per request: 0.051 [ms] (mean, across all concurrent requests) 45 | Transfer rate: 2862.88 [Kbytes/sec] received 46 | 47 | Connection Times (ms) 48 | min mean[+/-sd] median max 49 | Connect: 0 0 0.0 0 4 50 | Processing: 0 1 0.4 1 33 51 | Waiting: 0 1 0.4 1 29 52 | Total: 0 1 0.4 1 33 53 | 54 | Percentage of the requests served within a certain time (ms) 55 | 50% 1 56 | 66% 1 57 | 75% 1 58 | 80% 1 59 | 90% 1 60 | 95% 2 61 | 98% 2 62 | 99% 2 63 | 100% 33 (longest request) 64 | 65 | ``` -------------------------------------------------------------------------------- /docs/quick_start.md: -------------------------------------------------------------------------------- 1 | Quick Start 2 | =========== 3 | 4 | This software will enable you to create web applications in Java. It provides 5 | the bare minimum of what is necessary for that task, plainly and simply. This quick 6 | start assumes you have a Posix environment, and have Java 21 or higher installed. If 7 | not, see [environment](#environment) 8 | 9 | 10 | Step 1 - Download the example 11 | ----------------------------- 12 | 13 | Grab this project which demonstrates a simple approach to using Minum. 14 | 15 | Using Git: 16 | 17 | ```shell 18 | git clone https://github.com/byronka/minum_usage_example_smaller.git 19 | ``` 20 | 21 | _If you don't have Git_, you can download a zip file of Minum, which will need to be unzipped: 22 | 23 | https://github.com/byronka/minum_usage_example_smaller/archive/refs/heads/master.zip 24 | 25 | 26 | Step 2 - run the example 27 | ------------------------ 28 | 29 | Run this command in its directory: 30 | 31 | ```shell 32 | ./mvnw compile exec:java 33 | ``` 34 | 35 | It will compile and you will be able to view it at http://localhost:8080 36 | 37 | 38 | Step 3 - Think about the example 39 | -------------------------------- 40 | 41 | Let's look at the code: 42 | 43 | An annotated view of the main method 44 | 45 | 46 | Step 4 - modify the example 47 | --------------------------- 48 | 49 | * Stop the server and restart by running `./mvnw compile exec:java` 50 | * Change the path - have it serve content from /hi instead of /hello 51 | 52 | Next steps 53 | ---------- 54 | 55 | Now you are ready to go further. If you want a step-by-step tutorial on building a 56 | project with Minum from the ground up, check out the [getting started tutorial](getting_started/getting_started.md). 57 | 58 | Or, you may want to pore through a [larger example](https://github.com/byronka/minum_usage_example_mvn) 59 | 60 | Have fun! 61 | 62 | 63 | 64 | Environment 65 | ----------- 66 | 67 | To work with the Minum framework, it is required to have Java 21 or beyond installed. Also, 68 | the development has been done in Posix environments, like the Bash or Zsh shells, or Cygwin 69 | on Windows. 70 | 71 | Try this in your shell: 72 | 73 | ```shell 74 | javac -version 75 | ``` 76 | 77 | The result should be `javac 21` or higher. If it not, check 78 | out [Step-by-step guide to installing Java on Windows](development_handbook.md#step-by-step-guide-for-installing-java-on-windows) 79 | or [Java on Mac](development_handbook.md#java-on-mac) 80 | 81 | ***After changing environment variables, you must close and reopen your terminal to see the change*** 82 | 83 | Make sure to have the JAVA_HOME environment variable set. Test like this: 84 | 85 | ```shell 86 | echo $JAVA_HOME 87 | ``` 88 | 89 | The output should be the directory where Java is installed, but *not* the bin 90 | directory where java and javac live. Try this (this command changes directory to 91 | JAVA_HOME and then lists the files there): 92 | 93 | ```shell 94 | cd $JAVA_HOME 95 | ls 96 | ``` 97 | 98 | You should see results like: `bin conf include jmods legal lib release` 99 | 100 | This is why your `PATH` environment variable should include something like this: 101 | 102 | ```shell 103 | $JAVA_HOME/bin 104 | ``` -------------------------------------------------------------------------------- /docs/release_messages/version1.md: -------------------------------------------------------------------------------- 1 | I am happy to announce my minimalist zero-dependency web framework, Minum, is out of beta. 2 | http://github.com/byronka/minum 3 | 4 | You will be hard-pressed to find another modern project as obsessively minimalistic. Other frameworks will claim 5 | simplicity and minimalism and then, casually, mention they are built on a multitude of libraries. This follows 6 | self-imposed constraints, predicated on a belief that smaller and lighter is long-term better. 7 | 8 | Caveat emptor: This is a project by and for developers who know and like programming (rather than, let us say, 9 | configuring). It is written in Java, and presumes familiarity with the HTTP/HTML paradigm. 10 | 11 | Driving paradigms of this project: 12 | 13 | * ease of use 14 | * maintainability / sustainability 15 | * simplicity 16 | * performance 17 | * good documentation 18 | * good testing 19 | 20 | It requires Java 21, for its virtual threads (Project Loom) -------------------------------------------------------------------------------- /docs/simple_minum_program.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byronka/minum/7e1d83fb4927e5ca2f1d3231a21284c8de58f0f4/docs/simple_minum_program.jpg -------------------------------------------------------------------------------- /docs/todo/done/BUG_inconsistencies_in_DDPS_code.md: -------------------------------------------------------------------------------- 1 | There's too many inconsistent patterns in DDPS -------------------------------------------------------------------------------- /docs/todo/done/BUG_tests_added_to_wrong_suite.txt: -------------------------------------------------------------------------------- 1 | When reviewing the report of tests, noticed that the last test in a suite gets 2 | included in the next suite. Fix. -------------------------------------------------------------------------------- /docs/todo/done/a_user_should_be_able_to_add_photo_and_description.txt: -------------------------------------------------------------------------------- 1 | It should be possible for a user of the system to add a new photo with 2 | a description, and for that information to become available publicly 3 | (without requiring authentication). 4 | 5 | This is to start off an idea for a genealogical program. We will 6 | follow the typical iterative approach where we try to determine the 7 | simplest and smallest new functionality that aligns with our 8 | overarching need. 9 | 10 | Tasks: 11 | 1. ability to receive photos - requires reading form/multipart data DONE 12 | 2. simple web page to send a photo: required: short description (3-20 chars), long description (0-1000 chars), the photo itself 13 | 3. simple page to view all photos, with their short and long descriptions 14 | -------------------------------------------------------------------------------- /docs/todo/done/docs_paradigm_of_the_database.txt: -------------------------------------------------------------------------------- 1 | write essay on paradigm of our database - when to use it, when not, fat data vs thin, etc 2 | 3 | The database design for Minum is intended to prioritize simplicity and minimalism 4 | over all else. It does not provide the kind of safety you would expect with an 5 | ACID-compliant database. It does not have the cornucopia of features you would 6 | expect from a database handling enterprise-style risks. 7 | 8 | What it does have, however, is a nearly-absurd sparseness, good performance, and 9 | ease of use. This means for low-risk uses, of which there are quite a number, 10 | this small program might be just the ticket. -------------------------------------------------------------------------------- /docs/todo/done/docs_recommended_code_pattern.txt: -------------------------------------------------------------------------------- 1 | How the code should be organized if you intend to follow the overarching patterns here. 2 | 3 | WON'T DO -------------------------------------------------------------------------------- /docs/todo/done/docs_test_examining_logs.txt: -------------------------------------------------------------------------------- 1 | If you want to write a test that inspects the logs -------------------------------------------------------------------------------- /docs/todo/done/docs_testing_an_endpoint.txt: -------------------------------------------------------------------------------- 1 | How to test one, avoiding network as much as possible 2 | 3 | WON'T DO -------------------------------------------------------------------------------- /docs/todo/done/feature_cache_of_static_responses.txt: -------------------------------------------------------------------------------- 1 | done 9/29/2022 2 | 3 | Create a cache of static responses so the web server can quickly respond 4 | when it receives a request for something that only changes between restarts. 5 | 6 | Things like CSS files, JS scripts, images. 7 | 8 | make sure there is a working "resources/static" directory - actually test this thing out. -------------------------------------------------------------------------------- /docs/todo/done/feature_index_page.txt: -------------------------------------------------------------------------------- 1 | done 10/1/22 2 | 3 | Put together a highly graphical and interesting index page 4 | 5 | Maybe something with gif animation, or JavaScript? Better if 6 | it's lighter, maybe involve someone who knows colors and graphics better? 7 | 8 | Not really "highly graphical" or interesting, but whatevs. -------------------------------------------------------------------------------- /docs/user_story_checklist.txt: -------------------------------------------------------------------------------- 1 | User story code-review checklist: 2 | 3 | The following is used during story development as a reminder to the development team 4 | what is needed for high-quality software. 5 | 6 | [ ] risks carefully considered 7 | 8 | functional development and considerations 9 | documented thoughtfully 10 | [ ] classes 11 | [ ] methods 12 | [ ] tests 13 | [ ] unusual aspects documented within code 14 | [ ] READMEs 15 | [ ] developer documentation 16 | [ ] user documentation 17 | [ ] log entries added 18 | correctness 19 | [ ] unit tests written 20 | [ ] were the tests thorough, or only superficial? 21 | [ ] invariants applied - e.g. check(val > 10) 22 | [ ] integration tests written 23 | 24 | non-functionals: 25 | [ ] perf (what parts might be slow? Is it possible to create a low-level test?) 26 | [ ] security (might use a tool like Zap to walk through the system) 27 | [ ] accessibility 28 | [ ] minum.logging 29 | [ ] graceful degradation 30 | [ ] mobile-first 31 | 32 | white-box testing: 33 | [ ] static analysis considered 34 | [ ] should it be refactored? 35 | [ ] have you done a quick visual scan of the test log output for issues? 36 | 37 | rendered text is highly correct: 38 | [ ] rendered HTML is valid (through a tool like W3C's https://validator.w3.org/) 39 | [ ] dynamic parts are cleaned, e.g. using code like safeAttr(), safeHTML() 40 | [ ] CSS is valid (using a tool like W3C https://jigsaw.w3.org/css-validator/ ) 41 | 42 | static values and methods are well-designed. 43 | [] Any new or modified values must be: 44 | * true, literal constants. if any processing is required to build the constant, it's off the 45 | table, except for: 46 | * [null objects](https://en.wikipedia.org/wiki/Null_object_pattern), because the alternative is 47 | purely worse[^1]. 48 | * small helper utility methods that require no state - functional-style. 49 | * complex methods should not be static, because I may need to put logging in them, which is state. 50 | * Use a context object to hold items that have broader scope, such as 51 | logging, regular expressions, running threads I'll need to kill, ExecutorService, etc. 52 | * static factory methods are allowed, but they should receive ILogger so we can log. 53 | 54 | 55 | 56 | [^1]: It would require us to do context.emptyObjects().EmptyFoo() instead of Foo.EMPTY, a plainly 57 | worse outcome with minimal benefits. -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | Source directories 2 | ================== 3 | 4 | - main is the production code 5 | - test is the set of tests for the code in main -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/README.md: -------------------------------------------------------------------------------- 1 | Minum source code 2 | ================ 3 | 4 | These are the general functionalities provided by Minum. As you 5 | can see here, there is code for a database, for logging, for testing, 6 | and so on. 7 | 8 | There is documentation throughout the project. -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/database/DbException.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.database; 2 | 3 | import java.io.Serial; 4 | 5 | /** 6 | * Exceptions that occur in the {@link Db} 7 | */ 8 | public final class DbException extends RuntimeException { 9 | 10 | @Serial 11 | private static final long serialVersionUID = -9063971131447186027L; 12 | 13 | /** 14 | * A {@link RuntimeException} scoped to 15 | * the Minum database package. See {@link RuntimeException#RuntimeException(String)} 16 | */ 17 | public DbException(String message) { 18 | super(message); 19 | } 20 | 21 | /** 22 | * A {@link RuntimeException} scoped to 23 | * the Minum database package. See {@link RuntimeException#RuntimeException(String, Throwable)} 24 | */ 25 | public DbException(String message, Throwable cause) { 26 | super(message, cause); 27 | } 28 | 29 | /** 30 | * A {@link RuntimeException} scoped to 31 | * the Minum database package. See {@link RuntimeException#RuntimeException(Throwable)} 32 | */ 33 | public DbException(Throwable cause) { 34 | super(cause); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/database/README.md: -------------------------------------------------------------------------------- 1 | Database 2 | ======== 3 | 4 | A minimalist database. Strongly-typed in-memory collections, backed by eventually-synchronized 5 | disk persistence. See [Database JavaDocs](https://renomad.com/javadoc/com/renomad/minum/database/package-summary.html) -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/database/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This package contains classes for data persistence capabilities. 3 | * 4 | *
 5 |  * {@code
 6 |  * //------------------------
 7 |  * // Define the data type
 8 |  * //------------------------
 9 |  *
10 |  * public class PersonName extends DbData {
11 |  *
12 |  *     private long index;
13 |  *     private final String fullname;
14 |  *
15 |  *     public PersonName(Long index, String fullname) {
16 |  *         this.index = index;
17 |  *         this.fullname = fullname;
18 |  *     }
19 |  *
20 |  *     public static final PersonName EMPTY = new PersonName(0L, "");
21 |  *
22 |  *     // ... (several more lines. See PersonName.java in the tests directory)
23 |  *
24 |  * }
25 |  *
26 |  * //------------------------
27 |  * // Initialize the database
28 |  * //------------------------
29 |  *
30 |  * Db db = context.getDb("names", PersonName.EMPTY);
31 |  *
32 |  * //---------------------
33 |  * //  Add to the database
34 |  * //---------------------
35 |  *
36 |  * db.write(new PersonName(0L, "My Name"));
37 |  *
38 |  * //-------------------------------------------
39 |  * //  Get (read-only) by name from the database
40 |  * //-------------------------------------------
41 |  *
42 |  * PersonName foundPerson = SearchUtils.findExactlyOne(db.values().stream(), x -> x.getFullname().equals("My Name"));
43 |  *
44 |  * //------------------------------------------
45 |  * //  Get all the values (read-only) as a list
46 |  * //------------------------------------------
47 |  *
48 |  * PersonName allPersons = db.values().stream().toList()
49 |  *
50 |  * //--------------------------------
51 |  * //  Update a value in the database
52 |  * //--------------------------------
53 |  *
54 |  * foundPerson.setName("a new name");
55 |  * db.write(foundPerson);
56 |  *
57 |  * }
58 |  * 
59 | */ 60 | package com.renomad.minum.database; -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/htmlparsing/ParseNodeType.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.htmlparsing; 2 | 3 | /** 4 | * The different kinds of things in an HTML document. 5 | */ 6 | public enum ParseNodeType { 7 | /** 8 | * An HTML element. 9 | *

10 | * For example, a p (paragraph) or div (division) 11 | *

12 | */ 13 | ELEMENT, 14 | 15 | /** 16 | * String content inside an HTML element 17 | *

18 | * For example, {@code

Hi I am the content

} 19 | *

20 | */ 21 | CHARACTERS 22 | } -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/htmlparsing/ParsingException.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.htmlparsing; 2 | 3 | import java.io.Serial; 4 | 5 | /** 6 | * Thrown if a failure occurs parsing 7 | */ 8 | public final class ParsingException extends RuntimeException { 9 | 10 | @Serial 11 | private static final long serialVersionUID = 9158387443482452528L; 12 | 13 | /** 14 | * Construct an exception during parsing 15 | * @param message an informative message for recipients 16 | */ 17 | public ParsingException(String message) { 18 | super(message); 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/htmlparsing/README.md: -------------------------------------------------------------------------------- 1 | HTML parsing 2 | ============= 3 | 4 | A simplistic HTML5 parser. Useful for analysis and testing. 5 | 6 | See, for example, com.renomad.minum.web.FunctionalTesting.searchOne(), which is 7 | used in the tests at com.renomad.minum.FunctionalTests. 8 | 9 | Primary parsing code at HtmlParser.java. -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/htmlparsing/TagInfo.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.htmlparsing; 2 | 3 | import java.util.*; 4 | 5 | /** 6 | * tagname and attributes inside an HTML5 tag 7 | */ 8 | public final class TagInfo { 9 | 10 | private final TagName tagName; 11 | private final Map attributes; 12 | 13 | public TagInfo( 14 | TagName tagName, 15 | Map attributes 16 | ) { 17 | this.tagName = tagName; 18 | this.attributes = new HashMap<>(attributes); 19 | } 20 | 21 | /** 22 | * a null object 23 | */ 24 | public static final TagInfo EMPTY = new TagInfo(TagName.NULL, Map.of()); 25 | 26 | public TagName getTagName() { 27 | return tagName; 28 | } 29 | 30 | boolean containsAllAttributes(Set> entries) { 31 | return attributes.entrySet().containsAll(entries); 32 | } 33 | 34 | public String getAttribute(String key) { 35 | return attributes.get(key); 36 | } 37 | 38 | public Map getAttributes() { 39 | return new HashMap<>(attributes); 40 | } 41 | 42 | @Override 43 | public boolean equals(Object o) { 44 | if (this == o) return true; 45 | if (!(o instanceof TagInfo tagInfo)) return false; 46 | return tagName == tagInfo.tagName && Objects.equals(attributes, tagInfo.attributes); 47 | } 48 | 49 | @Override 50 | public int hashCode() { 51 | return Objects.hash(tagName, attributes); 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | return "TagInfo{" + 57 | "tagName=" + tagName + 58 | ", attributes=" + attributes + 59 | '}'; 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/htmlparsing/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts HTML text into a Java data structure. It processes quickly, and can provide 3 | * an ability to search for HTML elements by attributes. 4 | *

5 | * Here is an example of a test exercising the parser: 6 | *

7 | *
{@code
 8 |  *     @Test
 9 |  *     public void test_HtmlParser_Details1() {
10 |  *         String input = "
"; 11 | * var expected = List.of( 12 | * new HtmlParseNode( 13 | * ParseNodeType.ELEMENT, 14 | * new TagInfo(TagName.BR, Map.of("foo", "bar")), 15 | * List.of(), 16 | * "")); 17 | * List result = new HtmlParser().parse(input); 18 | * assertEquals(expected, result); 19 | * } 20 | * }
21 | *

22 | * Some of the testing library depends on this framework, such 23 | * as {@link com.renomad.minum.web.FunctionalTesting.TestResponse#searchOne(com.renomad.minum.htmlparsing.TagName, java.util.Map)}. 24 | * This is heavily used in the tests on Minum, as well as being available to applications, such as 25 | * this example from the Memoria project: 26 | *

27 | *
{@code
28 |  *        logger.test("GET the detail view of a person");
29 |  *         {
30 |  *             var response = ft.get("person?id=" + personId);
31 |  *             assertEquals(response.searchOne(TagName.H2, Map.of("class","lifespan-name")).innerText().trim(), "John Doe");
32 |  *             assertEquals(response.searchOne(TagName.SPAN, Map.of("class","lifespan-era")).innerText().trim(), "November 14, 1917 to March 19, 2003");
33 |  *         }
34 |  * }
35 | * @see examples of search utilities applying this code 36 | */ 37 | package com.renomad.minum.htmlparsing; -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/logging/ILogger.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.logging; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * Logging code interface 7 | */ 8 | public interface ILogger { 9 | 10 | /** 11 | * Logs helpful debugging information 12 | * @param msg a lambda for what is to be logged. example: () -> "Hello" 13 | */ 14 | void logDebug(ThrowingSupplier msg); 15 | 16 | /** 17 | * Logs helpful debugging information 18 | *

19 | * Similar to {@link #logDebug(ThrowingSupplier)} but used 20 | * for code that runs very often, requires extra calculation, or has 21 | * data of large size. 22 | *

23 | *

24 | * It is possible to disable trace logs and thus avoid performance impacts unless 25 | * the data is needed for deeper investigation. 26 | *

27 | * @param msg a lambda for what is to be logged. example: () -> "Hello" 28 | */ 29 | void logTrace(ThrowingSupplier msg); 30 | 31 | /** 32 | * Logs helpful debugging information inside threads 33 | * @param msg a lambda for what is to be logged. example: () -> "Hello" 34 | */ 35 | void logAsyncError(ThrowingSupplier msg); 36 | 37 | /** 38 | * This is for logging business-related topics 39 | *

40 | * This log type is expected to be printed least-often, and should 41 | * directly relate to a user action. An example would 42 | * be "New user created: alice" 43 | *

44 | * msg a lambda for what is to be logged. example: () -> "Hello" 45 | */ 46 | void logAudit(ThrowingSupplier msg); 47 | 48 | /** 49 | * When we are shutting down the system it is necessary to 50 | * explicitly stop the logger. 51 | * 52 | *

53 | * The logger has to stand apart from the rest of the system, 54 | * or else we'll have circular dependencies. 55 | *

56 | */ 57 | void stop(); 58 | 59 | /** 60 | * This method can be used to adjust the active log levels, which 61 | * is a mapping of keys of {@link LoggingLevel} to boolean values. 62 | * If the boolean value is true, that level of logging is enabled. 63 | */ 64 | Map getActiveLogLevels(); 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/logging/LoggingLevel.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.logging; 2 | 3 | /** 4 | * An enumeration of the levels of logging our system provides. 5 | */ 6 | public enum LoggingLevel { 7 | 8 | /** 9 | * Information useful for debugging. 10 | */ 11 | DEBUG, 12 | 13 | /** 14 | * Represents an error that occurs in a separate thread, so 15 | * that we are not able to catch it bubbling up 16 | */ 17 | ASYNC_ERROR, 18 | 19 | /** 20 | * Information marked as trace is pretty much entered for 21 | * the same reason as DEBUG - i.e. so we can see important 22 | * information about the running state of the program. The 23 | * only difference is that trace information is very voluminous. 24 | * That is, there's tons of it, and it could make it harder 25 | * to find the important information amongst a lot of noise. 26 | * For that reason, TRACE is usually turned off. 27 | */ 28 | TRACE, 29 | 30 | /** 31 | * Information marked audit is for business-related stuff. Like, 32 | * a new user being created. A photo being looked for. Stuff 33 | * closer to the user needs. 34 | */ 35 | AUDIT 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/logging/README.md: -------------------------------------------------------------------------------- 1 | Logging 2 | ======= 3 | 4 | These classes define minimalistic programs to enable decent logging. The performance is 5 | satisfactory, and there is nothing too crazy going on. 6 | 7 | Essentially, each call to a logger method will receive a closure following the 8 | `RunnableWithDescription` interface. It will pop that into the `LoggingActionQueue`, which 9 | will keep them in order as they are eventually output to standard out. -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/logging/TestLoggerException.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.logging; 2 | 3 | import java.io.Serial; 4 | 5 | /** 6 | * An implementation of {@link RuntimeException}, scoped 7 | * for the TestLogger. 8 | */ 9 | public final class TestLoggerException extends RuntimeException { 10 | 11 | @Serial 12 | private static final long serialVersionUID = 7590640788970680799L; 13 | 14 | /** 15 | * See {@link RuntimeException#RuntimeException(String)} 16 | */ 17 | public TestLoggerException(String message) { 18 | super(message); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/logging/TestLoggerQueue.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.logging; 2 | 3 | 4 | import java.io.Serial; 5 | import java.util.ArrayDeque; 6 | import java.util.concurrent.locks.ReentrantLock; 7 | 8 | /** 9 | * Used in {@link TestLogger} as a circular queue to store 10 | * the most recent log statements for analysis. 11 | */ 12 | public final class TestLoggerQueue extends ArrayDeque { 13 | 14 | @Serial 15 | private static final long serialVersionUID = -149106325553645154L; 16 | 17 | private final ReentrantLock queueLock; 18 | private final int capacity; 19 | 20 | public TestLoggerQueue(int capacity){ 21 | this.capacity = capacity; 22 | this.queueLock = new ReentrantLock(); 23 | } 24 | 25 | @Override 26 | public boolean add(String e) { 27 | queueLock.lock(); 28 | try { 29 | if (size() >= capacity) 30 | removeFirst(); 31 | return super.add(e); 32 | } finally { 33 | queueLock.unlock(); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/logging/ThrowingSupplier.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.logging; 2 | 3 | /** 4 | * a functional interface used in {@link ILogger}, allows exceptions 5 | * to bubble up. 6 | */ 7 | @FunctionalInterface 8 | public interface ThrowingSupplier{ 9 | 10 | T get() throws E; 11 | 12 | } -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/logging/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * These classes enable outputting messages during the program run, labeled to indicate 3 | * the category. It is able to do this without slowing down the system unduly. Start 4 | * by reviewing {@link com.renomad.minum.logging.ILogger} 5 | *

6 | * Examples: 7 | *

8 | *
 9 |  *     {@code
10 |  *     this.logger = context.getLogger();
11 |  *
12 |  *     logger.logDebug(() -> "an empty path was provided to writeString");
13 |  *
14 |  *     logger.logTrace(() -> String.format("client connected from %s", sw.getRemoteAddrWithPort()));
15 |  *
16 |  *     logger.logAsyncError(() -> String.format("Error while reading file: %s. %s", path, StacktraceUtils.stackTraceToString(e)));
17 |  *
18 |  *     logger.logAudit(() -> String.format("%s has posted a new video, %s, with short description of %s, size of %d",
19 |  *                authResult.user().getUsername(),
20 |  *                newFilename,
21 |  *                shortDescription,
22 |  *                countOfVideoBytes
23 |  *        ));
24 |  *     }
25 |  * 
26 | */ 27 | package com.renomad.minum.logging; -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Minum is a web library with all the components needed to build a web application, 3 | * including a web server and a database. 4 | */ 5 | package com.renomad.minum; -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/queue/AbstractActionQueue.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.queue; 2 | 3 | import com.renomad.minum.utils.RunnableWithDescription; 4 | import com.renomad.minum.utils.ThrowingRunnable; 5 | 6 | import java.util.concurrent.LinkedBlockingQueue; 7 | 8 | /** 9 | * This class provides the ability to pop items into 10 | * a queue thread-safely and know they'll happen later. 11 | *

12 | * For example, this is helpful for minum.logging, or passing 13 | * functions to a minum.database. It lets us run a bit faster, 14 | * since the I/O actions are happening on a separate 15 | * thread and the only time required is passing the 16 | * function of what we want to run later. 17 | */ 18 | public interface AbstractActionQueue { 19 | 20 | /** 21 | * Start the queue's processing 22 | */ 23 | AbstractActionQueue initialize(); 24 | 25 | /** 26 | * Adds something to the queue to be processed. 27 | *

28 | * An example: 29 | *

30 | *
31 |      * {@code   actionQueue.enqueue("Write person file to disk at " + filePath, () -> {
32 |      *             Files.writeString(filePath, pf.serialize());
33 |  *         });}
34 |      * 
35 | */ 36 | void enqueue(String description, ThrowingRunnable action); 37 | 38 | /** 39 | * Stops the action queue 40 | * @param count how many loops to wait before we crash it closed 41 | * @param sleepTime how long to wait in milliseconds between loops 42 | */ 43 | void stop(int count, int sleepTime); 44 | 45 | /** 46 | * This will prevent any new actions being 47 | * queued (by setting the stop flag to true and thus 48 | * causing an exception to be thrown 49 | * when a call is made to [enqueue]) and will 50 | * block until the queue is empty. 51 | */ 52 | void stop(); 53 | 54 | /** 55 | * Get the {@link java.util.Queue} of data that is supposed to get 56 | * processed. 57 | */ 58 | LinkedBlockingQueue getQueue(); 59 | 60 | /** 61 | * Indicate whether this has had its {@link #stop()} method completed. 62 | */ 63 | boolean isStopped(); 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/queue/ActionQueueKiller.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.queue; 2 | 3 | import com.renomad.minum.state.Context; 4 | import com.renomad.minum.logging.ILogger; 5 | import com.renomad.minum.utils.TimeUtils; 6 | 7 | /** 8 | * This class exists to properly kill off multiple action queues 9 | */ 10 | public final class ActionQueueKiller { 11 | 12 | private final Context context; 13 | private final ILogger logger; 14 | 15 | /** 16 | * If we were interrupted while attempting to cleanly kill the 17 | * action queues, this will be set true 18 | */ 19 | private boolean hadToInterrupt; 20 | 21 | public ActionQueueKiller(Context context) { 22 | this.context = context; 23 | this.logger = context.getLogger(); 24 | hadToInterrupt = false; 25 | } 26 | 27 | /** 28 | * Systematically stops and kills all the action queues that have been 29 | * instantiated in this call tree. 30 | */ 31 | public void killAllQueues() { 32 | logger.logDebug(() -> TimeUtils.getTimestampIsoInstant() + " Killing all queue threads. "); 33 | for (AbstractActionQueue aq = context.getActionQueueState().pollFromQueue(); aq != null ; aq = context.getActionQueueState().pollFromQueue()) { 34 | AbstractActionQueue finalAq = aq; 35 | finalAq.stop(); 36 | logger.logDebug(() -> TimeUtils.getTimestampIsoInstant() + " killing " + ((ActionQueue)finalAq).getQueueThread()); 37 | if (((ActionQueue)finalAq).getQueueThread() != null) { 38 | hadToInterrupt = true; 39 | System.out.println("had to interrupt " + finalAq); 40 | ((ActionQueue)finalAq).getQueueThread().interrupt(); 41 | } 42 | } 43 | } 44 | 45 | /** 46 | * A helpful indicator of whether this object was interrupted while 47 | * looping through the list of action queues 48 | * @return true If we were interrupted while attempting to cleanly kill the 49 | * action queues 50 | */ 51 | public boolean hadToInterrupt() { 52 | return hadToInterrupt; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/queue/ActionQueueState.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.queue; 2 | 3 | import java.util.Queue; 4 | import java.util.concurrent.LinkedBlockingQueue; 5 | 6 | /** 7 | * This class tracks the overall state of the {@link ActionQueue}s that 8 | * are in use throughout the system. We need one central place to 9 | * track these, so that at system shutdown we can close them all cleanly. 10 | *
11 | * As each ActionQueue gets created, it registers itself here. 12 | */ 13 | public class ActionQueueState { 14 | 15 | private final Queue aqQueue; 16 | 17 | public ActionQueueState() { 18 | aqQueue = new LinkedBlockingQueue<>(); 19 | } 20 | 21 | public String aqQueueAsString() { 22 | return aqQueue.toString(); 23 | } 24 | 25 | public void offerToQueue(AbstractActionQueue actionQueue) { 26 | aqQueue.offer(actionQueue); 27 | } 28 | 29 | public AbstractActionQueue pollFromQueue() { 30 | return aqQueue.poll(); 31 | } 32 | 33 | public boolean isAqQueueEmpty() { 34 | return aqQueue.isEmpty(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/queue/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This package contains classes for {@link com.renomad.minum.queue.ActionQueue}, which is 3 | * a background task processor. 4 | *

5 | * This enables programs to be run outside the normal request/response flow. For example, 6 | *

    7 | *
  • 8 | * a computationally-heavy or long-running process run nightly on the data. 9 | *
  • 10 | *
  • 11 | * An action by a user that could take a while to complete, such as compressing 12 | * a large number of files. 13 | *
  • 14 | *
15 | *

16 | *

17 | * A major difference between this and alternatives is its paradigm of lightness. Most 18 | * background processors concern themselves with the potential risks - like if power goes out 19 | * during a step, or if a remote endpoint fails, necessitating a retry. If addressing risks 20 | * such as those are prominent in your consideration, it may be worthwhile to use an 21 | * alternative. 22 | *

23 | *

24 | * But, if a prudent assessment is taken, in many cases the benefits of lightness and 25 | * minimalism are sufficiently valuable to make the tradeoff worthwhile. Minimalism 26 | * makes the system harder against bugs of all types. Everything has a tradeoff - using 27 | * large complex systems is likelier to cause subtle bugs of all kinds - correctness, 28 | * performance, security. 29 | *

30 | */ 31 | package com.renomad.minum.queue; -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/security/ForbiddenUseException.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.security; 2 | 3 | import java.io.Serial; 4 | 5 | /** 6 | * This is thrown when the user action is prevented by a 7 | * restriction we put on the system. 8 | *

9 | * For example, no user is allowed to send more than 10 | * Constants.MAX_READ_LINE_SIZE_BYTES to an endpoint. If 11 | * they do, we'll stop reading and throw this exception. 12 | *

13 | */ 14 | public final class ForbiddenUseException extends RuntimeException { 15 | 16 | @Serial 17 | private static final long serialVersionUID = -1588862919515625579L; 18 | 19 | /** 20 | * See {@link ForbiddenUseException} 21 | */ 22 | public ForbiddenUseException(String msg) { 23 | super(msg); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/security/ITheBrig.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.security; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Monitors the inmates who have misbehaved in our system. 7 | *

8 | * a client who needs addressing will be stored in a map in this class for as 9 | * long as required. After they have served their time, they 10 | * are released. 11 | *

12 | *

13 | * See also {@link UnderInvestigation} 14 | *

15 | */ 16 | public interface ITheBrig { 17 | // Regarding the BusyWait - indeed, we expect that the while loop 18 | // below is an infinite loop unless there's an exception thrown, that's what it is. 19 | ITheBrig initialize(); 20 | 21 | /** 22 | * Kills the infinite loop running inside this class. 23 | */ 24 | void stop(); 25 | 26 | /** 27 | * Put a client in jail for some infraction, for a specified time. 28 | * 29 | * @param clientIdentifier the client's address plus some feature identifier, like 1.2.3.4_too_freq_downloads 30 | * @param sentenceDuration length of stay, in milliseconds 31 | * @return whether we put this client in jail 32 | */ 33 | boolean sendToJail(String clientIdentifier, long sentenceDuration); 34 | 35 | /** 36 | * Return true if a particular client ip address is found 37 | * in the list. 38 | */ 39 | boolean isInJail(String clientIdentifier); 40 | 41 | /** 42 | * Get the current list of ip addresses that have been 43 | * judged as having carried out attacks on the system. 44 | */ 45 | List getInmates(); 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/security/Inmate.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.security; 2 | 3 | 4 | import com.renomad.minum.database.DbData; 5 | 6 | import java.util.Objects; 7 | 8 | import static com.renomad.minum.utils.SerializationUtils.deserializeHelper; 9 | import static com.renomad.minum.utils.SerializationUtils.serializeHelper; 10 | 11 | /** 12 | * Represents an inmate in our "jail". If someone does something we don't like, they do their time here. 13 | */ 14 | public final class Inmate extends DbData { 15 | 16 | /** 17 | * Builds an empty version of this class, except 18 | * that it has a current Context object 19 | */ 20 | public static final Inmate EMPTY = new Inmate(0L, "", 0L); 21 | private Long index; 22 | private final String clientId; 23 | private final Long releaseTime; 24 | 25 | /** 26 | * Represents an inmate in our "jail". If someone does something we don't like, they do their time here. 27 | * @param clientId a string representation of the client address plus a string representing the offense, 28 | * for example, "1.2.3.4_vuln_seeking" - 1.2.3.4 was seeking out vulnerabilities. 29 | * @param releaseTime the time, in milliseconds from the epoch, at which this inmate will be released 30 | * from the brig. 31 | */ 32 | public Inmate(Long index, String clientId, Long releaseTime) { 33 | this.index = index; 34 | this.clientId = clientId; 35 | this.releaseTime = releaseTime; 36 | } 37 | 38 | @Override 39 | public long getIndex() { 40 | return index; 41 | } 42 | 43 | @Override 44 | public void setIndex(long index) { 45 | this.index = index; 46 | } 47 | 48 | @Override 49 | public String serialize() { 50 | return serializeHelper(index, clientId, releaseTime); 51 | } 52 | 53 | @Override 54 | public Inmate deserialize(String serializedText) { 55 | final var tokens = deserializeHelper(serializedText); 56 | 57 | return new Inmate( 58 | Long.parseLong(tokens.get(0)), 59 | tokens.get(1), 60 | Long.parseLong(tokens.get(2))); 61 | } 62 | 63 | public String getClientId() { 64 | return clientId; 65 | } 66 | 67 | public Long getReleaseTime() { 68 | return releaseTime; 69 | } 70 | 71 | @Override 72 | public boolean equals(Object o) { 73 | if (this == o) return true; 74 | if (o == null || getClass() != o.getClass()) return false; 75 | Inmate inmate = (Inmate) o; 76 | return Objects.equals(index, inmate.index) && Objects.equals(clientId, inmate.clientId) && Objects.equals(releaseTime, inmate.releaseTime); 77 | } 78 | 79 | @Override 80 | public int hashCode() { 81 | return Objects.hash(index, clientId, releaseTime); 82 | } 83 | 84 | @Override 85 | public String toString() { 86 | return "Inmate{" + 87 | "index=" + index + 88 | ", clientId='" + clientId + '\'' + 89 | ", releaseTime=" + releaseTime + 90 | '}'; 91 | } 92 | } -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/security/MinumSecurityException.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.security; 2 | 3 | import java.io.Serial; 4 | 5 | /** 6 | * A {@link RuntimeException} scoped to the security package 7 | */ 8 | public final class MinumSecurityException extends RuntimeException { 9 | 10 | 11 | @Serial 12 | private static final long serialVersionUID = 1812161417927968297L; 13 | 14 | /** 15 | * See {@link RuntimeException#RuntimeException(String)} 16 | */ 17 | public MinumSecurityException(String message) { 18 | super(message); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/security/README.md: -------------------------------------------------------------------------------- 1 | Security 2 | ======== 3 | 4 | These classes keep an eye on misbehavior by attackers. For example, perhaps a client 5 | is trying to force the server to use an old, insecure version of TLS. That is suspicious, 6 | so we'll put them in time-out for 10 seconds, to slow them down. -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/security/UnderInvestigation.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.security; 2 | 3 | import com.renomad.minum.state.Constants; 4 | 5 | import java.util.List; 6 | import java.util.stream.Collectors; 7 | 8 | /** 9 | * Looking for bad actors in our system 10 | */ 11 | public final class UnderInvestigation { 12 | 13 | private final Constants constants; 14 | 15 | public UnderInvestigation(Constants constants) { 16 | this.constants = constants; 17 | } 18 | 19 | /** 20 | * Check for the kinds of error messages we usually see when an attacker is trying 21 | * their shenanigans on us. Returns true if we recognize anything. 22 | */ 23 | public String isClientLookingForVulnerabilities(String exceptionMessage) { 24 | List suspiciousErrors = constants.suspiciousErrors; 25 | return suspiciousErrors.stream().filter(exceptionMessage::contains).collect(Collectors.joining(";")); 26 | } 27 | 28 | 29 | /** 30 | * If the client is looking for paths like owa/auth/login.aspx, it means 31 | * they are probably some low-effort script scouring the web. In that case 32 | * the client is under control by a bad actor and we can safely block them. 33 | */ 34 | public String isLookingForSuspiciousPaths(String isolatedPath) { 35 | return constants.suspiciousPaths.stream().filter(isolatedPath::equals).collect(Collectors.joining(";")); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/security/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Code for handling the harsh internet environment. 3 | *

4 | * In the modern internet/web, sites undergo constant abuse. Scripts 5 | * are run constantly by attackers to seek out security vulnerabilities. 6 | *

7 | *

8 | * Many websites mitigate this by hiring services to protect themselves, placing 9 | * themselves in a more protected position one step back from the full internet. 10 | *

11 | *

12 | * This system was designed to be exposed out to the internet. To avoid some 13 | * of the most obvious attacks, the system looks for patterns indicating as 14 | * much. For example, there is no reason a user of the web application should 15 | * need to access an endpoint called ".env", but many insecure sites will allow 16 | * that file to be read, providing insight to attackers. Thus, attackers will 17 | * often request that file. If we see that request, it is assumed we are 18 | * getting a request from an attacker, and that ip address is put on a blacklist 19 | * for a short time. 20 | *

21 | */ 22 | package com.renomad.minum.security; -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/state/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This package holds classes that help hold necessary system state. 3 | *

4 | * There are just two classes in this package, but they are commonly used 5 | * throughout the Minum application. 6 | *

7 | *

8 | * One is {@link com.renomad.minum.state.Constants}, which contains constant 9 | * values that are used in various places. For example, the port that is 10 | * opened for secure endpoints. 11 | *

12 | *

13 | * Each value has a corresponding entry in the minum.config file, allowing 14 | * users to adjust parameters without needing to recompile. 15 | *

16 | *

17 | * The second class in this package is {@link com.renomad.minum.state.Context}, which 18 | * holds a reference to Constants and several other widely-needed items, so that 19 | * code in later parts of the call tree have access. 20 | *

21 | */ 22 | package com.renomad.minum.state; -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/templating/README.md: -------------------------------------------------------------------------------- 1 | Templating 2 | ========== 3 | 4 | See package-info.java for more detail on this package. -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/templating/RenderingResult.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.templating; 2 | 3 | /** 4 | * The result of rendering one of the {@link TemplateSection}s, used to build 5 | * up the full template. This is needed so we obtain information about whether our 6 | * user-supplied keys were applied. 7 | * @param renderedSection The result of rendering this section. In the case of a 8 | * {@link TemplateSection} that takes a key, this will be 9 | * the result of replacing that with what the user provided. 10 | * @param appliedKey In cases where a key was replaced with a value supplied by 11 | * the user, this will supply the key that was replaced. This is 12 | * useful to track how the supplied keys are being used in the template. 13 | */ 14 | public record RenderingResult(String renderedSection, String appliedKey) { } 15 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/templating/TemplateParseException.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.templating; 2 | 3 | import java.io.Serial; 4 | 5 | /** 6 | * Thrown when failing to parse something in a template 7 | */ 8 | public final class TemplateParseException extends RuntimeException { 9 | 10 | @Serial 11 | private static final long serialVersionUID = -8784893791425360686L; 12 | 13 | public TemplateParseException(String msg) { 14 | super(msg); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/templating/TemplateRenderException.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.templating; 2 | 3 | import java.io.Serial; 4 | 5 | /** 6 | * This exception is thrown when we try to render a string 7 | * template but fail to include a key for one of the key 8 | * values - that is, if the template is "hello {foo}", and 9 | * our map doesn't include a value for foo, this exception 10 | * will get thrown. 11 | */ 12 | public final class TemplateRenderException extends RuntimeException { 13 | 14 | @Serial 15 | private static final long serialVersionUID = -6403838479988560085L; 16 | 17 | public TemplateRenderException(String msg) { 18 | super(msg); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/templating/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Text templating capability. Mostly for HTML but useful 3 | * for any situation requiring substitution inside text. 4 | *

5 | * See {@link com.renomad.minum.templating.TemplateProcessor} 6 | *

7 | */ 8 | package com.renomad.minum.templating; -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/testing/README.md: -------------------------------------------------------------------------------- 1 | Testing 2 | ======= 3 | 4 | This package provides a minimal testing framework. -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/testing/RegexUtils.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.testing; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | 6 | /** 7 | * Handy helpers to make regular expression marginally 8 | * easier / more efficient, etc. 9 | */ 10 | public final class RegexUtils { 11 | 12 | /** 13 | * Helper to find a value in a string using a 14 | * Regex. Note, this is not nearly as performant, since 15 | * each call to this method will compile the regular 16 | * expression. 17 | * @return returns the first match found, or an empty string 18 | */ 19 | public static String find(String regex, String data) { 20 | Pattern pattern = Pattern.compile(regex); 21 | Matcher matcher = pattern.matcher(data); 22 | return matcher.find() ? matcher.group(0) : ""; 23 | } 24 | 25 | private RegexUtils() {} 26 | 27 | /** 28 | * Returns whether the regular expression matched the data 29 | * Note: This is a poor-performance method, mainly used 30 | * as a quick helper where performance concerns don't exist,since 31 | * each call to this method will compile the regular 32 | * expression. 33 | */ 34 | public static boolean isFound(String regex, String data) { 35 | Pattern pattern = Pattern.compile(regex); 36 | Matcher matcher = pattern.matcher(data); 37 | return matcher.find(); 38 | } 39 | 40 | /** 41 | * Find a value by regular expression, for testing 42 | *

43 | * A helper method to make things it easier to find a value in a string using a 44 | * Regex. This method is slow, since 45 | * each call will compile the regular 46 | * expression. 47 | *

48 | *

49 | * This version is similar to {@link #find(String, String)} except 50 | * that it allows you to specify a match group by name. 51 | * For example, here's a regex with a named match group, 52 | * in this example the name is "namevalue": 53 | *

54 | *

55 | *

56 |      *         {@code "\\bname\\b=\"(?.*?)\""}
57 |      *     
58 | *

59 | *

60 | * Thus, to use it here, you would search like this: 61 | *

62 | *

63 | *

64 |      *         {@code find("\\bname\\b=\"(?.*?)\"", data, "namevalue")}
65 |      *     
66 | *

67 | *

68 | * To summarize: in a regex, you specify a matching group by 69 | * surrounding it with parentheses. To name it, you insert 70 | * right after the opening parenthesis a question mark and 71 | * then a string literal surrounded by angle brackets 72 | *

73 | *

Important: the name of the match group must be alphanumeric - do 74 | * not use any special characters or punctuation

75 | * @return returns the first match found, or an empty string 76 | */ 77 | public static String find(String regex, String data, String matchGroupName) { 78 | Pattern pattern = Pattern.compile(regex); 79 | Matcher matcher = pattern.matcher(data); 80 | return matcher.find() ? matcher.group(matchGroupName) : ""; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/testing/StopwatchUtils.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.testing; 2 | 3 | /** 4 | * This class provides some tools for running a virtual stopwatch 5 | * while code is running, to examine code speed. 6 | *

7 | * example: 8 | *

9 | * 10 | *
11 |  {@code
12 |  final var timer = new StopWatch().startTimer();
13 |  for (var i = 1; i < 5; i++) {
14 |      doStuff();
15 |  }
16 |  final var time = timer.stopTimer();
17 |  printf("time taken was " + time " + milliseconds");
18 |  }
19 |  * 
20 | */ 21 | public final class StopwatchUtils { 22 | 23 | private long startTime; 24 | 25 | public StopwatchUtils startTimer() { 26 | this.startTime = System.currentTimeMillis(); 27 | return this; 28 | } 29 | 30 | public StopwatchUtils() { 31 | startTime = 0; 32 | } 33 | 34 | 35 | public long stopTimer() { 36 | final var endTime = System.currentTimeMillis(); 37 | return endTime - startTime; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/testing/TestFailureException.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.testing; 2 | 3 | import java.io.Serial; 4 | 5 | /** 6 | * Thrown when a test fails 7 | */ 8 | public final class TestFailureException extends RuntimeException{ 9 | 10 | @Serial 11 | private static final long serialVersionUID = 2937719847418284951L; 12 | 13 | /** 14 | * This constructor allows you to provide a text message 15 | * for insight into what exceptional situation took place. 16 | */ 17 | public TestFailureException(String msg) { 18 | super(msg); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/testing/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Automated software testing 3 | */ 4 | package com.renomad.minum.testing; -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/utils/ByteUtils.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.utils; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Handy helpers when working with bytes 7 | */ 8 | public final class ByteUtils { 9 | 10 | private ByteUtils() {} 11 | 12 | /** 13 | * A helper method to reduce some of the boilerplate 14 | * code when converting a list of bytes to an array. 15 | *

16 | * Often, we are gradually building up a list - the list takes 17 | * care of accommodating more elements as necessary. An 18 | * array, in contrast, is just a single size and doesn't 19 | * resize itself. It's much less convenient to use, so we 20 | * more often use lists. 21 | *

22 | */ 23 | public static byte[] byteListToArray(List result) { 24 | final var resultArray = new byte[result.size()]; 25 | for(int i = 0; i < result.size(); i++) { 26 | resultArray[i] = result.get(i); 27 | } 28 | return resultArray; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/utils/ConcurrentSet.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.utils; 2 | 3 | import java.util.Collections; 4 | import java.util.Iterator; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | 7 | /** 8 | * This uses a [ConcurrentHashMap] as its base. We store 9 | * the data in the keys only. We provide some syntactic sugar 10 | * so this seems similar to using a Set. 11 | *

12 | * This is a thread-safe data structure. 13 | */ 14 | public final class ConcurrentSet implements Iterable { 15 | 16 | private final ConcurrentHashMap map; 17 | 18 | public ConcurrentSet() { 19 | this.map = new ConcurrentHashMap<>(); 20 | } 21 | 22 | public void add(T element) { 23 | map.putIfAbsent(element, NullEnum.NULL); 24 | } 25 | 26 | public void remove(T element) { 27 | map.remove(element); 28 | } 29 | 30 | public int size() { 31 | return map.size(); 32 | } 33 | 34 | @Override 35 | public Iterator iterator() { 36 | return Collections.unmodifiableSet(map.keySet(NullEnum.NULL)).iterator(); 37 | } 38 | 39 | private enum NullEnum { 40 | /** 41 | * This is just a token for the value in the ConcurrentHashMap, since 42 | * we are only using the keys, never the values. 43 | */ 44 | NULL 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/utils/CryptoUtils.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.utils; 2 | 3 | import javax.crypto.SecretKeyFactory; 4 | import javax.crypto.spec.PBEKeySpec; 5 | import java.nio.charset.StandardCharsets; 6 | import java.security.spec.KeySpec; 7 | 8 | /** 9 | * Handy helpers for dealing with cryptographic functions 10 | */ 11 | public final class CryptoUtils { 12 | 13 | private CryptoUtils() { 14 | // cannot construct 15 | } 16 | 17 | /** 18 | * Converts an array of bytes to their corresponding hex string 19 | * @param bytes an array of bytes 20 | * @return a hex string of that array 21 | */ 22 | public static String bytesToHex(byte[] bytes) { 23 | StringBuilder hexString = new StringBuilder(); 24 | for (byte b : bytes) { 25 | String hex = Integer.toHexString(0xff & b); 26 | if (hex.length() == 1) hexString.append('0'); 27 | hexString.append(hex); 28 | } 29 | return hexString.toString(); 30 | } 31 | 32 | /** 33 | * Hash the input string with the provided PBKDF2 algorithm, and return a string representation 34 | * Note that the PBKDF2WithHmacSHA1 algorithm is specifically designed to take a long time, 35 | * to slow down an attacker. 36 | *

37 | * See docs/http_protocol/password_storage_cheat_sheet 38 | *

39 | */ 40 | public static String createPasswordHash(String password, String salt) { 41 | return createPasswordHash(password, salt, "PBKDF2WithHmacSHA1"); 42 | } 43 | 44 | static String createPasswordHash(String password, String salt, String algorithm) { 45 | final KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(StandardCharsets.UTF_8), 65536, 128); 46 | final SecretKeyFactory factory; 47 | 48 | try { 49 | factory = SecretKeyFactory.getInstance(algorithm); 50 | final byte[] hashed = factory.generateSecret(spec).getEncoded(); 51 | return bytesToHex(hashed); 52 | } catch (Exception e) { 53 | throw new UtilsException(e); 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/utils/FileReader.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.utils; 2 | 3 | import com.renomad.minum.logging.ILogger; 4 | 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.IOException; 7 | import java.io.RandomAccessFile; 8 | import java.nio.ByteBuffer; 9 | import java.nio.channels.FileChannel; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.util.Map; 13 | 14 | import static com.renomad.minum.utils.FileUtils.checkForBadFilePatterns; 15 | 16 | /** 17 | * Reads files from disk, optionally storing into a LRU cache. 18 | */ 19 | public final class FileReader implements IFileReader { 20 | 21 | private final Map lruCache; 22 | private final boolean useCacheForStaticFiles; 23 | private final ILogger logger; 24 | 25 | public FileReader(Map lruCache, boolean useCacheForStaticFiles, ILogger logger) { 26 | this.lruCache = lruCache; 27 | this.useCacheForStaticFiles = useCacheForStaticFiles; 28 | this.logger = logger; 29 | } 30 | 31 | @Override 32 | public byte[] readFile(String path) throws IOException { 33 | if (useCacheForStaticFiles && lruCache.containsKey(path)) { 34 | return lruCache.get(path); 35 | } 36 | 37 | try { 38 | checkForBadFilePatterns(path); 39 | } catch (Exception ex) { 40 | logger.logDebug(() -> String.format("Bad path requested at readFile: %s. Exception: %s", path, ex.getMessage())); 41 | return new byte[0]; 42 | } 43 | 44 | if (!Files.exists(Path.of(path))) { 45 | logger.logDebug(() -> String.format("No file found at %s, returning an empty byte array", path)); 46 | return new byte[0]; 47 | } 48 | 49 | return readTheFile(path, logger, useCacheForStaticFiles, lruCache); 50 | } 51 | 52 | static byte[] readTheFile(String path, ILogger logger, boolean useCacheForStaticFiles, Map lruCache) throws IOException { 53 | try (RandomAccessFile reader = new RandomAccessFile(path, "r"); 54 | ByteArrayOutputStream out = new ByteArrayOutputStream()) { 55 | FileChannel channel = reader.getChannel(); 56 | int bufferSize = 8 * 1024; 57 | if (bufferSize > channel.size()) { 58 | bufferSize = (int) channel.size(); 59 | } 60 | ByteBuffer buff = ByteBuffer.allocate(bufferSize); 61 | 62 | while (channel.read(buff) > 0) { 63 | out.write(buff.array(), 0, buff.position()); 64 | buff.clear(); 65 | } 66 | 67 | byte[] bytes = out.toByteArray(); 68 | if (bytes.length == 0) { 69 | logger.logTrace(() -> path + " filesize was 0, returning empty byte array"); 70 | return new byte[0]; 71 | } else { 72 | String s = path + " filesize was " + bytes.length + " bytes."; 73 | logger.logTrace(() -> s); 74 | 75 | if (useCacheForStaticFiles) { 76 | logger.logDebug(() -> "Storing " + path + " in the cache"); 77 | lruCache.put(path, bytes); 78 | } 79 | return bytes; 80 | } 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/utils/IFileReader.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.utils; 2 | 3 | import java.io.IOException; 4 | 5 | public interface IFileReader { 6 | 7 | /** 8 | * Reads a file from disk. 9 | *

10 | * Protects against some common negative cases: 11 | *

12 | *
    13 | *
  • If path is bad, log and return an empty byte array
  • 14 | *
  • If file does not exist, log and return an empty byte array
  • 15 | *
16 | */ 17 | byte[] readFile(String path) throws IOException; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/utils/InvariantException.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.utils; 2 | 3 | /** 4 | * An exception specific to our invariants. See {@link Invariants} 5 | */ 6 | @SuppressWarnings("serial") 7 | public final class InvariantException extends RuntimeException { 8 | public InvariantException(String msg) { 9 | super(msg); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/utils/Invariants.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.utils; 2 | 3 | /** 4 | * Utilities for asserting invariants within the code. 5 | *
6 | * The purpose here is to make firm statements about the code. This is 7 | * to help document the code, (e.g. at this point, x is the sum of the ...), 8 | * to include testing mindset in the code, and to guard against adding 9 | * bugs during maintenance. 10 | */ 11 | public final class Invariants { 12 | 13 | private Invariants() { 14 | // cannot construct 15 | } 16 | 17 | /** 18 | * Specify something which must be true. 19 | *

20 | * Throws an {@link InvariantException} if false 21 | * @param predicate the boolean expression that must be true at this point 22 | * @param message a message that will be included in the exception if this is false 23 | */ 24 | public static void mustBeTrue(boolean predicate, String message) { 25 | if (!predicate) { 26 | throw new InvariantException(message); 27 | } 28 | } 29 | 30 | /** 31 | * Specify something which must be false 32 | *

33 | * Throws an {@link InvariantException} if true 34 | * @param predicate the boolean expression that must be false at this point 35 | * @param message a message that will be included in the exception if this is true 36 | */ 37 | public static void mustBeFalse(boolean predicate, String message) { 38 | if (predicate) { 39 | throw new InvariantException(message); 40 | } 41 | } 42 | 43 | /** 44 | * specifies that the parameter must be not null. 45 | *

46 | * Throws an {@link InvariantException} if null. 47 | * @return the object if not null 48 | */ 49 | public static T mustNotBeNull(T object) { 50 | if (object == null) { 51 | throw new InvariantException("value must not be null"); 52 | } else { 53 | return object; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/utils/LRUCache.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.utils; 2 | 3 | import java.io.Serial; 4 | import java.util.LinkedHashMap; 5 | import java.util.Map; 6 | 7 | /** 8 | * A simple Least-Recently Used Cache 9 | * See LRU 10 | */ 11 | public final class LRUCache extends LinkedHashMap { 12 | 13 | @Serial 14 | private static final long serialVersionUID = -8687744696157499778L; 15 | 16 | // max number of entries allowed in this cache 17 | private static final int DEFAULT_MAX_ENTRIES = 100; 18 | private final int maxSize; 19 | 20 | @Override 21 | protected boolean removeEldestEntry(Map.Entry eldest) { 22 | return size() > maxSize; 23 | } 24 | 25 | private LRUCache(int maxSize) { 26 | // uses the same load factor as found in java.util.Hashmap.DEFAULT_LOAD_FACTOR 27 | super(maxSize + 1, 0.75f, true); 28 | this.maxSize = maxSize; 29 | } 30 | 31 | /** 32 | * Builds a map that functions as a least-recently used cache. 33 | * Sets the max size to DEFAULT_MAX_ENTRIES. If you want to specify the max size, 34 | * use the constructor at {@link #getLruCache(int)} 35 | *
36 | * Make sure, when using this, to assign it to a fully-defined 37 | * type, e.g. {@code Map foo = getLruCache()} 38 | * This is necessary since we provide this as a generic method, 39 | * and the assignment is what enables Java to determine 40 | * what types to build. 41 | */ 42 | public static Map getLruCache() { 43 | return getLruCache(DEFAULT_MAX_ENTRIES); 44 | } 45 | 46 | /** 47 | * Creates an LRUCache, allowing you to specify the max size. 48 | * Alternately, see {@link #getLruCache()}. 49 | *
50 | * Make sure, when using this, to assign it to a fully-defined 51 | * type, e.g. {@code Map foo = getLruCache(2)} 52 | * This is necessary since we provide this as a generic method, 53 | * and the assignment is what enables Java to determine 54 | * what types to build. 55 | */ 56 | public static Map getLruCache(int maxSize) { 57 | return new LRUCache<>(maxSize); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/utils/MyThread.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.utils; 2 | 3 | /** 4 | * This class exists just to avoid needing to handle 5 | * the exception when I use a regular Thread.sleep() 6 | */ 7 | public final class MyThread { 8 | 9 | private MyThread() { 10 | // cannot construct 11 | } 12 | 13 | /** 14 | * Same behavior as {@link Thread#sleep(long)}, but 15 | * wrapped so that it prints the exception's stacktrace 16 | * instead of letting it bubble up. 17 | * @param millis length of time in milliseconds. 18 | * @return false if the sleep succeeded without being interrupted 19 | */ 20 | public static boolean sleep(long millis) { 21 | try { 22 | Thread.sleep(millis); 23 | } catch (InterruptedException e) { 24 | handleInterrupted(e); 25 | return false; 26 | } 27 | return true; 28 | } 29 | 30 | static void handleInterrupted(InterruptedException e) { 31 | System.out.println("Interruption during MyThread.sleep: " + e); 32 | Thread.currentThread().interrupt(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/utils/README.md: -------------------------------------------------------------------------------- 1 | Utilities 2 | ========= 3 | 4 | There are a multitude of miscellaneous utilities here. 5 | 6 | Just a few: 7 | 8 | - ActionQueue: a thread-safe processing queue. Pop something in the hopper and it will eventually get done. 9 | - ByteUtils: manipulate bytes 10 | - Invariants: code that makes strong assertions in the production code of correctness. 11 | - MyThread: just lets you sleep on a thread without needing to handle the exception 12 | - ConcurrentSet: Convert a ConcurrentHashMap to a set 13 | - FileUtils: various programs for working with files -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/utils/RunnableWithDescription.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.utils; 2 | 3 | /** 4 | * This class is to improve maintainability in the system. It makes 5 | * possible reviewing the queue of actions and more easily understanding 6 | * the purpose of each Callable. 7 | */ 8 | public final class RunnableWithDescription implements ThrowingRunnable { 9 | 10 | private final String description; 11 | private final ThrowingRunnable r; 12 | 13 | /** 14 | * By constructing a {@link ThrowingRunnable} here, you can 15 | * provide a description of the runnable that will be reviewable 16 | * during debugging. 17 | */ 18 | public RunnableWithDescription(ThrowingRunnable r, String description) { 19 | this.description = description; 20 | this.r = r; 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | return description; 26 | } 27 | 28 | @Override 29 | public void run() { 30 | try { 31 | r.run(); 32 | } catch (Exception e) { 33 | throw new UtilsException(e); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/utils/SearchUtils.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.utils; 2 | 3 | import java.util.List; 4 | import java.util.Objects; 5 | import java.util.concurrent.Callable; 6 | import java.util.function.Predicate; 7 | import java.util.stream.Stream; 8 | 9 | import static com.renomad.minum.utils.Invariants.mustBeTrue; 10 | 11 | /** 12 | * Utilities for searching collections of data 13 | */ 14 | public final class SearchUtils { 15 | 16 | private SearchUtils() { 17 | // not meant to be instantiated 18 | } 19 | 20 | /** 21 | * This helper method will give you the one item in this list, or 22 | * null if there are none. If there's more than 1, it will throw 23 | * an exception. This is for those times when we absolutely expect 24 | * there to be just one of a thing in a database, like if we're searching 25 | * for Persons by id. 26 | * @param searchPredicate a {@link Predicate} run to search for an element in the stream. 27 | * @throws InvariantException if there are two or more results found 28 | */ 29 | public static T findExactlyOne(Stream streamOfSomething, Predicate searchPredicate) { 30 | return findExactlyOne(streamOfSomething, searchPredicate, () -> null); 31 | } 32 | 33 | /** 34 | * This is similar to {@link #findExactlyOne(Stream, Predicate)} except that you 35 | * can provide what gets returned if there are none found - so instead of 36 | * returning null, it can return something else. 37 | *
38 | * The values will be pre-filtered to skip any null values. 39 | *
40 | * @param alternate a {@link Callable} that will be run when no elements were found. 41 | * @param searchPredicate a {@link Predicate} run to search for an element in the stream. 42 | * @throws InvariantException if there are two or more results found 43 | */ 44 | public static T findExactlyOne(Stream streamOfSomething, Predicate searchPredicate, Callable alternate) { 45 | List listOfThings = streamOfSomething.filter(Objects::nonNull).filter(searchPredicate).toList(); 46 | mustBeTrue(listOfThings.isEmpty() || listOfThings.size() == 1, "Must be zero or one of this thing, or it's a bug. We found a size of " + listOfThings.size()); 47 | if (listOfThings.isEmpty()) { 48 | T returnValue; 49 | try { 50 | returnValue = alternate.call(); 51 | } catch (Exception ex) { 52 | throw new UtilsException(ex); 53 | } 54 | return returnValue; 55 | } else { 56 | return listOfThings.getFirst(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/utils/StacktraceUtils.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.utils; 2 | 3 | import java.io.PrintWriter; 4 | import java.io.StringWriter; 5 | import java.util.Arrays; 6 | import java.util.stream.Collectors; 7 | 8 | /** 9 | * Helper functions for manipulating stack traces. 10 | */ 11 | public final class StacktraceUtils { 12 | 13 | private StacktraceUtils() { 14 | // cannot construct 15 | } 16 | 17 | /** 18 | * grabs the stacktrace out of a {@link Throwable} as a string 19 | */ 20 | public static String stackTraceToString(Throwable ex) { 21 | StringWriter sw = new StringWriter(); 22 | PrintWriter pw = new PrintWriter(sw); 23 | ex.printStackTrace(pw); 24 | return sw.toString(); 25 | } 26 | 27 | /** 28 | * Converts an array of {@link StackTraceElement} to a single string, joining 29 | * them with a semicolon as delimiter. This way our stacktrace becomes a single line. 30 | */ 31 | public static String stackTraceToString(StackTraceElement[] elements) { 32 | return Arrays.stream(elements).map(StackTraceElement::toString).collect(Collectors.joining(";")); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/utils/ThrowingRunnable.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.utils; 2 | 3 | import com.renomad.minum.logging.ILogger; 4 | 5 | /** 6 | * This exists so that we are able to slightly better manage 7 | * exceptions in a highly threaded system. Here's the thing: 8 | * exceptions stop bubbling up at the thread invocation. If we 9 | * don't take care to deal with that in some way, we can easily 10 | * just lose the information. Something could be badly broken and 11 | * we could be totally oblivious to it. This interface is to 12 | * alleviate that situation. 13 | */ 14 | @FunctionalInterface 15 | public interface ThrowingRunnable { 16 | 17 | /** 18 | * The reason this throws an exception is so that lambdas 19 | * don't need to try-catch their thrown exceptions when they 20 | * use this. 21 | */ 22 | void run() throws Exception; 23 | 24 | static Runnable throwingRunnableWrapper(ThrowingRunnable throwingRunnable, ILogger logger) { 25 | return () -> { 26 | try { 27 | throwingRunnable.run(); 28 | } catch (Exception ex) { 29 | logger.logAsyncError(() -> StacktraceUtils.stackTraceToString(ex)); 30 | } 31 | }; 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/utils/TimeUtils.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.utils; 2 | 3 | import java.time.ZoneId; 4 | import java.time.ZonedDateTime; 5 | import java.time.format.DateTimeFormatter; 6 | import java.time.temporal.ChronoUnit; 7 | 8 | public final class TimeUtils { 9 | 10 | private TimeUtils() { 11 | // cannot construct 12 | } 13 | 14 | public static String getTimestampIsoInstant() { 15 | ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")); 16 | return getTimestampIsoInstantInner(now); 17 | } 18 | 19 | static String getTimestampIsoInstantInner(ZonedDateTime now) { 20 | return now.truncatedTo(ChronoUnit.MICROS).format(DateTimeFormatter.ISO_INSTANT); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/utils/UtilsException.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.utils; 2 | 3 | import java.io.Serial; 4 | 5 | public class UtilsException extends RuntimeException{ 6 | 7 | @Serial 8 | private static final long serialVersionUID = -7328610036937226491L; 9 | 10 | /** 11 | * See {@link RuntimeException#RuntimeException(Throwable)} 12 | */ 13 | public UtilsException(Throwable cause) { 14 | super(cause); 15 | } 16 | 17 | public UtilsException(String msg) { 18 | super(msg); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/utils/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Generally-useful utilities. 3 | */ 4 | package com.renomad.minum.utils; -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/BodyType.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | /** 4 | * The type of HTTP request body 5 | */ 6 | public enum BodyType { 7 | /** 8 | * a body exists but does not correspond to any known encoding 9 | */ 10 | UNRECOGNIZED, 11 | 12 | /** 13 | * Indicates there is no body 14 | */ 15 | NONE, 16 | 17 | /** 18 | * key-value pairs joined by ampersands, with the values encoded using 19 | * URL encoding, also known as percent encoding. 20 | * Look up application/x-www-form-urlencoded 21 | */ 22 | FORM_URL_ENCODED, 23 | 24 | /** 25 | * Splits up the content into partitions separated by a boundary value 26 | * that is specified by sender. Look up multipart/form-data 27 | */ 28 | MULTIPART 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/ContentDisposition.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * This class represents the information in the Content-Disposition 7 | * header of a multipart/form-data partition. Let's look at an example: 8 | * 9 | *

10 |  *  --i_am_a_boundary
11 |  *   Content-Type: text/plain
12 |  *   Content-Disposition: form-data; name="text1"
13 |  *
14 |  *   I am a value that is text
15 |  *   --i_am_a_boundary
16 |  *   Content-Type: application/octet-stream
17 |  *   Content-Disposition: form-data; name="image_uploads"; filename="photo_preview.jpg"
18 |  *  
19 | * 20 | *
21 | * In this example, there are two partitions, and each has a Content-Disposition header. 22 | * The first has a name of "text1" and the second has a name of "image_uploads". The 23 | * second partition also has a filename. 24 | *
25 | * This is useful for filtering partition data when an endpoint receives multipart data. 26 | * 27 | */ 28 | public final class ContentDisposition { 29 | private final String name; 30 | private final String filename; 31 | 32 | public ContentDisposition(String name, String filename) { 33 | 34 | this.name = name; 35 | this.filename = filename; 36 | } 37 | 38 | public String getName() { 39 | return name; 40 | } 41 | 42 | public String getFilename() { 43 | return filename; 44 | } 45 | 46 | @Override 47 | public boolean equals(Object o) { 48 | if (this == o) return true; 49 | if (o == null || getClass() != o.getClass()) return false; 50 | ContentDisposition that = (ContentDisposition) o; 51 | return Objects.equals(name, that.name) && Objects.equals(filename, that.filename); 52 | } 53 | 54 | @Override 55 | public int hashCode() { 56 | return Objects.hash(name, filename); 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return "ContentDisposition{" + 62 | "name='" + name + '\'' + 63 | ", filename='" + filename + '\'' + 64 | '}'; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/CountBytesRead.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | class CountBytesRead{ 4 | private int count; 5 | public void increment() {count += 1;} 6 | 7 | public void incrementBy(int i) { 8 | count += i; 9 | } 10 | 11 | public int getCount() { 12 | return count; 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/HttpServerType.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | /** 4 | * An enum to represent the mode of conversation for HTTP - 5 | * plain text or encrypted (TLS) 6 | */ 7 | public enum HttpServerType { 8 | /** 9 | * Represents a plain text, non-encrypted HTTP conversation 10 | */ 11 | PLAIN_TEXT_HTTP, 12 | 13 | /** 14 | * Represents an HTTPS encrypted TLS conversation 15 | */ 16 | ENCRYPTED_HTTP, 17 | 18 | /** 19 | * Indicates an unknown state 20 | */ 21 | UNKNOWN, 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/HttpVersion.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | /** 4 | * The HTTP versions we handle 5 | */ 6 | public enum HttpVersion { 7 | ONE_DOT_ZERO, ONE_DOT_ONE, NONE 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/IInputStreamUtils.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | 6 | /** 7 | * An interface for the {@link InputStreamUtils} implementation. 8 | * Solely created to provide better testing access 9 | */ 10 | interface IInputStreamUtils { 11 | 12 | /** 13 | * Reads a line of text, stopping when reading a newline. 14 | * Skips over carriage returns, so we read a HTTP_CRLF properly. 15 | *
16 | * If the stream ends, return what we have so far. 17 | */ 18 | String readLine(InputStream inputStream) throws IOException; 19 | 20 | /** 21 | * Reads "lengthToRead" bytes from the input stream 22 | */ 23 | byte[] read(int lengthToRead, InputStream inputStream); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/IRequest.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | /** 4 | * An interface for {@link Request}. Built 5 | * to enable easier testing on web handlers. 6 | * 7 | */ 8 | public interface IRequest { 9 | 10 | Headers getHeaders(); 11 | 12 | /** 13 | * Obtain information about the first line. An example 14 | * of a RequestLine is:
{@code GET /foo?bar=baz HTTP/1.1}
15 | */ 16 | RequestLine getRequestLine(); 17 | 18 | /** 19 | * This getter will process the body data fully on the first 20 | * call, and cache that data for subsequent calls. 21 | *
22 | * If there is a need to deal with very large data, such as 23 | * large images or videos, consider using {@link #getSocketWrapper()} 24 | * instead, which will allow fine-grained control over pulling 25 | * the bytes off the socket. 26 | *
27 | * For instance, if expecting a video (a large file), it may be prudent to store 28 | * the data into a file while it downloads, so that the server 29 | * does not need to hold the entire file in memory. 30 | */ 31 | Body getBody(); 32 | 33 | /** 34 | * Gets a string of the ip address of the client sending this 35 | * request. For example, "123.123.123.123" 36 | */ 37 | String getRemoteRequester(); 38 | 39 | /** 40 | * This getter is expected to be used for situations required finer-grained 41 | * control over the socket, such as when dealing with streaming input like a game or chat, 42 | * or receiving a very large file like a video. This will enable the user to 43 | * read and send on the socket - powerful, but requires care. 44 | *

45 | *
46 | * Note: This is an unusual method. 47 | *

48 | * It is an error to call this in addition to {@link #getBody()}. Use one or the other. 49 | * Mostly, expect to use getBody unless you really know what you are doing, such as 50 | * streaming situations or custom body protocols. 51 | *

52 | */ 53 | ISocketWrapper getSocketWrapper(); 54 | 55 | /** 56 | * This method provides an {@link Iterable} for getting the key-value pairs of a URL-encoded 57 | * body in an HTTP request. This method is intended to be used for situations 58 | * where the developer requires greater control, for example, if allowing large uploads 59 | * such as videos. 60 | *
61 | * If this extra level of control is not needed, the developer would benefit more from 62 | * using the {@link #getBody()} method, which is far more convenient. 63 | */ 64 | Iterable getUrlEncodedIterable(); 65 | 66 | /** 67 | * This method provides an {@link Iterable} for getting the partitions of a multipart-form 68 | * formatted body in an HTTP request. This method is intended to be used for situations 69 | * where the developer requires greater control, for example, if allowing large uploads 70 | * such as videos. 71 | *
72 | * If this extra level of control is not needed, the developer would benefit more from 73 | * using the {@link #getBody()} method, which is far more convenient. 74 | */ 75 | Iterable getMultipartIterable(); 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/IResponse.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * An interface for {@link Response}. Built 7 | * to enable easier testing on web handlers. 8 | */ 9 | public interface IResponse { 10 | /** 11 | * Any extra headers set on the Response by the developer 12 | */ 13 | Map getExtraHeaders(); 14 | 15 | /** 16 | * The {@link com.renomad.minum.web.StatusLine.StatusCode} set by the developer 17 | * for this Response. 18 | */ 19 | StatusLine.StatusCode getStatusCode(); 20 | 21 | /** 22 | * Returns the bytes of the Response body being sent to the client 23 | */ 24 | byte[] getBody(); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/IServer.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | import java.io.Closeable; 4 | import java.net.InetAddress; 5 | import java.net.ServerSocket; 6 | import java.util.concurrent.Future; 7 | 8 | /** 9 | * An interface for the {@link Server} implementation. 10 | * Solely created to provide better testing access 11 | */ 12 | public interface IServer extends Closeable { 13 | 14 | /** 15 | * This is where the central loop of our server is started 16 | */ 17 | void start(); 18 | 19 | /** 20 | * Get the string version of the address of this 21 | * server. See {@link InetAddress#getHostAddress()} 22 | */ 23 | String getHost(); 24 | 25 | /** 26 | * See {@link ServerSocket#getLocalPort()} 27 | */ 28 | int getPort(); 29 | 30 | /** 31 | * When we first create a SocketWrapper in Server, we provide it 32 | * a reference back to this object, so that it can call 33 | * this command. This class maintains a list of open 34 | * sockets in setOfSWs, and allows generated SocketWrappers 35 | * to deregister themselves from this list by using this method. 36 | */ 37 | void removeMyRecord(ISocketWrapper socketWrapper); 38 | 39 | /** 40 | * Obtain the {@link Future} of the central loop of this 41 | * server object 42 | */ 43 | Future getCentralLoopFuture(); 44 | 45 | HttpServerType getServerType(); 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/ISocketWrapper.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | import com.renomad.minum.state.Constants; 4 | 5 | import java.io.Closeable; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.net.SocketAddress; 9 | 10 | /** 11 | * This is the public interface to {@link ISocketWrapper}, whose 12 | * purpose is to make our lives easier when working with {@link java.net.Socket}. 13 | * Created to provide better testing access 14 | */ 15 | public interface ISocketWrapper extends Closeable { 16 | 17 | /** 18 | * Convert the provided string value into bytes 19 | * using the default charset, and send on the socket. 20 | */ 21 | void send(String msg) throws IOException; 22 | 23 | /** 24 | * Simply send the bytes on the socket, simple as that. 25 | */ 26 | void send(byte[] bodyContents) throws IOException; 27 | 28 | void send(byte[] bodyContents, int off, int len) throws IOException; 29 | 30 | void send(int b) throws IOException; 31 | 32 | /** 33 | * Sends a line of text, with carriage-return and line-feed 34 | * appended to the end, required for the HTTP protocol. 35 | */ 36 | void sendHttpLine(String msg) throws IOException; 37 | 38 | /** 39 | * Get the port of the server 40 | */ 41 | int getLocalPort(); 42 | 43 | /** 44 | * Returns a {@link SocketAddress}, which includes 45 | * the client's address and port 46 | */ 47 | SocketAddress getRemoteAddrWithPort(); 48 | 49 | /** 50 | * Returns a string of the remote host address without port 51 | */ 52 | String getRemoteAddr(); 53 | 54 | HttpServerType getServerType(); 55 | 56 | @Override 57 | void close() throws IOException; 58 | 59 | /** 60 | * Returns this socket's input stream for more granular access 61 | */ 62 | InputStream getInputStream(); 63 | 64 | /** 65 | * The hostname of the server, as set in the configuration 66 | * file of key HOST_NAME in {@link Constants#hostName} 67 | */ 68 | String getHostName(); 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/InvalidRangeException.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | /** 4 | * This exception is thrown if the range of bytes provided for a request 5 | * is improper - such as if the range values were negative, wrongly-ordered, 6 | * and so on. 7 | */ 8 | public class InvalidRangeException extends RuntimeException { 9 | public InvalidRangeException(String msg) { 10 | super(msg); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/LastMinuteHandlerInputs.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | 4 | /** 5 | * The parameters required to set up a handler that is run after everything else in the 6 | * web framework. See {@link WebFramework#registerLastMinuteHandler(ThrowingFunction)} 7 | * @param request a HTTP request from a user 8 | * @param response This is the response previously calculated. For context: this method is intended 9 | * to be run as a *special situation*, when the programmer wants a handler to run 10 | * at the "last minute" after previous processing, based on the response code. For 11 | * example, perhaps it is desired to override the response for 400 or 500 errors. 12 | *
13 | * It is valuable to get the previously-calculated response data, in case there is 14 | * something useful - like valuable error messages. 15 | */ 16 | public record LastMinuteHandlerInputs(IRequest request, IResponse response){} -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/Partition.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | import com.renomad.minum.utils.StringUtils; 4 | 5 | import java.util.Arrays; 6 | import java.util.Objects; 7 | 8 | /** 9 | * Represents a single partition in a multipart/form-data body response 10 | */ 11 | public final class Partition { 12 | 13 | private final Headers headers; 14 | private final byte[] content; 15 | private final ContentDisposition contentDisposition; 16 | 17 | public Partition(Headers headers, byte[] content, ContentDisposition contentDisposition) { 18 | this.headers = headers; 19 | this.content = content; 20 | this.contentDisposition = contentDisposition; 21 | } 22 | 23 | public Headers getHeaders() { 24 | return headers; 25 | } 26 | 27 | public ContentDisposition getContentDisposition() { 28 | return contentDisposition; 29 | } 30 | 31 | public byte[] getContent() { 32 | return content.clone(); 33 | } 34 | public String getContentAsString() { 35 | return StringUtils.byteArrayToString(content); 36 | } 37 | 38 | @Override 39 | public boolean equals(Object o) { 40 | if (this == o) return true; 41 | if (o == null || getClass() != o.getClass()) return false; 42 | Partition partition = (Partition) o; 43 | return Objects.equals(headers, partition.headers) && Arrays.equals(content, partition.content) && Objects.equals(contentDisposition, partition.contentDisposition); 44 | } 45 | 46 | @Override 47 | public int hashCode() { 48 | int result = Objects.hash(headers, contentDisposition); 49 | result = 31 * result + Arrays.hashCode(content); 50 | return result; 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | return "Partition{" + 56 | "headers=" + headers + 57 | ", contentDisposition=" + contentDisposition + 58 | '}'; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/PreHandlerInputs.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | /** 4 | * The input parameters that are required to add a pre-handler. See {@link WebFramework#registerPreHandler(ThrowingFunction)} 5 | * @param clientRequest the raw {@link Request} from the user 6 | * @param endpoint the endpoint that was properly chosen for the combination 7 | * of path and verb. 8 | */ 9 | public record PreHandlerInputs(IRequest clientRequest, ThrowingFunction endpoint, ISocketWrapper sw) { 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/README.md: -------------------------------------------------------------------------------- 1 | Web Code 2 | ======== 3 | 4 | This directory has the code necessary for our system to communicate using the Hyper-Text 5 | Transfer Protocol (HTTP). 6 | 7 | _FullSystem_ - This ties together much of the functionality here for startup. If you 8 | examine the code, particularly `FullSystem.start()`, you will see how many variables 9 | get initialized. Typically, you will use `FullSystem.initialize()` to kick off your 10 | system. 11 | 12 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/SetOfSws.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | import com.renomad.minum.logging.ILogger; 4 | import com.renomad.minum.utils.ConcurrentSet; 5 | 6 | import java.io.IOException; 7 | 8 | /** 9 | * This is a data structure of the live set of {@link ISocketWrapper} 10 | * in our system. It exists so that we can keep tabs on how many 11 | * open sockets we have, and can then find them all in one place 12 | * when we need to kill them at server shutdown. 13 | * @param nameOfSet This parameter is used to distinguish different servers' 14 | * list of sockets (e.g. 15 | * the server for 80 vs 443) 16 | */ 17 | record SetOfSws( 18 | ConcurrentSet socketWrappers, 19 | ILogger logger, 20 | String nameOfSet) { 21 | 22 | void add(ISocketWrapper sw) { 23 | socketWrappers().add(sw); 24 | int size = socketWrappers().size(); 25 | logger.logTrace(() -> nameOfSet + " added " + sw + " to SetOfSws. size: " + size); 26 | } 27 | 28 | void remove(ISocketWrapper sw) { 29 | socketWrappers().remove(sw); 30 | int size = socketWrappers().size(); 31 | logger.logTrace(() -> nameOfSet +" removed " + sw + " from SetOfSws. size: " + size); 32 | } 33 | 34 | void stopAllServers() throws IOException { 35 | for(ISocketWrapper s : socketWrappers()) { 36 | s.close(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/ThrowingConsumer.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | /** 4 | * This exists so that we are able to better manage 5 | * exceptions in a highly threaded system. 6 | *
7 | * Exceptions stop bubbling up at the thread invocation. If we 8 | * don't take care to deal with that in some way, we can easily 9 | * just lose the information. Something could be badly broken and 10 | * we could be totally oblivious to it. This interface is to 11 | * alleviate that situation. 12 | */ 13 | @FunctionalInterface 14 | public interface ThrowingConsumer { 15 | void accept(T t) throws Exception; 16 | 17 | } -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/ThrowingFunction.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | /** 4 | * This exists so that we are able to better manage 5 | * exceptions in a highly threaded system. 6 | *
7 | * exceptions stop bubbling up at the thread invocation. If we 8 | * don't take care to deal with that in some way, we can easily 9 | * just lose the information. Something could be badly broken and 10 | * we could be totally oblivious to it. This interface is to 11 | * alleviate that situation. 12 | */ 13 | @FunctionalInterface 14 | public interface ThrowingFunction { 15 | R apply(T t) throws Exception; 16 | 17 | } -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/UrlEncodedKeyValue.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * Represents a key-value pair with URL-encoding. 7 | * This is the format of data when the Request is sent with a 8 | * content-type header of application/x-www-form-urlencoded. 9 | */ 10 | public final class UrlEncodedKeyValue { 11 | private final String key; 12 | private final UrlEncodedDataGetter uedg; 13 | 14 | public UrlEncodedKeyValue(String key, UrlEncodedDataGetter uedg) { 15 | this.key = key; 16 | this.uedg = uedg; 17 | } 18 | 19 | public String getKey() { 20 | return key; 21 | } 22 | 23 | public UrlEncodedDataGetter getUedg() { 24 | return uedg; 25 | } 26 | 27 | @Override 28 | public boolean equals(Object o) { 29 | if (this == o) return true; 30 | if (o == null || getClass() != o.getClass()) return false; 31 | UrlEncodedKeyValue that = (UrlEncodedKeyValue) o; 32 | return Objects.equals(key, that.key) && Objects.equals(uedg, that.uedg); 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return Objects.hash(key, uedg); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/WebServerException.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.web; 2 | 3 | import java.io.Serial; 4 | 5 | /** 6 | * This is just a {@link RuntimeException} that is scoped 7 | * for our web server. 8 | */ 9 | public final class WebServerException extends RuntimeException{ 10 | 11 | @Serial 12 | private static final long serialVersionUID = 3964129858639403836L; 13 | 14 | /** 15 | * See {@link RuntimeException#RuntimeException(Throwable)} 16 | */ 17 | public WebServerException(Throwable cause) { 18 | super(cause); 19 | } 20 | 21 | public WebServerException(String msg) { 22 | super(msg); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/renomad/minum/web/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Code and data for HTTP web serving. 3 | *

4 | * Here is a typical "main" method for an application. The important thing to note is we are initializing {@link com.renomad.minum.web.FullSystem} and 5 | * using it to register endpoints. A more organized approach is to put the endpoint registrations 6 | * into another file. See the example project in the Minum codebase or any of the other example projects. 7 | *

8 | *
 9 |  * {@code
10 |  * package org.example;
11 |  *
12 |  * import com.renomad.minum.web.FullSystem;
13 |  * import com.renomad.minum.web.Response;
14 |  *
15 |  * import static com.renomad.minum.web.RequestLine.Method.GET;
16 |  *
17 |  * public class Main {
18 |  *
19 |  *     public static void main(String[] args) {
20 |  *         FullSystem fs = FullSystem.initialize();
21 |  *         fs.getWebFramework().registerPath(GET, "", request -> Response.htmlOk("

Hi there world!

")); 22 | * fs.block(); 23 | * } 24 | * } 25 | * } 26 | *
27 | *

28 | * Here's an example of a business-related function using authentication and the Minum database. This 29 | * code is extracted from the SampleDomain.java file in the src/test directory: 30 | *

31 | *
{@code
32 |  *       public IResponse formEntry(IRequest r) {
33 |  *         final var authResult = auth.processAuth(r);
34 |  *         if (! authResult.isAuthenticated()) {
35 |  *             return Response.buildLeanResponse(CODE_401_UNAUTHORIZED);
36 |  *         }
37 |  *         final String names = db
38 |  *                 .values().stream().sorted(Comparator.comparingLong(PersonName::getIndex))
39 |  *                 .map(x -> "
  • " + StringUtils.safeHtml(x.getFullname()) + "
  • \n") 40 | * .collect(Collectors.joining()); 41 | * 42 | * return Response.htmlOk(nameEntryTemplate.renderTemplate(Map.of("names", names))); 43 | * } 44 | * } 45 | *
    46 | */ 47 | package com.renomad.minum.web; -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module com.renomad.minum { 2 | 3 | exports com.renomad.minum.database; 4 | exports com.renomad.minum.htmlparsing; 5 | exports com.renomad.minum.logging; 6 | exports com.renomad.minum.queue; 7 | exports com.renomad.minum.security; 8 | exports com.renomad.minum.state; 9 | exports com.renomad.minum.templating; 10 | exports com.renomad.minum.testing; 11 | exports com.renomad.minum.utils; 12 | exports com.renomad.minum.web; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/certs/README.txt: -------------------------------------------------------------------------------- 1 | This keystore was found at https://github.com/openjdk/jdk/tree/master/test/jdk/javax/net/ssl/etc 2 | 3 | I have _no idea_ how to build this myself. I tried a variety of approaches, but when trying 4 | to follow their notes, they casually allude to 5 | 6 | "This can be generated using hacked (update the keytool source code so that 7 | it can be used for version 1 X.509 certificate) keytool command:" 8 | 9 | I mean ... hello? talk about unhelpful. 10 | 11 | I tried to run the commands listed in their readme, ignoring the note about a hacked tool, 12 | but I did not succeed. By simply copying their generated file, however, all was well. -------------------------------------------------------------------------------- /src/main/resources/certs/keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byronka/minum/7e1d83fb4927e5ca2f1d3231a21284c8de58f0f4/src/main/resources/certs/keystore -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/EqualsTests.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum; 2 | 3 | import com.renomad.minum.htmlparsing.HtmlParseNode; 4 | import com.renomad.minum.htmlparsing.ParseNodeType; 5 | import com.renomad.minum.htmlparsing.TagInfo; 6 | import com.renomad.minum.htmlparsing.TagName; 7 | import com.renomad.minum.security.Inmate; 8 | import com.renomad.minum.state.Constants; 9 | import com.renomad.minum.state.Context; 10 | import com.renomad.minum.web.*; 11 | import nl.jqno.equalsverifier.EqualsVerifier; 12 | import org.junit.After; 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | import static com.renomad.minum.testing.TestFramework.buildTestingContext; 20 | import static com.renomad.minum.testing.TestFramework.shutdownTestingContext; 21 | 22 | public class EqualsTests { 23 | 24 | private Context context; 25 | 26 | @Before 27 | public void init() { 28 | context = buildTestingContext("EqualsVerifier tests"); 29 | } 30 | 31 | @After 32 | public void cleanup() { 33 | shutdownTestingContext(context); 34 | } 35 | 36 | @Test 37 | public void equalsTest() { 38 | EqualsVerifier.forClass(Constants.class).verify(); 39 | 40 | EqualsVerifier.forClass(Response.class) 41 | .withIgnoredFields("outputGenerator") 42 | .verify(); 43 | 44 | EqualsVerifier.forClass(Body.class) 45 | .withPrefabValues(Headers.class, 46 | new Headers(List.of("a")), 47 | new Headers(List.of("b")) 48 | ).verify(); 49 | 50 | EqualsVerifier.forClass(RequestLine.class) 51 | .withPrefabValues(Context.class, 52 | new Context(null, new Constants()), 53 | new Context(null, new Constants()) 54 | ) 55 | .verify(); 56 | 57 | EqualsVerifier.simple().forClass(Inmate.class).verify(); 58 | 59 | EqualsVerifier.forClass(HtmlParseNode.class) 60 | .withPrefabValues(HtmlParseNode.class, 61 | new HtmlParseNode( 62 | ParseNodeType.ELEMENT, 63 | new TagInfo(TagName.BR, Map.of("foo", "bar")), 64 | List.of(), 65 | ""), 66 | new HtmlParseNode( 67 | ParseNodeType.ELEMENT, 68 | new TagInfo(TagName.A, Map.of("class", "biz")), 69 | List.of(), 70 | "")) 71 | .verify(); 72 | 73 | EqualsVerifier.forClass(TagInfo.class).verify(); 74 | 75 | EqualsVerifier.forClass(PathDetails.class).verify(); 76 | 77 | EqualsVerifier.forClass(ContentDisposition.class).verify(); 78 | 79 | EqualsVerifier.forClass(Partition.class).verify(); 80 | 81 | EqualsVerifier.forClass(UrlEncodedKeyValue.class).verify(); 82 | 83 | EqualsVerifier.forClass(Headers.class).verify(); 84 | 85 | 86 | } 87 | 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/htmlparsing/TagInfoTests.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.htmlparsing; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.Map; 6 | 7 | import static com.renomad.minum.testing.TestFramework.assertEquals; 8 | 9 | public class TagInfoTests { 10 | 11 | @Test 12 | public void happyPath() { 13 | TagInfo tagInfo = new TagInfo(TagName.P, Map.of("class", "foo")); 14 | assertEquals(tagInfo.toString(), "TagInfo{tagName=P, attributes={class=foo}}"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/logging/CustomLoggingLevel.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.logging; 2 | 3 | public enum CustomLoggingLevel { 4 | 5 | /** 6 | * For logging each request as it comes in 7 | */ 8 | REQUEST 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/logging/LoggingActionQueueTests.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.logging; 2 | 3 | import com.renomad.minum.state.Context; 4 | import com.renomad.minum.utils.MyThread; 5 | import com.renomad.minum.utils.RunnableWithDescription; 6 | import com.renomad.minum.utils.UtilsException; 7 | import org.junit.After; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | 11 | import static com.renomad.minum.testing.TestFramework.*; 12 | 13 | /** 14 | * These are tests for the background processor for the logging 15 | * system. It's particularly difficult to test because the normal 16 | * facilities for testing are unavailable. 17 | */ 18 | public class LoggingActionQueueTests { 19 | 20 | private Context context; 21 | 22 | @Before 23 | public void init() { 24 | context = buildTestingContext("TestLogger tests"); 25 | } 26 | 27 | @After 28 | public void cleanup() { 29 | shutdownTestingContext(context); 30 | } 31 | 32 | @Test 33 | public void testGetQueueThread() { 34 | var testQueue = new LoggingActionQueue("my test queue", context.getExecutorService(), context.getConstants()); 35 | testQueue.initialize(); 36 | MyThread.sleep(10); 37 | assertEquals(testQueue.getQueueThread().getName(), "my test queue"); 38 | } 39 | 40 | @Test 41 | public void testGetQueue() { 42 | var testQueue = new LoggingActionQueue("my test queue", context.getExecutorService(), context.getConstants()); 43 | testQueue.initialize(); 44 | testQueue.enqueue("Printing a test comment", () -> {MyThread.sleep(20);System.out.println("This is a test");}); 45 | testQueue.enqueue("Printing a test comment", () -> {MyThread.sleep(20);System.out.println("This is a test");}); 46 | 47 | assertEquals(testQueue.getQueue().peek().toString(), "Printing a test comment"); 48 | } 49 | 50 | @Test 51 | public void testErrorWhileRunningAction() { 52 | assertThrows(UtilsException.class, "java.lang.RuntimeException: This is a test exception", () -> LoggingActionQueue.runAction( 53 | new RunnableWithDescription(() -> { 54 | throw new RuntimeException("This is a test exception"); 55 | }, "Testing runAction"))); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/sampledomain/LruCacheTests.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.sampledomain; 2 | 3 | import com.renomad.minum.utils.LRUCache; 4 | import org.junit.Test; 5 | 6 | import java.util.Map; 7 | 8 | import static com.renomad.minum.testing.TestFramework.*; 9 | 10 | public class LruCacheTests { 11 | 12 | public LruCacheTests() { 13 | } 14 | 15 | /* 16 | * The LRU Cache (LRUCache) is a useful cache, based on 17 | * LinkedHashMap, which is in the Java standard library. 18 | * 19 | * Simply: When you add something to the cache, and there's 20 | * no more room, it just drops the oldest element. If you get 21 | * something, it should make that thing "new" again. 22 | */ 23 | @Test 24 | public void test_LRUCache_HappyPath() { 25 | Map lruCache = LRUCache.getLruCache(2); 26 | 27 | lruCache.put("a", new byte[]{1}); 28 | lruCache.put("body", new byte[]{2}); 29 | lruCache.put("c", new byte[]{3}); 30 | 31 | assertEquals(lruCache.size(), 2); 32 | assertEqualByteArray(lruCache.get("body"), new byte[]{2}); 33 | assertEqualByteArray(lruCache.get("c"), new byte[]{3}); 34 | assertTrue(lruCache.get("a") == null); 35 | } 36 | 37 | /** 38 | * if we get an item from the LRU cache, it should avoid being least recently used 39 | */ 40 | @Test 41 | public void test_GetItem_NotOldest() { 42 | Map lruCache = LRUCache.getLruCache(2); 43 | 44 | lruCache.put("a", new byte[]{1}); 45 | lruCache.put("body", new byte[]{2}); 46 | lruCache.get("a"); 47 | lruCache.put("c", new byte[]{3}); 48 | 49 | assertEquals(lruCache.size(), 2); 50 | assertEqualByteArray(lruCache.get("a"), new byte[]{1}); 51 | assertEqualByteArray(lruCache.get("c"), new byte[]{3}); 52 | assertTrue(lruCache.get("body") == null); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/sampledomain/PersonName.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.sampledomain; 2 | 3 | import com.renomad.minum.database.DbData; 4 | 5 | import static com.renomad.minum.utils.SerializationUtils.deserializeHelper; 6 | import static com.renomad.minum.utils.SerializationUtils.serializeHelper; 7 | 8 | public class PersonName extends DbData { 9 | 10 | private long index; 11 | private final String fullname; 12 | 13 | public PersonName(Long index, String fullname) { 14 | this.index = index; 15 | 16 | this.fullname = fullname; 17 | } 18 | 19 | public static final PersonName EMPTY = new PersonName(0L, ""); 20 | 21 | @Override 22 | public long getIndex() { 23 | return index; 24 | } 25 | 26 | @Override 27 | public void setIndex(long index) { 28 | this.index = index; 29 | } 30 | 31 | public String getFullname() { 32 | return fullname; 33 | } 34 | 35 | @Override 36 | public String serialize() { 37 | return serializeHelper(index, fullname); 38 | } 39 | 40 | @Override 41 | public PersonName deserialize(String serializedText) { 42 | 43 | final var tokens = deserializeHelper(serializedText); 44 | 45 | return new PersonName(Long.parseLong(tokens.get(0)), tokens.get(1)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/sampledomain/README.md: -------------------------------------------------------------------------------- 1 | Sample domain 2 | ============= 3 | 4 | It is important to understand that the following hints are critical to correctly-functioning code. 5 | 6 | See the sample files here as a template for developing your own business needs. PersonName is just 7 | sample data - notice how it supports database persistence by implementing DbData. Check 8 | the function implementations for `getIndex`, `serialize`, and `deserialize` as good examples for your own data. 9 | 10 | Also note key aspects of the SampleDomain class, which is the code that uses PersonName. It 11 | uses a Db class for each type of data it uses (in this case, that is PersonName). 12 | 13 | The functions in SampleDomain follow a signature of: 14 | 15 | public Response FUNCTION_NAME_HERE (Request r) 16 | 17 | Just as it looks, these functions receive the HTTP "request" information and return a HTTP "response". See 18 | those classes for more information. 19 | 20 | Note that SampleDomain is also demonstrating usage of the AuthUtils class. This is able to determine 21 | authentication by examining the headers in the HTTP request. 22 | 23 | See how the class and related method are "registered" in the class titled "minum.TheRegister". Use 24 | the same patterns in your own code to register each new business need. 25 | 26 | This should keep your systems plain and maintainable. -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/sampledomain/auth/AuthResult.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.sampledomain.auth; 2 | 3 | import java.time.ZonedDateTime; 4 | 5 | /** 6 | * This data structure contains important information about 7 | * a particular person's authentication. Like, are they 8 | * currently authenticated? 9 | */ 10 | public record AuthResult(Boolean isAuthenticated, ZonedDateTime creationDate, User user) { 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/sampledomain/auth/LoginResult.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.sampledomain.auth; 2 | 3 | public record LoginResult(LoginResultStatus status, User user) { 4 | } 5 | -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/sampledomain/auth/LoginResultStatus.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.sampledomain.auth; 2 | 3 | public enum LoginResultStatus { 4 | 5 | SUCCESS, 6 | 7 | NO_USER_FOUND, 8 | 9 | DID_NOT_MATCH_PASSWORD 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/sampledomain/auth/README.md: -------------------------------------------------------------------------------- 1 | These files provide some basic authentication capabilities for the sample project -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/sampledomain/auth/RegisterResult.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.sampledomain.auth; 2 | 3 | public record RegisterResult(RegisterResultStatus status, User newUser) {} 4 | -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/sampledomain/auth/RegisterResultStatus.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.sampledomain.auth; 2 | 3 | public enum RegisterResultStatus { 4 | 5 | SUCCESS, 6 | 7 | ALREADY_EXISTING_USER 8 | } 9 | -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/sampledomain/auth/User.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.sampledomain.auth; 2 | 3 | import com.renomad.minum.database.DbData; 4 | 5 | import static com.renomad.minum.utils.SerializationUtils.deserializeHelper; 6 | import static com.renomad.minum.utils.SerializationUtils.serializeHelper; 7 | 8 | /** 9 | * A data structure representing authentication information for a user. 10 | */ 11 | public class User extends DbData { 12 | 13 | private long id; 14 | private final String username; 15 | private final String hashedPassword; 16 | private final String salt; 17 | private final String currentSession; 18 | 19 | /** 20 | * @param id the unique identifier for this record 21 | * @param username a user-chosen unique identifier for this record (system must not allow two of the same username) 22 | * @param hashedPassword the hash of a user's password 23 | * @param salt some randomly-generated letters appended to the user's password. This is 24 | * to prevent dictionary attacks if someone gets access to the database contents. 25 | * See "Salting" in docs/http_protocol/password_storage_cheat_sheet.txt 26 | * @param currentSession If this use is currently authenticated, there will be a {@link SessionId} for them 27 | */ 28 | public User(long id, String username, String hashedPassword, String salt, String currentSession) { 29 | this.id = id; 30 | this.username = username; 31 | this.hashedPassword = hashedPassword; 32 | this.salt = salt; 33 | this.currentSession = currentSession; 34 | } 35 | 36 | public static final User EMPTY = new User(0L, "", "", "", null); 37 | 38 | @Override 39 | public long getIndex() { 40 | return id; 41 | } 42 | 43 | @Override 44 | public void setIndex(long index) { 45 | this.id = index; 46 | } 47 | 48 | public Long getId() { 49 | return id; 50 | } 51 | 52 | public String getUsername() { 53 | return username; 54 | } 55 | 56 | public String getHashedPassword() { 57 | return hashedPassword; 58 | } 59 | 60 | public String getSalt() { 61 | return salt; 62 | } 63 | 64 | public String getCurrentSession() { 65 | return currentSession; 66 | } 67 | 68 | @Override 69 | public String serialize() { 70 | return serializeHelper(id, username, hashedPassword, salt, currentSession); 71 | } 72 | 73 | @Override 74 | public User deserialize(String serializedText) { 75 | final var tokens = deserializeHelper(serializedText); 76 | return new User( 77 | Long.parseLong(tokens.get(0)), 78 | tokens.get(1), 79 | tokens.get(2), 80 | tokens.get(3), 81 | tokens.get(4) 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/sampledomain/auth/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This package enables authentication and authorization. 3 | *

    4 | *
    5 |  * authentication: do we know who you are?
    6 |  * authorization: are you allowed to access (data/page/whatever)?
    7 |  * 
    8 | */ 9 | package com.renomad.minum.sampledomain.auth; -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/sampledomain/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This minum.sampledomain is here as the standard template for how 3 | * to build any domain-oriented code. Here, domain means 4 | * your need - are you building a forum? A game? Whatever it 5 | * is that relies on the underlying web framework to give 6 | * it access to web requests and responses. 7 | */ 8 | package com.renomad.minum.sampledomain; -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/sampledomain/photo/Photograph.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.sampledomain.photo; 2 | 3 | import com.renomad.minum.database.DbData; 4 | 5 | import static com.renomad.minum.utils.SerializationUtils.deserializeHelper; 6 | import static com.renomad.minum.utils.SerializationUtils.serializeHelper; 7 | 8 | public class Photograph extends DbData { 9 | 10 | private long index; 11 | private final String photoUrl; 12 | private final String shortDescription; 13 | private final String description; 14 | 15 | public Photograph(long index, String photoUrl, String shortDescription, String description) { 16 | this.index = index; 17 | this.photoUrl = photoUrl; 18 | this.shortDescription = shortDescription; 19 | this.description = description; 20 | } 21 | 22 | public static final Photograph EMPTY = new Photograph(0L, "", "", ""); 23 | 24 | @Override 25 | public long getIndex() { 26 | return index; 27 | } 28 | 29 | @Override 30 | public void setIndex(long index) { 31 | this.index = index; 32 | } 33 | 34 | @Override 35 | public String serialize() { 36 | return serializeHelper(index, photoUrl, shortDescription, description); 37 | } 38 | 39 | public String getPhotoUrl() { 40 | return photoUrl; 41 | } 42 | 43 | public String getShortDescription() { 44 | return shortDescription; 45 | } 46 | 47 | public String getDescription() { 48 | return description; 49 | } 50 | 51 | @Override 52 | public Photograph deserialize(String serializedText) { 53 | final var tokens = deserializeHelper(serializedText); 54 | 55 | return new Photograph( 56 | Long.parseLong(tokens.get(0)), 57 | tokens.get(1), 58 | tokens.get(2), 59 | tokens.get(3)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/sampledomain/photo/README.md: -------------------------------------------------------------------------------- 1 | Photo 2 | ===== 3 | 4 | This package contains files needed to support the capability of receiving and displaying photos and 5 | accompanying text. -------------------------------------------------------------------------------- /src/test/java/com/renomad/minum/sampledomain/photo/Video.java: -------------------------------------------------------------------------------- 1 | package com.renomad.minum.sampledomain.photo; 2 | 3 | 4 | import com.renomad.minum.database.DbData; 5 | 6 | import static com.renomad.minum.utils.SerializationUtils.deserializeHelper; 7 | import static com.renomad.minum.utils.SerializationUtils.serializeHelper; 8 | 9 | public class Video extends DbData