Sinobu

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:

  1. Which class to instantiate actually.
  2. How to instantiate it.
  3. 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 generic Lifestyle.
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.

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.

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;
}

Tree Navigation

Methods for navigating the DOM tree relative to the current elements, including moving vertically (up to parents, down to children) and horizontally (sideways to siblings).

Method Link Description
XML#parent() Get the direct parent of each element in the current set. Duplicates are removed.
XML#children() Get the direct children of each element in the set.
XML#firstChild() Get the first direct child of each element in the set.
XML#lastChild() Get the last direct child of each element in the set.
XML#prev() Get the immediately preceding sibling of each element in the set.
XML#next() Get the immediately following sibling of each element in the set.
public void children() {
    XML root1 = I.xml("""
            <root>
                <first/>
                <center/>
                <last/>
            </root>
            """);
    assert root1.children().size() == 3;

    XML root2 = I.xml("""
            <root>
                text<first/>is
                <child>
                    <center/>
                </child>
                ignored<last/>!!
            </root>
            """);
    assert root2.children().size() == 3;

    XML root3 = I.xml("<root/>");
    assert root3.children().size() == 0;
}
public void next() {
    XML root = I.xml("""
            <root>
                <first/>
                <center/>
                text is ignored
                <last/>
            </root>
            """);

    XML next1 = root.find("first").next();
    assert next1.name().equals("center");

    XML next2 = root.find("center").next();
    assert next2.name().equals("last");

    XML next3 = root.find("last").next();
    assert next3.size() == 0;
}

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 an XML 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:

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 ,-*/?LWR
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 - Use Object#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:

  1. Look for category.property (e.g. system.file)
  2. If not found, look for *.property (e.g. *.file)
  3. 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.

Template Engine

Compares the performance of Sinobu's Mustache template engine implementation. Measures rendering speed and overhead for template processing with context data.