Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright (c) 2004-2026, University of Oslo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.hisp.dhis.organisationunit;

import static java.util.Objects.requireNonNull;

import java.util.List;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import org.hisp.dhis.common.UID;

/**
* @param pager pager information
* @param organisationUnits list of OUs matching the query
* @param ancestors additional ancestors for the query
*/
public record OrgTree(
@CheckForNull Pager pager,
@Nonnull Stream<OrgTreeEntry> organisationUnits,
@Nonnull Stream<OrgTreeEntry> ancestors) {

public record OrgTreeEntry(
@Nonnull OrgUnitPath path, @Nonnull String displayName, int level, boolean leaf) {

public OrgTreeEntry(@Nonnull OrgUnitPath path, @Nonnull String displayName, boolean leaf) {
this(path, displayName, path.length(), leaf);
}

public OrgTreeEntry {
requireNonNull(path);
requireNonNull(displayName);
if (path.length() != level)
throw new IllegalArgumentException("Path length and level must match");
}

public UID id() {
return path.toUID();
}

public boolean isDirectParent(OrgTreeEntry other) {
OrgUnitPath parent = path.parent();
return parent != null && parent.equals(other.path);
}
}

public record Pager(int page, int pageSize, int total) {
public int pageCount() {
return 1 + (total / pageSize);
}
}

/**
* @param paths some path to check if they are not flat (all on the same level)
* @return if given path are on different levels
*/
public static boolean isHierarchical(List<OrgUnitPath> paths) {
int len = paths.size();
if (len <= 1) return false;
int level = paths.get(0).length();
return paths.stream().allMatch(path -> level == path.length());
}

/*
Hierarchy Sorting
*/

/**
* @param entries must be sorted already by level as 1st or major sort order
* @return the input sorted hierarchical, meaning children occur directly below their parents
* maintaining any 2nd order sorting the input had for the children
*/
public static List<OrgTreeEntry> sortedHierarchical(List<OrgTreeEntry> entries) {
int len = entries.size();
if (len <= 1) return entries;
OrgTreeEntry[] sorted = entries.toArray(OrgTreeEntry[]::new);
int minMoveLevel = sorted[0].level + 1;
int from = 1;
while (from < len && sorted[from].level < minMoveLevel) from++;
while (from < len) {
// find to (insert position)
int to = from - 1;
OrgTreeEntry e = sorted[from];
while (to >= 0 && !e.isDirectParent(sorted[to])) to--;
if (to >= 0) { // parent found...
OrgTreeEntry parent = sorted[to];
to++; // move past parent
// move past previously moved entries
while (to < from && sorted[to].level == e.level && sorted[to].isDirectParent(parent)) to++;
System.arraycopy(sorted, to, sorted, to + 1, from - to);
sorted[to] = e;
}
from++;
}
return List.of(sorted);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Copyright (c) 2004-2026, University of Oslo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.hisp.dhis.organisationunit;

import static java.util.Objects.requireNonNull;

import com.fasterxml.jackson.annotation.JsonAlias;
import java.util.List;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import org.apache.commons.lang3.StringUtils;
import org.hisp.dhis.common.Locale;
import org.hisp.dhis.common.OpenApi;
import org.hisp.dhis.common.UID;

/**
* A query either uses {@link #search} to find OUs containing the search term in their display name
* (or short name) or they use {@link #depth} in combination with {@link #roots} to page through
* subtrees.
*
* <p>Both queries might be combined with {@link #groups} and {@link #groupSets} filters as well as
* a {@link #level} and {@link #currentlyOpen} filters.
*
* @param page
* @param pageSize
* @param hierarchySize number of OUs in the user's search hierarchy (approximation)
* @param roots
* @param groups
* @param level
* @param search
* @param shortName
* @param depth
*/
public record OrgTreeParams(
int page,
int pageSize,
int hierarchySize,
@Nonnull Locale locale,
@Nonnull List<UID> roots,
@Nonnull List<UID> groups,
@CheckForNull Integer level,
@CheckForNull Boolean currentlyOpen,
@CheckForNull String search,
boolean shortName,
@CheckForNull Integer depth) {

public OrgTreeParams {
requireNonNull(locale);
requireNonNull(roots);
requireNonNull(groups);
if (page < 1) throw new IllegalArgumentException("Page must be positive");
if (pageSize < 1) throw new IllegalArgumentException("Page size must be positive");
if (level != null && level < 1) throw new IllegalArgumentException("Level must be positive");
if (depth != null && depth < 1) throw new IllegalArgumentException("Depth must be positive");
}

public int offset() {
return (page - 1) * pageSize;
}

/**
* @return the {@link #search} as DB like expression
*/
@CheckForNull
public String searchLike() {
return likePattern(search);
}

private static String likePattern(String pattern) {
if (StringUtils.isBlank(pattern)) return null;
int len = pattern.length();
if (len > 2 && pattern.charAt(0) == '"' && pattern.charAt(len - 1) == '"')
return likeEscape(pattern);
if (pattern.indexOf('*') < 0) return "%" + likeEscape(pattern) + "%";
return pattern.replace("*", "%").replace("?", "_").replace("\\", "\\\\");
}

private static String likeEscape(String input) {
return input.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");
}

/** The parameters as seen and provided from user input. */
public record Input(

// pager
@CheckForNull Integer page,
@CheckForNull Integer pageSize,

// context
@CheckForNull Locale locale,

// filters
@OpenApi.Description(
"""
Includes only children to any of the given nodes.
If an ID in the list is a children of another ID in the list the result is the same as ignoring that ID.
If no root is given the current user's search hierarchy is used.
""")
@CheckForNull
List<UID> roots,
@OpenApi.Description(
"Includes only matches that are member in any of the given group (if not empty).")
@CheckForNull
List<UID> groups,
@OpenApi.Description(
"Includes only matches that are member of any groups contained in any of the given group sets (if not empty).")
@CheckForNull
List<UID> groupSets,
@OpenApi.Description("Includes only matches with the given level.") @CheckForNull
Integer level,
@OpenApi.Description(
"Includes only matches that are currently open for data entry based on their `openingDate` and `closedDate`")
@CheckForNull
Boolean currentlyOpen,
@OpenApi.Description(
"""
Includes any OU where `displayName` has a substring match for the `search` term.
When combined with `shortName` the `displayShortName` is searched instead.""")
@CheckForNull
@JsonAlias("q")
String search,
@OpenApi.Description(
"Can be used with `search` to match on `displayShortName` instead of `displayName`")
@CheckForNull
Boolean shortName,
@OpenApi.Description(
"""
Includes the given number of levels of children for each of the given `roots`.
Cannot be combined with `search`.""")
@CheckForNull
Integer depth) {

public Input {
if (depth != null && search != null)
throw new IllegalArgumentException(
"A query with `search` term cannot be limited by `depth` at the same time.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2004-2026, University of Oslo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.hisp.dhis.organisationunit;

import javax.annotation.Nonnull;

/**
* Service API to search {@link OrganisationUnit} and fetch the matches as a connected (sub)tree.
*
* @since 2.44
* @author Jan Bernitt
*/
public interface OrgTreeService {

@Nonnull
OrgTreeParams decode(@Nonnull OrgTreeParams.Input params);

@Nonnull
OrgTree query(@Nonnull OrgTreeParams params);
}
Loading
Loading