Overview
Sinobu is not obsolete framework but utility. It acts as an extremely condensed facade for common development tasks, designed to be intuitive and efficient.
Weighing in at approximately 120 KB with zero external dependencies, Sinobu prioritizes lightweight deployment and avoids dependency conflicts. Despite its small size, its various operations are engineered for high performance, often rivaling or exceeding competing specialized libraries (see Benchmark).
This library aims to simplify and consolidate functionalities frequently encountered in real-world projects, making them easier and safer to use. Key areas covered include:
- Dependency Injection
- Object lifecycle management
- Property based object modeling
- HTTP(S) Client
- Web Socket Client
- JSON
- HTML (Tag Soup Tolerant)
- XML
- Reactive Programming (Rx)
- Asynchronous & Parallel processing
- Multilingualization
- Template Engine (Mustache Syntax)
- Dynamic Extensibility / Plugin System
- Object persistence
- Logging (Garbage-Less)
- Virtual Job Scheduler
- Task Scheduling (Cron Syntax)
Sinobu adheres to core principles ensuring ease of use, safety, and efficiency.
Futures
Sinobu's design philosophy is guided by several core principles, aimed at delivering a powerful yet minimalistic development experience:
π Lightweight
Sinobu maintains a minimal footprint (approximately 120 KB) with absolutely no external dependencies. This design choice results in faster startup times, significantly smaller deployment packages, and complete avoidance of dependency conflicts β an ideal fit for embedded systems, microservices, or any environment where lean execution is key.
β‘ High Performance
Performance is at the heart of every component. Internal operations are implemented with a focus on low memory overhead and high throughput. Most allocations are short-lived and garbage-friendly, which makes Sinobu suitable even for latency-sensitive systems. See the benchmark section for in-depth comparative results.
β¨ Simplicity
The API surface is deliberately kept minimal and expressive. Developers can often
accomplish complex tasks using a few static methods on the I
facade class. This
reduces boilerplate, encourages readability, and lowers the learning curve for new
contributors.
void access() {
I.http("http://xxx.com/", XML.class).to(html -> {
String name = html.find("#user").text();
});
}
π₯οΈ Multifunction
Despite its small size, Sinobu is packed with a wide array of capabilities. These features are not isolated; they are thoughtfully composed so that they work together seamlessly, forming a cohesive and versatile toolkit.
β»οΈ Reactivity
Sinobu embraces reactivity as a foundation for composing asynchronous, event-driven
logic. The Signal
class acts as a reactive primitive that allows values to
propagate through a declarative flow of transformations, observers, and combinators.
π‘οΈ Type Safety
All APIs are designed to leverage Javaβs static type system to the fullest. This promotes correctness, allows for confident refactoring, and minimizes the risk of runtime errors.
How to install
It is probably easiest to use a build tool such as Maven or Gradle. Replace `` with the desired version number.
<dependency>
<groupId>com.github.teletha</groupId>
<artifactId>sinobu</artifactId>
<version>VERSION<version>
</dependency>
Getting Involved
Sinobu is an open-source project hosted on GitHub. Contributions, bug reports, and feature requests are welcome.
- Repository:https://github.com/teletha/sinobu
- Issues: Please report bugs or suggest features via GitHub Issues.
- Contributions: Pull requests are welcome! Please ensure code style consistency and include tests where applicable.
Concept
In Sinobu, lifestyle refers to the way an object is created and managed, corresponding to the scope in terms of DI containers such as SpringFramework and Guice, but without the process of registering with the container or destroying the object.
Creating an object
In Java, it is common to use the new operator on the constructor to create a new object. In many cases, this is sufficient, but in the following situations, it is a bit insufficient.
- To manage the number of objects to be created.
- To create objects associated with a specific context.
- To generate objects with complex dependencies.
- The type of the object to be generated is not statically determined.
While DI containers such as SpringFramework or Guice are commonly used to deal with such problems, Sinobu comes with its own very simple DI container. The following code shows the creation of an object using DI container.
void createObject() {
class Tweet {
}
Tweet one = I.make(Tweet.class);
assert one != null;
}
As you can see from the above code, there is no actual container object; Sinobu has
only one global container in the JVM, and that object cannot be accessed directly. In
order to create an object from a container, we need to call I#make(Class)
.
Defining lifestyle
In order to define a lifestyle, you need to implement Lifestyle interface. This interface is essentially equivalent to Callable. It is called when container requests the specific type. It makes the following 3 decisions:
- Which class to instantiate actually.
- How to instantiate it.
- How to manage the instances.
Sinobu defines two lifestyles that are frequently used. One is the prototype pattern and the other is the singleton pattern.
Prototype
The default lifestyle is Prototype, it creates a new instance on demand. This is applied automatically and you have to configure nothing.
public void prototype() {
class Tweet {
}
Tweet one = I.make(Tweet.class);
Tweet other = I.make(Tweet.class);
assert one != other; // two different instances
}
Singleton
The other is the singleton lifestyle, which keeps a single instance in the JVM and always returns it. This time, the lifestyle is applied with annotations when defining the class.
public void singleton() {
@Managed(Singleton.class)
class Tweet {
}
Tweet one = I.make(Tweet.class);
Tweet other = I.make(Tweet.class);
assert one == other; // same instance
}
Custom lifestyle
You can also implement lifestyles tied to specific contexts. Custom class requires to
implement the Lifestyle interface and receive the requested type in the constructor.
I'm using I#prototype(Class)
here to make Dependency Injection work, but you
can use any instantiation technique.
class PerThread<T> implements Lifestyle<T> {
private final ThreadLocal<T> local;
PerThread(Class<T> requestedType) {
// use sinobu's default instance builder
Lifestyle<T> prototype = I.prototype(requestedType);
// use ThreadLocal as contextual instance holder
local = ThreadLocal.withInitial(prototype::get);
}
public T call() throws Exception {
return local.get();
}
}
public void perThread() {
@Managed(PerThread.class)
class User {
}
// create contextual user
User user1 = I.make(User.class);
User user2 = I.make(User.class);
assert user1 == user2; // same
new Thread(() -> {
User user3 = I.make(User.class);
assert user1 != user3; // different
}).start();
}
Builtin lifestyles
Sinobu has built-in defined lifestyles for specific types.
Applying lifestyle
To apply a non-prototype lifestyle, you need to configure each class individually. There are two ways to do this.
Use Managed annotation
One is to use Managed
annotation. This method is useful if you want to apply
lifestyle to classes that are under your control.
@Managed(Singleton.class)
class UnderYourControl {
}
Use Lifestyle extension
Another is to load the Lifestyle implementation. Sinobu has a wide variety of extension points, and Lifestyle is one of them. This method is useful if you want to apply lifestyle to classes that are not under your control.
class GlobalThreadPool implements Lifestyle<Executor> {
private static final Executor pool = Executors.newCachedThreadPool();
public Executor call() throws Exception {
return pool;
}
}
public void loadLifestyle() {
I.load(GlobalThreadPool.class);
}
Concept
Dependency Injection (DI) is a mechanism that solves various problems related to component dependencies in 'a nice way'. Component dependency refers to the relationship from upper layer to lower layer, such as Controller β Service β Repository in a general layered architecture. 'A nice way' means that the framework will take care of the problem without the developer having to work hard manually.
In modern Java application development, DI is an almost indispensable mechanism. The detailed explanation of the DI concept is left to another website.
DI Container
Unlike other DI frameworks, there is no explicit DI container in Sinobu. It has only one container implicitly inside, but the user is not aware of it. Therefore, there is also no need to define dependencies explicitly by means of code or external files. All dependencies are automatically resolved based on type.
Injection Type
Commonly, there are four main types in which a client can receive injected services.
- Constructor injection, where dependencies are provided through a client's class constructor.
- Setter injection, where the client exposes a setter method which accepts the dependency.
- Field injection, where the client exposes a field which accepts the dependency.
- Interface injection, where the dependency's interface provides an injector method that will inject the dependency into any client passed to it.
Sinobu supports constructor injection exclusively. Other forms of injection are intentionally not supported, as they compromise object safety and are unlikely to be supported in the future.
Constructor Injection
The most common form of dependency injection is for a class to request its dependencies through its constructor. This ensures the client is always in a valid state, since it cannot be instantiated without its necessary dependencies.
public void objectInjection() {
class Injectable {
}
class Injection {
private Injectable value;
Injection(Injectable injectable) {
this.value = injectable;
}
}
assert I.make(Injection.class).value != null;
}
Injectable Type
Any type can be injectable, but there are a few types that receive special treatment.
- Primitives - A default value (0 for int, false for boolean) is assigned.
public void primitiveIntInjection() {
class Injection {
private int value;
Injection(int injectable) {
this.value = injectable;
}
}
assert I.make(Injection.class).value == 0;
}
public void primitiveBooleanInjection() {
class Injection {
private boolean value;
Injection(boolean injectable) {
this.value = injectable;
}
}
assert I.make(Injection.class).value == false;
}
Lifestyle
- The resolution of dependencies can be delayed until the user actually needs it. Type variables must be correctly specified.
public void objectLazyInjection() {
class Injection {
private Lifestyle<Person> lazy;
Injection(Lifestyle<Person> injectable) {
this.lazy = injectable;
}
}
assert I.make(Injection.class).lazy.get() != null;
}
Class
- The currently processing model type. This feature is mainly available when implementing the special genericLifestyle
.
public void typeInjection() {
class Injection {
private Class type;
Injection(Class injectable) {
this.type = injectable;
}
}
assert I.make(Injection.class).type == null;
}
Priority
If only one constructor is defined for the class being injected, it is used. If more than one constructor is defined, it must detect which constructor is to be used.
The constructor with the Managed
annotation has the highest priority.
public void priorityManaged() {
class Injection {
private int value;
Injection() {
value = 10;
}
// This constructore is used.
@Managed
Injection(int injectable) {
this.value = injectable;
}
}
assert I.make(Injection.class).value == 0;
}
Next priority is given to constructors with the Inject annotation. The Inject annotation targets all annotations with the simple name 'Inject', so annotations such as jakarta.inject.Inject used in JSR330 etc. can also be used.
public void priorityInject() {
class Injection {
private int value;
Injection() {
value = 10;
}
// This constructore is used.
@Inject
Injection(int injectable) {
this.value = injectable;
}
}
assert I.make(Injection.class).value == 0;
}
If no annotation is found, the constructor with the lowest number of arguments is used.
public void priorityMinParam() {
class Injection {
private int value;
// This constructore is used.
Injection() {
value = 10;
}
Injection(int injectable) {
this.value = injectable;
}
}
assert I.make(Injection.class).value == 10;
}
If several constructors with the smallest number of arguments are defined, the first constructor found among them is used. (which is implementation-dependent in the JDK)
public void priorityMinParams() {
class Injection {
private int value;
// This constructore is used.
Injection(boolean injectable) {
value = injectable ? 1 : 10;
}
Injection(int injectable) {
this.value = injectable;
}
}
assert I.make(Injection.class).value == 10;
}
Circular Reference
One of the problems with constructor injection is that it cannot resolve circular dependencies. To partially solve this problem, Sinobu provides a delayed dependency injection method, but it does not completely solve any situation. If a solution is not possible, an error will occur.
public static class CircularLifestyleA {
private Lifestyle<CircularLifestyleB> other;
private CircularLifestyleA(Lifestyle<CircularLifestyleB> other) {
this.other = other;
}
}
public static class CircularLifestyleB {
private Lifestyle<CircularLifestyleA> other;
private CircularLifestyleB(Lifestyle<CircularLifestyleA> other) {
this.other = other;
}
}
public void circularDependenciesWithProvider() {
CircularLifestyleA circularA = I.make(CircularLifestyleA.class);
assert circularA.other != null;
CircularLifestyleB circularB = I.make(CircularLifestyleB.class);
assert circularB.other != null;
}
Concept
In the context of Java programming, a 'property' generally refers to the characteristics or attributes of a class or object. These properties define the state of an object and encapsulate the data associated with it. In Java, properties are typically represented as private fields within a class, with corresponding getter and setter methods to access and modify their values.
Here's an example of a Java class with properties:
class Person {
private String name; // Property: name
private int age; // Property: age
// Constructor
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Getter method for name property
public String getName() {
return name;
}
// Setter method for name property
public void setName(String name) {
this.name = name;
}
// Getter method for age property
public int getAge() {
return age;
}
// Setter method for age property
public void setAge(int age) {
this.age = age;
}
}
In this example, the Person class has two properties: name and age. These properties are declared as private fields within the class to encapsulate their implementation details and prevent direct access from outside the class. Getter and setter methods (getName(), setName(), getAge(), setAge()) are provided to allow controlled access to these properties.
Using properties in Java classes promotes encapsulation, which is one of the fundamental principles of object-oriented programming (OOP). Encapsulation hides the internal state of an object and exposes only the necessary interfaces for interacting with it, improving code maintainability, reusability, and flexibility.
Models
In Sinobu, model is a set of properties. If a class you define has properties, it is already a model.
Models can be retrieved from a class using the kiss.Model#of(Class)
method, but
there should not be many situations where the end user needs to retrieve the model
directly.
Model model = Model.of(Person.class);
Property
In Sinobu, a property refers to a value accessible by name and defined by a field or method. Property name is arbitrary.
By Field
Property definition by field.
public String filedProperty = "This is property";
With the final modifier, the value cannot be changed and is therefore not treated as a property.
public final String finalField = "This is NOT property";
Access modifiers other than public are not treated as properties.
protected String nonPublicField = "This is NOT property";
If you want to treat fields with access modifiers other than public as properties,
use the Managed
annotation.
@Managed
protected String managedField = "This is property";
By Variable Field
Property definition by Variable
field.
public Variable<String> variableField = Variable.of("This is property");
Unlike normal fields, they are treated as properties even if they have final modifier.
public final Variable<String> finalField = Variable.of("This is property");
Access modifiers other than public are not treated as properties.
protected Variable<String> nonPublicField = Variable.of("This is NOT property");
If you want to treat fields with access modifiers other than public as properties,
use the Managed
annotation.
@Managed
protected Variable<String> managedField = Variable.of("This is property");
By Method
Property definition by method. The getter and setter methods must be defined according to the Java Bean naming conventions.
class GetterAndSetter {
private String property = "This is property";
public String getProperty() {
return property;
}
public void setProperty(String property) {
this.property = property;
}
}
The getter and setter methods do not need to have a public modifier.
class NonPublicGetterAndSetter {
private String property = "This is property";
String getProperty() {
return property;
}
void setProperty(String property) {
this.property = property;
}
}
Manipulation
Normally, end users do not use Model
API to manipulate or traverse properties.
The following is mainly required for framework and library production.
Models and properties can be used to get, set and monitor property values.
Get
Get the value by name.
public void getProperty() {
Person person = new Person();
person.setAge(1);
Model model = Model.of(Person.class);
Property property = model.property("age");
assert model.get(person, property).equals(1);
}
Set
Set the value by name.
public void setProperty() {
Person person = new Person();
Model model = Model.of(Person.class);
Property property = model.property("age");
// assign property value
model.set(person, property, 1);
assert 1 == person.getAge();
}
If you set a property value, it is recommended that you reassign the return value to the object. This is necessary because the Record is immutable.
public void setAtRecord() {
Point point = new Point(0, 0);
Model<Point> model = Model.of(Point.class);
point = model.set(point, model.property("x"), 10);
assert point.x == 10;
point = model.set(point, model.property("y"), 20d);
assert point.y == 20d;
}
Monitor
Monitor the Variable
value.
public void observeVariableProperty() {
class Item {
public Variable<Integer> count = Variable.of(0);
}
Item item = new Item();
Model model = Model.of(Item.class);
List<Integer> values = model.observe(item, model.property("count")).toList();
assert values.isEmpty();
item.count.set(1);
item.count.set(2);
item.count.set(3);
assert values.get(0) == 1;
assert values.get(1) == 2;
assert values.get(2) == 3;
}
Usage
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import org.junit.jupiter.api.Test;
import kiss.Extensible;
import kiss.I;
public class ExtensionTest {
/**
* This is extension point.
*/
interface Codec<T> extends Extensible {
String encode(T value);
}
/**
* This is extension with LocalDate key.
*/
class LocalDateCodec implements Codec<LocalDate> {
public String encode(LocalDate value) {
return DateTimeFormatter.ISO_DATE.format(value);
}
}
/**
* Load all extensions once at application's entry point.
*/
static {
I.load(ExtensionTest.class);
}
/**
* User code.
*/
@Test
void usage() {
// find extension by type
Codec<LocalDate> codec = I.find(Codec.class, LocalDate.class);
assert codec.encode(LocalDate.of(2022, 11, 11)).equals("2022-11-11");
}
}
Extension Point
Sinobu has a general-purpose plug-in mechanism for extending application functions.
An
extensible place is called Extension Point, and its substance is a type (interface or
class) marked with the Extensible
interface.
We give a definition of Extension Point like the following.
- It implements
Extensible
interface directly.
interface ThisIsExtensionPoint extends Extensible {
}
class ThisIsAlsoExtensionPoint implements Extensible {
// This class is both Extension Point and Extension.
}
interface ThisIsNotExtensionPoint extends ThisIsExtensionPoint {
}
In the usage example, Codec is the extension point that converts any object to a string representation.
interface Codec<T> extends Extensible {
String encode(T value);
}
Extension
We give a definition of Extension like the following.
- It implements any Extension Point or is Extension Point itself.
- It must be concrete class and has a suitable constructor for Sinobu (see also
I#make(Class)
method).
class ThisIsExtension implements Extensible {
// This class is both Extension Point and Extension.
}
class ThisIsAlsoExtension extends ThisIsExtension {
// But not Extension Point.
}
class ThisIsNotExtension extends ThisIsExtension {
public ThisIsNotExtension(NotInjectable object) {
// because of invalid constructor
}
}
In the usage example, LocalDateCodec is the extension that is special implementation
for
LocalDate
.
class LocalDateCodec implements Codec<LocalDate> {
public String encode(LocalDate value) {
return DateTimeFormatter.ISO_DATE.format(value);
}
}
Extension Key
You can provide Extension Key for each Extensions by using parameter.
interface ExtensionPointWithKey<K> extends Extensible {
}
class ExtensionWithStringKey implements ExtensionPointWithKey<String> {
// Associate this Extension with String class.
}
class ExtensionWithListKey implements ExtensionPointWithKey<List> {
// Associate this Extension with List interface.
}
The key makes easy finding an Extension you need (see also
I#find(Class, Class)
).
void findExtensionByKey() {
assert I.find(ExtensionPointWithKey.class, String.class) instanceof ExtensionWithStringKey;
}
Dynamic Loading
All extensions are not recognized automatically, you have to load them explicitly
using
I#load(Class)
.
class ExtensionUser {
static {
I.load(ExtensionUser.class);
}
// write your code
}
class ApplicationMain {
public static void main(String[] args) {
I.load(ApplicationMain.class);
// start your application
}
}
Concept
JSON (JavaScript Object Notation) is a lightweight data-interchange format. It is easy for humans to read and write and easy for machines to parse and generate. It has become a de facto standard for data exchange on the web, especially for APIs. (JSON on Wikipedia)
Sinobu offers built-in support for JSON processing with an emphasis on performance, usability, and integration with Java object models.
β Simplicity
Offering straightforward APIs for parsing JSON from various sources and serializing Java objects into JSON.
void read() {
I.json("""
{
"name": "Misa",
"age": 21
}
""");
}
π§© Model-Centric
Seamlessly mapping JSON data to and from Java objects (POJOs or Records) based on Sinobu's property model, reducing boilerplate.
public void writeRecord() {
record Person(String name, int age) {
}
Person person = new Person("Joe", 23);
String json = I.write(person);
assertSame(json, """
{
"age": 23,
"name": "Joe"
}
""");
}
π Direct Manipulation
Parsed JSON can be navigated and mutated via a DOM-like tree model using the JSON class. Access elements by key or index, modify values, or restructure nodes with intuitive syntax.
public void readValue() {
JSON json = I.json("""
{
"number" : 10,
"bool" : false
}
""");
assert json.get(int.class, "number") == 10;
assert json.get(boolean.class, "bool") == false;
}
Reading
You can read JSON from strings, files, and various inputs. All data will be expanded into memory in a tree format. It is not a streaming format, so please be careful when parsing very large JSON.
Access to the value
You can access the value by specifying the key.
public void readValue() {
JSON json = I.json("""
{
"number" : 10,
"bool" : false
}
""");
assert json.get(int.class, "number") == 10;
assert json.get(boolean.class, "bool") == false;
}
Access to the nested value
You can specify a key multiple times to access nested values.
public void readNestedValue() {
JSON json = I.json("""
{
"child1" : {
"name" : "first"
},
"child2" : {
"name" : "second"
}
}
""");
assert json.get("child1").get("name").as(String.class).equals("first");
assert json.get("child2").get("name").as(String.class).equals("second");
}
You can also find all values by the sequential keys.
public void readNestedValueBySequentialKeys() {
JSON json = I.json("""
[
{
"type" : {
"name" : "first"
}
},
{
"type" : {
"name" : "second"
}
}
]
""");
List<String> found = json.find(String.class, "*", "type", "name");
assert found.size() == 2;
assert found.get(0).equals("first");
assert found.get(1).equals("second");
}
Writing
You can write JSON from property-based model.
public void writeRecord() {
record Person(String name, int age) {
}
Person person = new Person("Joe", 23);
String json = I.write(person);
assertSame(json, """
{
"age": 23,
"name": "Joe"
}
""");
}
Concept
Sinobu provides functionality for handling HTML and XML documents.
It uses a built-in, lenient parser (often called a tag soup parser)
that can handle malformed HTML, similar to how web browsers do.
The core class for this is XML
, which offers a jQuery-like
API for traversing and manipulating the document structure.
π Tag Soup Friendly
Handles malformed HTML gracefully, tolerating missing or mismatched tags commonly found in real-world web content.
public void caseInconsistencyLowerUpper() {
XML root = parseAsHTML("<div>crazy</DIV>");
assert root.find("div").size() == 1;
}
π― CSS Selector Power
Leverages CSS selectors for efficient and flexible element selection, similar to JavaScript libraries like jQuery.
public void tag() {
XML xml = I.xml("""
<root>
<h1></h1>
<article/>
<article></article>
</root>
""");
assert xml.find("h1").size() == 1;
assert xml.find("article").size() == 2;
}
π οΈ jQuery-Like API
Provides a fluent and chainable API for easy DOM manipulation, inspired by the familiar jQuery syntax.
public void append() {
XML root = I.xml("<m><Q><P/></Q><Q><P/></Q></m>");
assert root.find("Q").append("<R/><R/>").find("R").size() == 4;
assert root.find("Q > R").size() == 4;
assert root.find("Q > R:first-child").size() == 0;
}
π XML Ready
Supports not only HTML but also XML documents, providing a unified interface for structured data processing.
Reading
You can parse HTML/XML from various sources using the I
class utility methods.
Sinobu automatically detects whether the input is a URL, file path, or raw string.
The parser is designed to be tolerant of errors and can handle common issues
found in real-world HTML, such as missing tags or incorrect nesting.
This makes it suitable for scraping or processing potentially messy markup.
I#xml(String)
I#xml(java.nio.file.Path)
I#xml(java.io.InputStream)
I#xml(java.io.Reader)
I#xml(Node)
public void htmlLiteral() {
XML root = I.xml("<html/>");
assert root.size() == 1;
assert root.name() == "html";
}
And can parse the invalid structure.
public void caseInconsistencyLowerUpper() {
XML root = parseAsHTML("<div>crazy</DIV>");
assert root.find("div").size() == 1;
}
public void slipOut() {
XML root = parseAsHTML("<a><b>crazy</a></b>");
assert root.find("a").text().equals("crazy");
assert root.find("b").text().equals("crazy");
}
Writing
You can serialize the XML structure back into a string representation or write it to an
Appendable
(like Writer
or StringBuilder
).
Sinobu offers both compact and formatted output options.
The XML#toString()
method provides a compact string representation without
extra formatting.
public void format() {
XML root = I.xml("<root><child/><child/></root>");
assert root.toString().equals(normalize("""
<root>
<child/>
<child/>
</root>
"""));
}
The XML#to(Appendable, String, String...)
method allows for pretty-printing
the XML structure with configurable indentation for improved readability.
You can specify the indentation string (e.g., a tab character \t
or spaces).
public void specialIndent() {
StringBuilder out = new StringBuilder();
I.xml("<root><child><nested/></child></root>").to(out, "*");
assert out.toString().equals(normalize("""
<root>
*<child>
**<nested/>
*</child>
</root>
"""));
}
You can also specify tag names that should be treated as inline elements (no line
breaks around them), preserving the original formatting for elements like <span>
or
<a>
.
public void inlineElement() {
StringBuilder out = new StringBuilder();
I.xml("<root><inline/></root>").to(out, "\t", "inline");
assert out.toString().equals("<root><inline/></root>");
}
By using the special prefix `` before a tag name, you can also specify tags that
should always be treated as non-empty elements (always have a closing tag like
<script></script>
, even if empty).
public void nonEmptyElement() {
StringBuilder out = new StringBuilder();
I.xml("<root/>").to(out, "\t", "&root");
assert out.toString().equals("<root></root>");
}
By passing null
as the indent character, formatting (indentation and line breaks)
is disabled, similar to toString()
but writing to an Appendable
.
public void withoutFormat() {
StringBuilder out = new StringBuilder();
I.xml("<root><child/></root>").to(out, null);
assert out.toString().equals("<root><child/></root>");
}
CSS Selector
Sinobu leverages CSS selectors for querying elements within the document structure using
the XML#find(String)
method. This provides a powerful and familiar way to select
nodes, similar to JavaScript's document.querySelectorAll
.
This library supports many standard CSS3 selectors and includes some useful extensions.
public void tag() {
XML xml = I.xml("""
<root>
<h1></h1>
<article/>
<article></article>
</root>
""");
assert xml.find("h1").size() == 1;
assert xml.find("article").size() == 2;
}
public void attribute() {
XML xml = I.xml("""
<m>
<e A='one' B='one'/>
<e A='two' B='two'/>
</m>
""");
assert xml.find("[A]").size() == 2;
assert xml.find("[A=one]").size() == 1;
}
public void clazz() {
XML xml = I.xml("""
<root>
<p class="on"/>
<p class="on large"/>
<p class=""/>
<p no-class-attr="on"/>
</root>
""");
assert xml.find(".on").size() == 2;
assert xml.find(".large").size() == 1;
assert xml.find(".on.large").size() == 1;
}
public void mix() {
XML xml = I.xml("""
<m>
<ok/>
<ok>
<ok id="not"/>
<not>
<ok/>
</not>
</ok>
</m>
""");
assert xml.find("ok").size() == 4;
assert xml.find("not ok").size() == 1;
assert xml.find("ok > ok").size() == 1;
}
Combinators
Combinators define the relationship between selectors.
Combinator | Description | Notes |
---|---|---|
(Space) |
Descendant | Default combinator between selectors |
> |
Child | Direct children |
+ |
Adjacent Sibling | Immediately following sibling |
~ |
General Sibling | All following siblings |
< |
Adjacent Previous Sibling | Sinobu Extension |
, |
Selector List | Groups multiple selectors |
| |
Namespace Separator | Used with type and attribute selectors |
Basic Selectors
Basic selectors target elements based on their type, class, or ID.
Selector Type | Example | Description |
---|---|---|
Type | div |
By element name |
Class | .warning |
By class attribute |
ID | #main |
By id attribute |
Universal | * |
All elements |
Attribute Selectors
Attribute selectors target elements based on the presence or value of their attributes.
Selector Syntax | Description |
---|---|
[attr] |
Elements with an attr attribute |
[attr=value] |
Elements where attr equals value |
[attr~=value] |
Elements where attr contains the word value |
[attr*=value] |
Elements where attr contains substring value |
[attr^=value] |
Elements where attr starts with value |
[attr$=value] |
Elements where attr ends with value |
[ns:attr] |
Elements with attribute attr in namespace ns |
[ns:attr=value] |
Elements with namespaced attribute and value |
Pseudo Class Selectors
Pseudo-classes select elements based on their state, position, or characteristics not reflected by simple selectors. The table includes standard pseudo-classes and kiss library specific extensions (marked as Sinobu Extension).
Pseudo-Class | Description | Notes |
---|---|---|
:first-child |
First element among siblings | |
:last-child |
Last element among siblings | |
:only-child |
Element that is the only child | |
:first-of-type |
First element of its type among siblings | |
:last-of-type |
Last element of its type among siblings | |
:only-of-type |
Element that is the only one of its type | |
:nth-child(n) |
n-th element among siblings | keyword |
:nth-last-child(n) |
n-th element among siblings, from last | keyword |
:nth-of-type(n) |
n-th element of its type among siblings | keyword |
:nth-last-of-type(n) |
n-th element of type among siblings from last | keyword |
:empty |
Elements with no children (incl. text) | |
:not(selector) |
Elements not matching the inner selector |
|
:has(selector) |
Elements having a descendant matching selector |
|
:root |
Document's root element | |
:contains(text) |
Elements containing text directly |
Sinobu Extension |
:parent |
Parent element | Sinobu Extension |
Note: User interface state pseudo-classes (like :hover
, :focus
, :checked
) are
generally not supported as they relate to browser interactions rather than static
document structure analysis.
Manipulation
The XML
object provides a fluent API, similar to jQuery, for modifying the
document structure.
Important: These manipulation methods modify the underlying DOM structure directly;
the XML
object itself is mutable in this regard. Explore the nested classes for
specific manipulation categories.
Adding Content
Methods for inserting new content (elements, text, or other XML structures) relative to the selected elements in the document.
Method Link | Description |
---|---|
XML#append(Object) |
Insert content at the end of each element. |
XML#prepend(Object) |
Insert content at the beginning of each element. |
XML#before(Object) |
Insert content before each element. |
XML#after(Object) |
Insert content after each element. |
XML#child(String) |
Create and append a new child element. |
XML#child(String, Consumer) |
Create, append, and configure a new child. |
public void append() {
XML root = I.xml("<m><Q><P/></Q><Q><P/></Q></m>");
assert root.find("Q").append("<R/><R/>").find("R").size() == 4;
assert root.find("Q > R").size() == 4;
assert root.find("Q > R:first-child").size() == 0;
}
public void prepend() {
String xml = "<m><Q><P/></Q><Q><P/></Q></m>";
XML e = I.xml(xml);
assert e.find("Q").prepend("<R/><R/>").find("R").size() == 4;
assert e.find("Q > R").size() == 4;
assert e.find("Q > R:first-child").size() == 2;
}
Removing Content
Methods for removing content or elements from the document.
Method Link | Description |
---|---|
XML#empty() |
Remove all child nodes from elements. |
XML#remove() |
Remove the selected elements from the DOM. |
public void empty() {
XML root = I.xml("<Q><P/><P/></Q>");
root.empty();
assert root.find("P").size() == 0;
}
public void remove() {
XML root = I.xml("<Q><S/><T/><S/></Q>");
assert root.find("*").remove().size() == 3;
assert root.find("S").size() == 0;
assert root.find("T").size() == 0;
}
Wrapping
Methods for wrapping selected elements with new HTML structures.
Method Link | Description |
---|---|
XML#wrap(Object) |
Wrap each selected element individually. |
XML#wrapAll(Object) |
Wrap all elements together with one structure. |
public void wrap() {
String xml = "<m><Q/><Q/></m>";
XML e = I.xml(xml);
e.find("Q").wrap("<P/>");
assert e.find("P > Q").size() == 2;
assert e.find("P").size() == 2;
assert e.find("Q").size() == 2;
}
public void wrapAll() {
String xml = "<m><Q/><Q/></m>";
XML e = I.xml(xml);
e.find("Q").wrapAll("<P/>");
assert e.find("P > Q").size() == 2;
assert e.find("P").size() == 1;
assert e.find("Q").size() == 2;
}
Text Content
Methods for getting or setting the text content of elements. Text content represents the combined text of an element and its descendants.
Method Link | Description |
---|---|
XML#text() |
Get the combined text content of elements. |
XML#text(String) |
Set text content, replacing existing. |
public void textGet() {
String xml = "<Q>ss<P>a<i>a</i>a</P><P> b </P><P>c c</P>ss</Q>";
assert I.xml(xml).find("P").text().equals("aaa b c c");
}
public void textSet() {
String xml = "<Q><P>aaa</P></Q>";
XML e = I.xml(xml);
e.find("P").text("set");
assert e.find("P:contains(set)").size() == 1;
}
Attributes
Methods for managing element attributes (e.g., href
, src
, id
). Since the
class
attribute is frequently manipulated, dedicated helper methods are provided
for convenience.
Method Link | Description |
---|---|
XML#attr(String) |
Get attribute value for the first element. |
XML#attr(String, Object) |
Set attribute value; null removes attribute. |
XML#addClass(String...) |
Add one or more classes. |
XML#removeClass(String...) |
Remove one or more classes. |
XML#toggleClass(String) |
Add or remove a class based on presence. |
XML#hasClass(String) |
Check if any element has the class. |
public void attrGet() {
String xml = "<Q name='value' key='map'/>";
assert I.xml(xml).attr("name").equals("value");
assert I.xml(xml).attr("key").equals("map");
}
public void attrSet() {
String xml = "<Q name='value' key='map'/>";
XML e = I.xml(xml);
e.attr("name", "set");
assert e.attr("name").equals("set");
assert e.attr("key").equals("map");
assert e.attr("name", null).find("Q[name]").size() == 0;
}
public void addClass() {
assert I.xml("<a/>").addClass("add").attr("class").equals("add");
assert I.xml("<a class='base'/>").addClass("add").attr("class").equals("base add");
assert I.xml("<a class='base'/>").addClass("base").attr("class").equals("base");
assert I.xml("<a class='base'/>").addClass("add", "base", "ad").attr("class").equals("base add ad");
}
Cloning
Method for duplicating elements, creating a deep copy.
Method Link | Description |
---|---|
XML#clone() |
Create a deep copy of selected elements. |
Traversing
Navigate the DOM tree relative to the currently selected elements.
Most traversal methods return a new XML
object containing the resulting elements,
allowing for method chaining without modifying the original selection (unless intended).
Explore the nested classes for specific traversal categories.
Filtering
Methods for filtering the current set of selected elements or finding new ones within the current context.
Method Link | Description |
---|---|
XML#first() |
Reduce the set to the first element. |
XML#last() |
Reduce the set to the last element. |
XML#find(String) |
Find descendants matching the selector. |
public void first() {
XML xml = I.xml("""
<root>
<child1 class='a'/>
<child2 class='a'/>
<child3 class='a'/>
</root>
""");
XML found = xml.find(".a");
assert found.size() == 3;
XML first = found.first();
assert first.size() == 1;
assert first.name().equals("child1");
}
public void tag() {
XML xml = I.xml("""
<root>
<h1></h1>
<article/>
<article></article>
</root>
""");
assert xml.find("h1").size() == 1;
assert xml.find("article").size() == 2;
}
Iteration
The XML
object implements Iterable
, allowing easy iteration
over each selected DOM element individually using a standard Java for-each loop.
This is useful for processing each element in a selection.
void iterate() {
XML elements = I.xml("<div><p>1</p><p>2</p></div>").find("p");
for (XML p : elements) {
System.out.println(p.text());
}
}
Usage
import org.junit.jupiter.api.Test;
import kiss.I;
class MustacheTest {
String template = "She is {name}, {age} years old.";
record Person(String name, int age) {
}
@Test
void use() {
Person data = new Person("Takina Inoue", 16);
assert I.express(template, data).equals("She is Takina Inoue, 16 years old.");
}
}
Mustache
Mustache can be used for HTML, config files, source code - anything. It works by expanding tags in a template using values provided in a hash or object. We call it "logic-less" because there are no if statements, else clauses, or for loops. Instead there are only tags. Some tags are replaced with a value, some nothing, and others a series of values.
Java19 does not yet have a language built-in template syntax. Therefore, Sinobu provides a mechanism to parse Mustache syntax instead.
Syntax
To use Mustache, you must first create a Mustache template, which is written using a markup language such as HTML or XML. In template, you use special symbols called Mustache delimiters to specify where the data should be inserted. Mustache delimiters are written in the following format:
{placeholder}
As you can see, Mustache delimiter is a string of characters enclosed in single brace, such as "{placeholder}". This string specifies the location where the data should be inserted. For example, consider the following Mustache template:
String template = "She is {name}, {age} years old.";
When using this template, you need to provide data for placeholders. For instance, you might have the following JavaBean object as data:
record Person(String name, int age)
Passing the template string and context data to method
I#express(String, Object...)
, we get a string in which the various
placeholders are replaced by its data.
Person data = new Person("Takina Inoue", 16);
assert I.express(template, data).equals("She is Takina Inoue, 16 years old.");
Next, within the program that uses the Mustache template, the Mustache engine is initialized. At this point, the Mustache engine is passed the template and the data. The data is written using data structures such as JavaScript objects.
Finally, the Mustache engine is used to render the Mustache template. At this time, the Mustache engine replaces the Mustache specifier in the template and populates the data to produce a finished HTML, XML, or other markup language document.
The specific usage varies depending on the programming language and framework, but the above steps are a rough outline of the basic procedure for using Mustache.
Usage at Sinobu
In SInobu, Mustache can be used by calling the
I#express(String, Object...)
method. This method parses the given string, reads the necessary variables from
the
context, substitutes them, and returns the resulting string.
Concept
Sinobu provides a concise and powerful API for making HTTP(S) requests and handling
WebSocket connections, built on top of Java's standard HttpClient
.
It simplifies common tasks like handling responses, content negotiation,
and asynchronous processing using Signal
.
π‘ Fluent API
Simple static methods in I
enable common HTTP GET requests and WebSocket
connections without boilerplate.
π§ Standard Integration
Built on java.net.http.HttpRequest.Builder
, allowing fine-grained control over
headers, methods, and request bodies.
π Automatic Content Handling
Response bodies are automatically converted to suitable types (String, JSON, XML, beans, etc.), with gzip/deflate decompression handled transparently.
βοΈ Reactive Streams
Both HTTP and WebSocket messages are streamed asynchronously via Signal
,
promoting non-blocking and reactive design.
π WebSocket Support
Provides a simple API for establishing WebSocket connections and handling incoming/outgoing messages with ease.
Request and Response
Making HTTP requests and processing responses is streamlined using I#http
methods.
You can make simple GET requests with just a URL or use Java's
java.net.http.HttpRequest.Builder
for full control over the request details (method, headers, body, etc.).
Responses are delivered asynchronously as a Signal
.
// Simple GET request, response as String
I.http("https://example.com/data", String.class).to(html -> {
System.out.println("Fetched HTML: " + html.substring(0, 100) + "...");
});
// POST request with custom headers, response mapped to a User object
HttpRequest.Builder request = HttpRequest.newBuilder(URI.create("https://api.example.com/users"))
.POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"John\"}"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer your_token");
I.http(request, User.class).to(user -> {
System.out.println("Created user: " + user.getName());
});
// Synchronous execution (blocks until response or error)
try {
String result = I.http("https://example.com", String.class).waitForTerminate().to().exact();
System.out.println("Synchronous result: " + result);
} catch (Exception e) {
System.err.println("Request failed: " + e);
}
Errors during the request (network issues, HTTP status codes >= 400) are propagated
through the I#signalError(Throwable)
channel.
Supported Type
The I#http
methods automatically convert the response body to the specified Java
type.
This simplifies handling different content types.
Supported types include:
String
: The response body is read as a UTF-8 string.InputStream
: Provides direct access to the (potentially decompressed) response body stream. You are responsible for closing this stream.HttpResponse
: Provides the full `HttpResponse` object, giving access to status code, headers, and the body stream. XML
: Parses the response body as XML/HTML into anXML
object.- JSON: Parses the response body as JSON into a JSON object.
- Any JSON-mappable Bean/Record: Parses the JSON response body and maps it to an instance of the specified class using Sinobu's object mapping capabilities.
Automatic Decompression:
Sinobu automatically inspects the Content-Encoding
response header. If the content is
compressed using gzip
or deflate
, it will be decompressed transparently before
being passed to the type converter or returned as an InputStream
.
WebSocket Support
Sinobu provides a simple way to establish WebSocket connections using
I#http(String, Consumer, HttpClient...)
.
Communication is handled reactively using Signal
for incoming messages and a
WebSocket
object for sending messages.
Disposable connection = I.http("wss://echo.websocket.org", ws -> {
// Connection opened callback - send a message
System.out.println("WebSocket Opened!");
ws.sendText("Hello WebSocket!", true);
// You can send more messages later using the 'ws' object
// ws.sendText("Another message", true);
// Request more messages from the server (default is 1)
// ws.request(5); // Request up to 5 more messages
}).to(message -> { // onNext - received message
System.out.println("Received: " + message);
// ws.sendText("Got it: " + message, true); // Example: Echo back
}, error -> { // onError - connection error
System.err.println("WebSocket Error: " + error);
}, () -> { // onComplete - connection closed
System.out.println("WebSocket Closed");
});
// To close the connection later:
// connection.dispose();
The Consumer<WebSocket> open
lambda is executed once the connection is successfully
established.
The WebSocket
instance provided allows you to send messages (sendText
,
sendBinary
, sendPing
, etc.)
and manage the connection state (request
, sendClose
).
Incoming messages are received through the Signal
returned by I.http
.
Automatic Decompression:
Similar to HTTP responses, Sinobu automatically handles WebSocket messages compressed
with the
standard permessage-deflate
extension (commonly used for gzip/deflate over WebSockets),
ensuring you receive decompressed text messages in the Signal
.
Custom HttpClient
While I#http
methods use a default, shared HttpClient
instance
internally,
you can provide your own configured HttpClient
instance(s) as optional trailing
arguments
to any of the I.http
methods. This allows customization of timeouts, proxies, SSL
contexts,
authenticators, cookie handlers, etc., using the standard Java HttpClient.Builder
API.
HttpClient customClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.followRedirects(HttpClient.Redirect.NORMAL)
// .proxy(...)
// .sslContext(...)
// .authenticator(...)
// .cookieHandler(...)
.build();
// Use the custom client for the request
I.http("https://example.com", String.class, customClient).to(response -> { ... });
// Use it for WebSocket too
I.http("wss://example.com/ws", ws -> { ... }, customClient).to(message -> { ... });
If multiple clients are passed, the first non-null one is used. If none are provided or
all are null,
the default client (I.client
) is used.
Concept
The concept of ReactiveX is very well summarized on the official website, so it is better to read there.
The Signal
class that implements the Reactive Pattern. This class provides
methods for subscribing to the Signal
as well as delegate methods to the various
observers.
In Reactive Pattern an observer subscribes to a Signal
. Then that observer reacts
to whatever item or sequence of items the Signal
emits. This pattern facilitates
concurrent operations because it does not need to block while waiting for the
Signal
to emit objects, but instead it creates a sentry in the form of an observer that stands
ready to react appropriately at whatever future time the Signal
does so.
The subscribe method is how you connect an Observer
to a Signal
. Your
Observer
implements some subset of the following methods:
Observer#accept(Object)
- ASignal
calls this method whenever theSignal
emits an item. This method takes as a parameter the item emitted by theSignal
.Observer#error(Throwable)
- ASignal
calls this method to indicate that it has failed to generate the expected data or has encountered some other error. It will not make further calls toObserver#error(Throwable)
orObserver#complete()
. TheObserver#error(Throwable)
method takes as its parameter an indication of what caused the error.Observer#complete()
- ASignal
calls this method after it has calledObserver#accept(Object)
for the final time, if it has not encountered any errors.
By the terms of the Signal
contract, it may call Observer#accept(Object)
zero or more times, and then may follow those calls with a call to either
Observer#complete()
or Observer#error(Throwable)
but not both, which will
be its last call. By convention, in this document, calls to
Observer#accept(Object)
are usually called βemissionsβ of items,
whereas calls to Observer#complete()
or Observer#error(Throwable)
are
called βnotifications.β
Concept
Scheduling tasks to run at specific times or intervals is a common requirement. Sinobu provides a simple yet powerful mechanism for this, based on the well-known Cron expression format. It integrates seamlessly with Sinobu's reactive streams.
β¨ Simple API
Schedule tasks with a single static method call: I#schedule(String)
.
No complex setup or scheduler instances needed.
void scheduling() {
I.schedule(() -> {
// execute your job immediately on job thread
});
I.schedule("0 0 * * *").to(() -> {
// execute your job regularly on job thread
});
}
ποΈ Cron Expression Syntax
Define complex schedules using the standard Cron format, specifying minutes,
hours, days, months, and weekdays. Sinobu supports standard fields and special
characters like *
, /
, -
, ,
, L
, W
, #
, ?
, and the randomizer R
.
π Reactive Integration
Scheduled events are delivered as a Signal
, allowing you to use
reactive operators for flow control (e.g., take),
transformation, and lifecycle management (Disposable
).
Usage
You can use I#schedule(String)
to schedule jobs by cron expression.
void scheduling() {
I.schedule("0 0 * * *").to(() -> {
// execute your job
});
}
To stop continuous task scheduling, execute the return value Disposable
.
void stopScheduling() {
Disposable disposer = I.schedule("0 0 * * *").to(() -> {
// execute your job
});
// stop scheduling
disposer.dispose();
}
Since the return value is Signal
, it is easy to stop after five executions.
void stopSchedulingAfter5Executions() {
I.schedule("0 0 * * *").take(5).to(() -> {
// execute your job
});
}
Format
A Cron expression consists of five or six fields, separated by spaces. From left to right, they represent seconds, minutes, hours, days, months, and days of the week, with seconds being optional. The possible values for each field are as follows, and some special function characters can also be used.
If you want to specify 9:00 every morning, it would look like this
0 0 9 * * *
Seconds field is optional and can be omitted.
0 9 * * *
Field | Required | Acceptable Values | Special Characters |
---|---|---|---|
Seconds | No | 0 ~ 59 | , - * / R |
Minutes | Yes | 0 ~ 59 | , - * / R |
Hours | Yes | 0 ~ 23 | , - * / R |
Days | Yes | 1 ~ 31 | , - * / ? L W R |
Months | Yes | 1 ~ 12 or JAN ~ DEC | , - * / R |
Weekdays | Yes | 0 ~ 7 or SUN ~ SAT
0 and 7 represent Sunday |
, - * / ? L # R |
Second
The seconds field can be a number from 0 to 59. It is optional and does not have to be specified.
Expression | Description | Example | Execution Timing |
---|---|---|---|
* |
Every minute | * * * * * |
every minute |
*/5 |
Every 5 minutes | */5 * * * * |
every 5 minutes |
5 |
Specific minute | 5 * * * * |
at the 5th minute |
1-30 |
Range | 1-30 * * * * |
from minute 1 to 30 |
0,30 |
Multiple values | 0,30 * * * * |
at minutes 0 and 30 |
Hour
The time field can be a number from 0 to 23.
Expression | Description | Example | Execution Timing |
---|---|---|---|
* |
Every hour | 0 * * * * |
at the 0th minute of every hour |
*/3 |
Every 3 hours | 0 */3 * * * |
every 3 hours at the 0th minute |
0 |
Midnight | 0 0 * * * |
at 12:00 AM every day |
9-17 |
Business hours | 0 9-17 * * * |
at the 0th minute between 9 AM and 5 PM |
8,12,18 |
Multiple times | 0 8,12,18 * * * |
at 8 AM, 12 PM, and 6 PM |
Day
The date field can be a number between 1 and 31.
Expression | Description | Example | Execution Timing |
---|---|---|---|
* |
Every day | 0 0 * * * |
at 12:00 AM every day |
1 |
First day of month | 0 0 1 * * |
at 12:00 AM on the 1st of every month |
L |
Last day of month | 0 0 L * * |
at 12:00 AM on the last day of every month |
*/2 |
Every other day | 0 0 */2 * * |
at 12:00 AM every other day |
1,15 |
Multiple days | 0 0 1,15 * * |
at 12:00 AM on the 1st and 15th of every month |
15W |
Nearest weekday | 0 0 15W * * |
at 12:00 AM on the nearest weekday to the 15th |
Month
The month field can be a number from 1 to 12.
It can also be specified in English abbreviations such as JUN
, FEB
, MAR
, etc.
Expression | Description | Example | Execution Timing |
---|---|---|---|
* |
Every month | 0 0 1 * * |
at 12:00 AM on the 1st of every month |
1 or JAN |
January | 0 0 1 1 * |
at 12:00 AM on January 1st |
*/3 |
Quarterly | 0 0 1 */3 * |
at 12:00 AM on the 1st every 3 months |
3-5 |
Specified period | 0 0 1 3-5 * |
at 12:00 AM on the 1st from March to May |
1,6,12 |
Multiple months | 0 0 1 1,6,12 * |
at 12:00 AM on January 1st, June 1st, and December 1st |
Day of Week
The day of the week field can be a number from 0 to 7, where 0 is Sunday, 1 is Monday, 2 is Tuesday, and so on, returning to Sunday at 7.
You can also specify the English abbreviation of SUN
, MON
, TUE
, etc.
Expression | Description | Example | Execution Timing |
---|---|---|---|
* |
Every day | 0 0 * * * |
every day at 00:00 |
1-5 |
Weekdays only | 0 0 * * 1-5 |
at 00:00 on weekdays |
0,6 |
Weekends only | 0 0 * * 0,6 |
at 00:00 on Saturday and Sunday |
1#1 |
Nth weekday | 0 0 * * 1#1 |
at 00:00 on the first Monday of each month |
5L |
Last weekday | 0 0 * * 5L |
at 00:00 on the last Friday of each month |
Special Characters
In addition to numbers, each field can contain characters with special meanings and functions.
Character | Description | Example | Execution Timing |
---|---|---|---|
* |
All values | * * * * * |
every minute |
, |
List of values | 1,15,30 * * * * |
at 1 minute, 15 minutes, and 30 minutes past every hour |
- |
Range | 9-17 * * * * |
every minute from 9 AM to 5 PM |
/ |
Interval | */15 * * * * |
every 15 minutes |
L |
Last | 0 0 L * * |
at 00:00 on the last day of each month |
W |
Nearest weekday | 0 0 15W * * |
at 00:00 on the nearest weekday to the 15th |
# |
Nth occurrence | 0 0 * * 1#1 |
at 00:00 on the first Monday of each month |
R |
Random | R 10 * * * |
once at a random minute in 10:00 a.m |
? |
No date specification | 0 0 ? * MON |
at 00:00 every Monday |
Complex time specifications can be made by combining several special characters as follows
Example | Execution Timing |
---|---|
30 17 * * 5L |
at 17:30 on the last Friday of each month |
*/30 9-17 * * 1-5 |
every 30 minutes from 9 AM to 5 PM on weekdays |
0 0 1,15,L * * |
at 00:00 on the 1st, 15th, and last day of each month |
0 12 * * 1#2,1#4 |
at 12:00 on the 2nd and 4th Monday of each month |
0 9 15W * * |
at 09:00 on the nearest weekday to the 15th |
0 22 * 1-3 0L |
at 22:00 on the last Sunday of each month from January to March |
15 9-17/2 * * 1,3,5 |
every 2 hours from 09:15 to 17:15 on Mondays, Wednesdays, and Fridays |
0 0-5/2 * * * |
every 2 hours between 00:00 and 05:00 every day |
0 18-22 * * 1-5 |
at every hour from 18:00 to 22:00 on weekdays |
0 0 1 * 2 |
at 00:00 on the first day of every month, but only if it is a Tuesday |
0 0 15 * 1#3 |
at 00:00 on the third Monday of every month when it falls on the 15th |
0 12 1 1 * |
at 12:00 on January 1st every year |
*/10 * * * 6#3 |
every 10 minutes on the third Saturday of each month |
0 8-18/3 * * 0 |
every 3 hours from 08:00 to 18:00 on Sundays |
0 0-23/6 * * * |
every 6 hours, at 00:00, 06:00, 12:00, and 18:00 every day |
0 15 1 1,7 * |
at 15:00 on January 1st and July 1st every year |
0 20 * * 1#2 |
at 20:00 on the second Monday of each month |
0-30R 10,22 * * * |
once each at random times between 0 and 30 minutes at 10:00 or 22:00 |
0 0 1-5,10-15 * * |
at 00:00 on the 1st to 5th and the 10th to 15th day of each month |
0 1-5/2,10-15 * * * |
at every 2 hours from 01:00 to 05:00 and every hour from 10:00 to 15:00 every day |
Concept
Sinobu provides a minimalist, high-performance logging library designed with an emphasis on garbage-less operation and zero-boilerplate code. It is especially well-suited for high-throughput applications where every microsecond and memory allocation matters.
π No Boilerplate
Logging is as simple as calling a static method like I#info(Object)
. Therefore,
thereβs no need to create logger instances per class. This approach dramatically reduces
code clutter and eliminates repetitive logger setup.
void log() {
I.info("Hello Logging");
}
β»οΈ Garbage Less
Many logging libraries create temporary objects (log events, strings, byte arrays, etc.) each time they output logs, causing GC latency; Sinobu tackles this problem head on and does not create new objects unless you explicitly request stack trace.
β‘ High Performance
Sinobu is engineered for speed. Its optimized encoding, buffer reuse, and minimal synchronization mean it can outperform many mainstream logging libraries. Even under heavy logging loads, it maintains consistent performance with minimal CPU and memory impact. Check benchmark.
Usage
Logging is performed via static methods such as I#info(Object)
or
I#info(String, Object)
. These methods support various input types:
Object
- UseObject#toString()
representation as message.Supplier
- Delaying the construction of message avoids extra processing when the log level prevents writing.Throwable
- A message and a stack trace back to the cause are written.
void basic() {
I.trace("Write your message");
}
void lazyEvaluation() {
I.debug(() -> "Delaying the construction of message.");
}
void throwable() {
try {
mayBeThrowError();
} catch (Exception e) {
I.error("Write message and stack trace");
I.error(e);
}
}
Most methods come in two forms:
- Without category - logs to the default system category.
- With category - the first argument specifies the log category, allowing fine-grained control.
void categorized() {
I.debug("Write message in system category");
I.warn("database", "Write message in database category");
}
Configuration
Logging behavior can be configured via environment variables (I#env(String)
Configuration keys follow a specific pattern to allow both fine-grained and global
control.
Resolution Order
When any environment variable is required, configuration values are resolved in the following order:
- Look for
category.property
(e.g.system.file
) - If not found, look for
*.property
(e.g.*.file
) - If still not found, use the built-in default (e.g.
Level#WARNING
)
Log Appender
Log messages can be routed to different output destinations. While logs may go to the
console by default (depending on the underlying System#out
implementation),
Sinobu allows you to explicitly configure destinations. You can even configure
multiple destinations for the same logger category.
Property Key | Value Type | Description |
---|---|---|
category.console |
Level |
Enables console logging for the specified category. |
category.file |
Level |
Enables file logging for the specified category. |
category.extra |
Level |
Enables extra logging for the specified category.
You must define I#Logger as extra logger. |
File Logging Options
Options specific to file logging when category.file
is enabled.
Log files are typically named category<Date>.log
(e.g. database2024-10-27.log
).
Property Key | Value Type | Description |
---|---|---|
category.dir |
String |
Specifies the directory where log files for this category should be created. Default is .log |
category.append |
boolean |
If true , appends to existing log files for this category. If false , overwrites. Default is true . |
category.rotate |
int |
Number of past daily log files to keep for this category. Older files are deleted. 0 disables rotation. Default is 90. |
Formatting Options
Options related to the formatting of log messages for a specific category.
Property Key | Value Type | Description |
---|---|---|
category.caller |
Level |
Includes caller info for messages in this category at or above the given level. Can impact performance. |
Usage
Sinobu provides a simple mechanism for persisting the state of objects
using the Storable
interface. This allows objects to save their properties
to JSON file and restore them later, making it easy to maintain application
state across restarts.
𦀠Interface Based
Implement the Storable
interface on your class to enable persistence.
class Data implements Storable<Data> {
public int property;
}
βοΈ Saving Data
The Storable#store()
method saves the current state of the object's properties
to file. (check property)
void save() {
Data data = new Data();
data.property = 10;
data.store(); // save data to file
}
βοΈ Restoring Data
The Storable#restore()
method restores the object's properties from file (check
property). If the file doesn't exist or
an error occurs during reading/parsing, the operation typically fails silently, leaving
the object in its current state (often default values).
void restore() {
Data other = new Data();
other.restore(); // load data from file
assert other.property == 10;
}
Transient
Normally all properties are eligible for preservation, but there are several ways to make explicit which properties you do not want to preserve.
If the property is defined by field, add the transient modifier to the field declaration.
class TransientField implements Storable<TransientField> {
public transient int unstorableProperty;
}
If the property is defined by method, add java.beans.Transient
annotation. (You
only need to add it to either the setter or the getter)
class TransientMethod implements Storable<TransientMethod> {
private int value;
@java.beans.Transient
public int getUnstorableProperty() {
return value;
}
public void setUnstorableProperty(int value) {
this.value = value;
}
}
Automatic Saving
Instead of manually calling Storable#store()
every time a change occurs,
you can be configured to save their state automatically when their properties change.
This is achieved using the Storable#auto()
method.
Since monitoring the values of arbitrary properties would be prohibitively expensive,
value detection is only possible for properties defined by Variable
.
Calling Storable#auto()
instance monitors its (and nested) properties.
When a change is detected, it schedules a save operation. By default, this operation is
debounced (typically waiting 1 second after the last change) to avoid excessive writes
during rapid changes.
void autoSave() {
class Data implements Storable<Data> {
public final Variable<String> name = Variable.empty();
}
Data data = new Data();
data.auto(); // enable auto-save
data.name.set("Misa"); // save after 1 sec
}
Calling Disposable#dispose()
on the returned object will stop the automatic
saving process for that instance.
void stopAutoSave() {
class Data implements Storable<Data> {
public final Variable<String> name = Variable.empty();
}
Data data = new Data();
Disposable stopper = data.auto();
stopper.dispose(); // stop auto-save
}
Storage Location
By default, persistence file is stored in a directory named .preferences
within
the application's working directory. The filename is derived from the fully qualified
class name of the storable object, ending with .json
.
(e.g., .preferences/com.example.MyAppSettings.json
).
This location can be customized in two main ways:
Location Method
You can override the Storable#locate()
method within your implementing class
to return a custom Path
for the persistence file.
class Custom implements Storable<Custom> {
public Path locate() {
return Path.of("setting.txt");
}
}
Environment Variable
You can set a global preference directory by defining the environment variable
PreferenceDirectory
using I#env(String, Object)
. If this variable is set,
the default implementation will use this directory instead of .preferences
.
void define() {
I.env("PreferenceDirectory", "/user/home/setting");
}
Overview
This section presents benchmark results comparing the performance of Sinobu with other popular libraries for various common tasks. The goal is to provide objective data on Sinobu's efficiency in terms of execution speed, memory allocation, and garbage collection impact.
π¬ Methodology
Benchmarks were conducted using a custom benchmarking framework inspired by principles similar to JMH (Java Microbenchmark Harness). This includes dedicated warm-up phases to allow for JIT compilation and stabilization, and techniques like blackholes to prevent dead code elimination and ensure accurate measurement of the intended operations. Each major benchmark suite (e.g., JSON, Logging) is typically run in a separate JVM process to ensure isolation and prevent interference between tests. Benchmarks were run under specific hardware and software configurations. The results shown in the graphs typically represent throughput (operations per second), execution time, or memory allocation. Interpretation depends on the specific metric shown in each graph (e.g. higher throughput is better, lower time/allocation is better).
π Comparisons and Metrics
Comparisons are often made against well-known libraries relevant to each domain (e.g., Jackson/Gson for JSON, Logback/Log4j2 for Logging). The latest stable versions of competitor libraries available at the time of measurement were typically used.
Operations specific to each domain (e.g., JSON parsing, logging throughput, template rendering) are performed to measure key performance indicators such as:
- Execution speed (throughput or time per operation)
- Garbage collection load (allocation rate)
- Memory consumption (footprint, retained size - though less frequently shown in graphs)
Lower values for time and allocation generally indicate better performance, while higher values for throughput are better.
β οΈ Disclaimer
Benchmark results can vary depending on the execution environment (JVM version, OS, hardware). These results should be considered indicative rather than absolute measures of performance in all scenarios.
Logging
Compares the performance of Sinobu's logging framework against other logging libraries. Focuses on throughput (operations per second) and garbage generation under different scenarios, highlighting Sinobu's garbage-less design advantage.
JSON
Compares Sinobu's JSON processing capabilities (parsing, traversing, mapping) against other well-known Java JSON libraries like FastJSON, Jackson, and Gson. Results highlight performance across various operations and document sizes.
Parse Small
Measures the time and resources required to parse small JSON documents.
Parse Large
Measures the performance of parsing larger JSON documents, testing scalability.
Parse Huge
Measures the performance of parsing very large (huge) JSON documents, stressing memory and CPU usage.
Traversing
Evaluates the efficiency of navigating and accessing data within a parsed JSON structure (DOM-like access).
Mapping
Benchmarks the process of mapping JSON data directly to Java objects (POJOs/Records).
HTML
Compares the performance of Sinobu's HTML/XML parser (including tag soup handling) against other Java parsers. Focuses on parsing speed and memory usage for typical web documents.