HappyTree is a data structure API designed for handling Java objects that have
tree-like behavior, where an @Id attribute is referenced as a
@Parent attribute of its children.
Transform collections of plain Java objects into actual hierarchical tree structures in memory, where each object is wrapped into a tree node called Element.
Perform operations like adding/removing children, moving nodes, copying to other trees, and converting structures to JSON/XML formats.
Handle Java objects as tree nodes with operations like copying, cutting, removing, creating, and updating.
Transform linear data structures with tree-like relationships into actual hierarchical trees.
Build new trees from scratch, adding elements one by one to create custom structures.
Convert your tree structures to JSON or XML format for easy serialization and data exchange.
Generic-based API with full type safety and compile-time checking for your domain objects.
Simple annotation-based configuration with Maven/Gradle support and minimal setup required.
Add the required annotations to your Java class:
@Tree
public class Directory {
@Id
private Integer directoryId;
@Parent
private Integer directoryParentId;
private String directoryName;
// Default constructor and Getters & Setters
}
Create a tree from an existing collection:
// From existing collection
Collection<Directory> directories = getDirectories();
TreeManager manager = HappyTree.createTreeManager();
TreeTransaction transaction = manager.getTransaction();
transaction.initializeSession("myTree", directories);
// Or create an empty tree
transaction.initializeSession("emptyTree", Directory.class);
Perform operations on your tree elements:
// Get elements
Element<Directory> admin = manager.getElementById(adminId);
Element<Directory> accessControl = manager.getElementById(accessId);
// Move elements
manager.cut(accessControl, admin);
// Create new elements
Directory security = new Directory();
security.setDirectoryId(securityId);
security.setDirectoryParentId(adminId);
Element<Directory> securityElement =
manager.createElement(securityId, adminId, security);
securityElement = manager.persistElement(securityElement);
// Update the element
security = securityElement.unwrap();
security.setDirectoryName("Security");
securityElement.wrap(security);
securityElement = manager.updateElement(securityElement);
// Export the element 'Security' to JSON
System.out.println(securityElement.toJSON());
// Export the entire tree to XML
Element<Directory> root = manager.root();
System.out.println(root.toXML());
Maven
Add to your pom.xml:
<dependency>
<groupId>com.madzera</groupId>
<artifactId>happytree</artifactId>
<version>2.0.0</version>
</dependency>
Gradle
Add to your build.gradle:
implementation 'com.madzera:happytree:2.0.0'
The groupId has changed from com.madzera.happytree to com.madzera in version 2.0.0
// 1. Define your model
@Tree
public class Directory {
@Id
private Integer directoryId;
@Parent
private Integer directoryParentId;
private String directoryName;
// Constructors, getters, setters...
}
// 2. Create or retrieve data from a previous structure (DB)
List<Directory> directories = Arrays.asList(
new Directory(1, null, "Root"),
new Directory(2, 1, "Documents"),
new Directory(3, 1, "Pictures"),
new Directory(4, 2, "Work"),
new Directory(5, 2, "Personal")
);
// 3. Initialize the tree
TreeManager manager = HappyTree.createTreeManager();
TreeTransaction transaction = manager.getTransaction();
transaction.initializeSession("fileSystem", directories);
// 4. Retrieve the specific 'Pictures' and 'Personal' nodes
Element<Directory> pictures = manager.getElementById(3);
Element<Directory> personal = manager.getElementById(5);
// 5. Move 'Personal' from 'Documents' to 'Pictures'
Element<Directory> personal = manager.getElementById(5);
manager.cut(personal, pictures);
// 6. Export the result in JSON format and print it
Element<Directory> root = manager.root();
String json = root.toJSON();
System.out.println(json);
Represents a tree node - access data, navigate children, and export to JSON/XML
| Method | Description |
|---|---|
getId() |
Obtains the element identifier. This identifier is unique within the tree session when attached to the tree |
setId(Object id) |
Sets the element identifier. The change requires updating the element to take effect. The identifier must be unique and nonnull |
getParent() |
Obtains the parent identifier of this element |
setParent(Object parent) |
Sets the parent identifier reference of this element. If null or nonexistent parent, the element will be at root level when persisted/updated |
getChildren() |
Obtains all child elements of the current element. This includes all descendants recursively |
addChildren(Element<T> child) |
Adds a new child element into the current element. If the child contains children, they will also be added |
addChildren(Collection<Element<T>> children) |
Adds a list of child elements to be contained in the current children list. Includes all nested children recursively |
getElementById(Object id) |
Searches within the current element for an element according to the @Id parameter. Returns null if not found. Search is performed recursively |
removeChildren(Collection<Element<T>> children) |
Removes a subset of elements within this one. All children and elements below the hierarchy are also removed recursively |
removeChild(Element<T> child) |
Removes the specified child element from the children list. All its children and elements below are also removed recursively |
removeChild(Object id) |
Removes the element from the children list by @Id. The element and all its children are removed |
wrap(T object) |
Encapsulates any object node within the element, as long as it has the same class type as other objects in the same tree session |
unwrap() |
Returns a copy of the object node wrapped in this element. Provides access to the encapsulated object |
attachedTo() |
Returns the TreeSession instance to which this element belongs. An element is always associated with a session |
lifecycle() |
Returns the current lifecycle state of this element (NOT_EXISTED, ATTACHED, or DETACHED) |
toJSON() |
Converts the whole element structure into a JSON format (minified). This includes all children recursively |
toPrettyJSON() |
Converts the whole element structure into a well-formatted JSON string. This includes all children recursively |
toXML() |
Converts the whole element structure into an XML string (minified). This includes all children recursively |
toPrettyXML() |
Converts the whole element structure into a well-formatted XML string. This includes all children recursively |
search(Predicate<Element<T>> condition) |
Searches for elements that satisfy a specific condition within this element and its children recursively. Returns a list of matching elements with their hierarchical structure preserved |
apply(Consumer<Element<T>> action) |
Applies a function to be performed on this element and all its children recursively. Changes are not automatically reflected and require persist/update operations |
apply(Consumer<Element<T>> action, Predicate<Element<T>> condition) |
Applies a function to be performed on elements that satisfy the specified condition within the element subtree. Changes require persist/update operations |
Handles elements within tree sessions - create, update, move, copy, and query tree nodes
| Method | Description |
|---|---|
cut(Element<T> from, Element<T> to) |
Cuts the source element to inside of the target element, whether for the same session or not (they must have the same object type). All children of the source element will be cut as well. If target is null, moves to root level |
cut(Object from, Object to) |
Cuts an element identified by its @Id and moves it inside another element within the same session. Returns null if the target element is not found. If the target element is null or not found, the source element is moved to the root level |
copy(Element<T> from, Element<T> to) |
Copies the source element into the target element in another tree session. The entire structure of the copied element will be pasted inside the target element. Copying within the same tree session would result in a duplicate @Id exception |
removeElement(Element<T> element) |
Removes the corresponding element from the tree session and returns the removed element. After removal, the element and all its children will have the NOT_EXISTED state |
removeElement(Object id) |
Removes the element by its @Id. All children of the found element are removed as well from the removed element itself. Returns null if @Id cannot be found |
getElementById(Object id) |
Returns the element given its @Id in the tree session. Returns null if the @Id is null or if the element cannot be found in the tree |
containsElement(Element<T> parent, Element<T> descendant) |
Verifies whether the parent element is inside of it the descendant element in the current session. Returns false if elements are null or not attached |
containsElement(Object parent, Object descendant) |
Verifies whether the parent element (identified by @Id) contains the descendant element (identified by @Id) within the current session. Returns false if either element is not found |
containsElement(Element<T> element) |
Verifies that the current tree session has the specified element. Returns false if element is null, not found, or not in ATTACHED state |
containsElement(Object id) |
Verifies that the current tree session has the specified element by the given @Id. Returns false if element is not found or @Id is null |
createElement(Object id, Object parent, T wrappedNode) |
Creates an element with the @Id, parent and the wrapped object node. Returns a new element with the NOT_EXISTED state in lifecycle. Must be persisted to be inserted to the tree |
persistElement(Element<T> newElement) |
Persists a new element into the current tree session. The new element must have a unique identifier and NOT_EXISTED state. Returns a copy with ATTACHED state |
updateElement(Element<T> element) |
Updates the state of the element within the tree. Returns a copy with ATTACHED state |
getTransaction() |
Obtains the TreeTransaction instance associated with this manager. Every operation defined in this interface needs to check the transaction |
root() |
Returns the root of the tree in the current session. The root encompasses all other elements and has no @Id, @Parent, or object wrapped node |
search(Predicate<Element<T>> condition) |
Searches for elements that satisfy a specific condition within the entire tree structure. Returns a list of elements (without their children) that match the condition |
apply(Consumer<Element<T>> action) |
Applies a function to be performed on all elements within the entire tree structure (except root). Changes are automatically reflected on the tree session |
apply(Consumer<Element<T>> action, Predicate<Element<T>> condition) |
Applies a function to be performed on elements that satisfy the specified condition within the entire tree structure (except root). Changes are automatically reflected on the tree session |
Represents a tree instance - provides session identification and state
| Method | Description |
|---|---|
getSessionId() |
Returns the session identifier name. A session identifier is defined when the session is initialized and must be unique |
isActive() |
Verifies whether the session is active. Returns true if the session is active (can be handled), false if deactivated (exists in memory but cannot be handled) |
tree() |
Returns the entire tree session structure, represented by the root element. From the root element, it is possible to navigate through all children recursively, accessing the entire tree structure |
Manages tree sessions - initialize, activate, clone, and destroy sessions
| Method | Description |
|---|---|
initializeSession(String identifier, Class<T> type) |
Initializes a new empty tree session with the specified identifier. Creates an empty tree where the API client must create elements one by one. The session is automatically checked out as the current session |
initializeSession(String identifier, Collection<T> nodes) |
Initializes a session with a specified identifier and transforms a list of linear objects (with logical tree structure) into an actual tree structure through the API Transformation Process. The session is automatically available as the current session |
destroySession(String identifier) |
Removes the session with the specified identifier permanently. The tree and its elements within this session are also removed and cannot be retrieved |
destroySession() |
Removes the current session permanently. The tree and its elements within this session are also removed. The API client needs to specify a new session to be checked out after removal |
destroyAllSessions() |
Removes all registered sessions permanently. The removal occurs for both activated and deactivated sessions |
sessionCheckout(String identifier) |
Selects a tree session to work with. The current session remains in the background while the checked-out session becomes the current session. Passing null for non-existent identifier cancels the current session |
activateSession(String identifier) |
Activates a session by the specified identifier. With an active session, its elements can be handled freely within the tree. It does not automatically make it as the current session |
activateSession() |
Activates the current session. With an active session, its elements can be handled freely within the tree. The session will always be active after invoking this method |
deactivateSession(String identifier) |
Deactivates a session by the specified identifier. The session is just disabled but not removed. With a deactivated session, its elements cannot be handled |
deactivateSession() |
Deactivates the current session. The session is just disabled but not removed from the list of registered sessions. With a deactivated session, its elements cannot be handled |
sessions() |
Returns the list of all registered sessions. The list includes both activated and deactivated sessions |
cloneSession(String from, String to) |
Replicates the tree session defined by the "from" identifier to the session defined by the "to" identifier. Faithfully reproduces all elements from source tree to target tree. If target exists, it is replaced. It does not automatically check out the cloned session after the cloning process ends |
cloneSession(TreeSession from, String to) |
Replicates the tree session defined by the "from" session instance to the session defined by the "to" identifier. Faithfully reproduces all elements from source tree to target tree. If target exists, it is replaced. It does not automatically check out the cloned session after the cloning process ends |
currentSession() |
Returns the current session of the transaction. The current session is the one that the transaction is referring to at this moment. Returns null if no session is checked out |
Reorganize your tree structure by moving elements to different parents.
// Initialize tree with directories
TreeManager manager = HappyTree.createTreeManager();
TreeTransaction transaction = manager.getTransaction();
transaction.initializeSession("fileSystem", directories);
// Get elements
Element<Directory> personal = manager.getElementById(5);
Element<Directory> pictures = manager.getElementById(3);
// Move 'Personal' folder under 'Pictures'
manager.cut(personal, pictures);
// Verify the move
assertTrue(manager.containsElement(pictures, personal));
System.out.println("Folder moved successfully!");
Build trees dynamically by creating new elements from scratch.
// Create empty tree session
transaction.initializeSession("newTree", Directory.class);
// Create new directory object
Directory security = new Directory();
security.setDirectoryId(100);
security.setDirectoryParentId(null);
security.setDirectoryName("Security");
// Wrap in Element and persist
Element<Directory> securityElement =
manager.createElement(100, null, security);
securityElement = manager.persistElement(securityElement);
// Element is now part of the tree
assertEquals("ATTACHED", securityElement.lifecycle());
System.out.println("New element created: " + security.getDirectoryName());
Find specific elements using predicates and functional programming.
// Search for directories containing "Doc"
List<Element<Directory>> results = manager.search(element -> {
Directory dir = element.unwrap();
return dir.getDirectoryName().contains("Doc");
});
// Process results
results.forEach(element -> {
Directory dir = element.unwrap();
System.out.println("Found: " + dir.getDirectoryName());
});
// Search within a specific element
Element<Directory> root = manager.root();
List<Element<Directory>> children = root.search(e ->
e.getChildren().size() > 2
);
Apply transformations to multiple elements at once.
// Apply operation to all elements in tree
manager.apply(element -> {
Directory dir = element.unwrap();
// Add prefix to all directory names
dir.setDirectoryName("[ARCHIVED] " + dir.getDirectoryName());
element.wrap(dir);
});
// Apply only to elements matching condition
manager.apply(
element -> {
Directory dir = element.unwrap();
dir.setDirectoryName(dir.getDirectoryName().toUpperCase());
element.wrap(dir);
},
element -> element.getChildren().size() == 0 // Only leaf nodes
);
// Changes are automatically reflected in the tree
System.out.println("Bulk update completed!");
Manage multiple independent trees simultaneously.
TreeManager manager = HappyTree.createTreeManager();
TreeTransaction transaction = manager.getTransaction();
// Initialize multiple sessions
transaction.initializeSession("personal", personalDirs);
transaction.initializeSession("work", workDirs);
// Switch between sessions
transaction.sessionCheckout("personal");
Element<Directory> personalRoot = manager.root();
transaction.sessionCheckout("work");
Element<Directory> workRoot = manager.root();
// Copy element between sessions
Element<Directory> project = manager.getElementById(42);
manager.copy(project, personalRoot);
// Clone entire session
transaction.cloneSession("work", "work-backup");
System.out.println("Session cloned successfully!");
Convert your tree to JSON or XML format for serialization.
// Get root element
Element<Directory> root = manager.root();
// Export to minified JSON
String json = root.toJSON();
System.out.println(json);
// Export to formatted JSON (readable)
String prettyJson = root.toPrettyJSON();
System.out.println(prettyJson);
// Export to XML
String xml = root.toXML();
System.out.println(xml);
// Export formatted XML
String prettyXml = root.toPrettyXML();
// Export specific subtree
Element<Directory> documents = manager.getElementById(2);
String subtreeJson = documents.toPrettyJSON();
// Save to file
Files.write(Paths.get("tree-export.json"),
prettyJson.getBytes(StandardCharsets.UTF_8));
We welcome contributions! Whether it's bug reports, feature requests, documentation improvements, or code contributions, your help is appreciated.