You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hugegraph.apache.org by ji...@apache.org on 2026/07/10 18:52:41 UTC
(hugegraph) branch master updated: feat(server): adapt Hubble 2.0 & add graph/role management (#3008)
This is an automated email from the ASF dual-hosted git repository.
imbajin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hugegraph.git
The following commit(s) were added to refs/heads/master by this push:
new 99936be5f feat(server): adapt Hubble 2.0 & add graph/role management (#3008)
99936be5f is described below
commit 99936be5f41fccd193f120e01206e3cf3c73a050
Author: Yeaury <ye...@qq.com>
AuthorDate: Sat Jul 11 02:52:37 2026 +0800
feat(server): adapt Hubble 2.0 & add graph/role management (#3008)
Main Changes
Graph management (GraphsAPI.java)
GraphSpace and default-role APIs (GraphSpaceAPI.java)
GraphSpace managers (ManagerAPI.java)
Schema templates (SchemaTemplateAPI.java)
Authentication and graph metadata support
---------
Co-authored-by: imbajin <ji...@apache.org>
---
.../main/java/org/apache/hugegraph/api/API.java | 32 +-
.../org/apache/hugegraph/api/auth/ManagerAPI.java | 54 +++-
.../org/apache/hugegraph/api/auth/UserAPI.java | 26 +-
.../apache/hugegraph/api/profile/GraphsAPI.java | 352 ++++++++++++++++++++-
.../apache/hugegraph/api/space/GraphSpaceAPI.java | 241 ++++++++++++--
.../hugegraph/api/space/SchemaTemplateAPI.java | 221 +++++++++++++
.../apache/hugegraph/auth/HugeGraphAuthProxy.java | 65 ++++
.../org/apache/hugegraph/config/ServerOptions.java | 2 +-
.../org/apache/hugegraph/core/GraphManager.java | 121 ++++++-
.../org/apache/hugegraph/auth/AuthManager.java | 26 ++
.../java/org/apache/hugegraph/auth/HugeUser.java | 32 +-
.../apache/hugegraph/auth/StandardAuthManager.java | 197 ++++++++++++
.../hugegraph/auth/StandardAuthManagerV2.java | 224 ++++++++++++-
.../backend/cache/CachedSchemaTransactionV2.java | 10 +-
.../java/org/apache/hugegraph/util/ConfigUtil.java | 39 +++
.../org/apache/hugegraph/dist/HugeGraphServer.java | 114 +++++--
.../apache/hugegraph/dist/HugeGremlinServer.java | 37 ++-
.../org/apache/hugegraph/dist/MemoryMonitor.java | 2 +-
hugegraph-server/hugegraph-test/pom.xml | 6 +
.../org/apache/hugegraph/api/ApiTestSuite.java | 3 +
.../java/org/apache/hugegraph/api/BaseApiTest.java | 11 +-
.../api/DefaultRoleApiStandaloneTest.java | 182 +++++++++++
.../apache/hugegraph/api/GraphSpaceApiTest.java | 156 +++++++++
.../hugegraph/api/GraphsApiStandaloneTest.java | 306 ++++++++++++++++++
.../hugegraph/api/SchemaTemplateApiTest.java | 133 ++++++++
.../org/apache/hugegraph/core/EdgeCoreTest.java | 18 +-
.../org/apache/hugegraph/core/MultiGraphsTest.java | 6 +-
.../org/apache/hugegraph/core/RamTableTest.java | 15 +-
.../org/apache/hugegraph/core/VertexCoreTest.java | 27 +-
.../org/apache/hugegraph/unit/UnitTestSuite.java | 4 +
.../unit/api/space/GraphSpaceAPITest.java | 274 ++++++++++++++++
.../unit/auth/HugeGraphAuthProxyTest.java | 49 +++
.../unit/cache/CachedSchemaTransactionTest.java | 85 ++---
.../apache/hugegraph/dist/HugeGraphServerTest.java | 79 +++++
.../hugegraph/unit/config/ServerOptionsTest.java | 22 ++
35 files changed, 3013 insertions(+), 158 deletions(-)
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/API.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/API.java
index 3220cf6b0..b50195b85 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/API.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/API.java
@@ -17,6 +17,7 @@
package org.apache.hugegraph.api;
+import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.Callable;
@@ -86,9 +87,16 @@ public class API {
MetricsUtil.registerMeter(API.class, "expected-error");
private static final Meter unknownErrorMeter =
MetricsUtil.registerMeter(API.class, "unknown-error");
- private static final String STANDALONE_ERROR =
+ protected static final String STANDALONE_ERROR =
"GraphSpace management is not supported in standalone mode";
+ /**
+ * Shared date formatter for API response timestamps (thread-safe, reusable).
+ * Example output: {@code "2024-05-01 12:30:00"}
+ */
+ protected static final DateTimeFormatter DATE_FORMATTER =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
public static HugeGraph graph(GraphManager manager, String graphSpace,
String graph) {
HugeGraph g = manager.graph(graphSpace, graph);
@@ -282,6 +290,28 @@ public class API {
}
}
+ /**
+ * Returns {@code true} if the profile's {@code "name"} or {@code "nickname"}
+ * field starts with the given prefix. Returns {@code true} when prefix is
+ * null or empty (i.e. no filtering).
+ *
+ * <p>Used by {@code GraphsAPI.listProfile} and {@code GraphSpaceAPI.listProfile}
+ * to filter results by a user-supplied prefix string.
+ *
+ * @param profile a map containing at least a {@code "name"} key
+ * @param prefix the prefix to match against; ignored when blank
+ * @return whether the entry matches the prefix filter
+ */
+ protected static boolean isPrefix(Map<String, Object> profile, String prefix) {
+ if (prefix == null || prefix.isEmpty()) {
+ return true;
+ }
+ String name = profile.get("name").toString();
+ Object nicknameObj = profile.get("nickname");
+ String nickname = nicknameObj != null ? nicknameObj.toString() : "";
+ return name.startsWith(prefix) || nickname.startsWith(prefix);
+ }
+
public static class ApiMeasurer {
public static final String EDGE_ITER = "edge_iterations";
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/ManagerAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/ManagerAPI.java
index 071e4b8a6..37aee8c65 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/ManagerAPI.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/ManagerAPI.java
@@ -22,9 +22,12 @@ package org.apache.hugegraph.api.auth;
import java.util.ArrayList;
import java.util.List;
+import org.apache.commons.lang3.StringUtils;
+
import org.apache.hugegraph.api.API;
import org.apache.hugegraph.api.filter.StatusFilter;
import org.apache.hugegraph.auth.AuthManager;
+import org.apache.hugegraph.auth.HugeDefaultRole;
import org.apache.hugegraph.auth.HugeGraphAuthProxy;
import org.apache.hugegraph.auth.HugePermission;
import org.apache.hugegraph.core.GraphManager;
@@ -259,6 +262,49 @@ public class ManagerAPI extends API {
result));
}
+ @GET
+ @Timed
+ @Path("default")
+ public String checkDefaultRole(@Context GraphManager manager,
+ @PathParam("graphspace") String graphSpace,
+ @QueryParam("role") String role,
+ @QueryParam("graph") String graph) {
+ LOG.debug("check if current user is default role: {} {} {}",
+ role, graphSpace, graph);
+ ensurePdModeEnabled(manager);
+ AuthManager authManager = manager.authManager();
+ String user = HugeGraphAuthProxy.username();
+
+ E.checkArgument(StringUtils.isNotEmpty(role) &&
+ StringUtils.isNotEmpty(graphSpace),
+ "Must pass graphspace and role params");
+
+ HugeDefaultRole defaultRole;
+ try {
+ defaultRole = HugeDefaultRole.valueOf(role.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ E.checkArgument(false, "Invalid role value '%s'", role);
+ defaultRole = null; // unreachable, satisfies compiler
+ }
+ validGraphSpace(manager, graphSpace);
+ boolean hasGraph = defaultRole.equals(HugeDefaultRole.OBSERVER);
+ E.checkArgument(!hasGraph || StringUtils.isNotEmpty(graph),
+ "Must set a graph for observer");
+ if (hasGraph) {
+ validGraph(manager, graphSpace, graph);
+ }
+
+ boolean result;
+ if (hasGraph) {
+ result = authManager.isDefaultRole(graphSpace, graph, user,
+ defaultRole);
+ } else {
+ result = authManager.isDefaultRole(graphSpace, user,
+ defaultRole);
+ }
+ return manager.serializer().writeMap(ImmutableMap.of("check", result));
+ }
+
private void validUser(AuthManager authManager, String user) {
E.checkArgument(authManager.findUser(user) != null ||
authManager.findGroup(user) != null,
@@ -274,7 +320,13 @@ public class ManagerAPI extends API {
private void validGraphSpace(GraphManager manager, String graphSpace) {
E.checkArgument(manager.graphSpace(graphSpace) != null,
- "The graph space is not exist");
+ "The graph space '%s' does not exist", graphSpace);
+ }
+
+ private void validGraph(GraphManager manager, String graphSpace,
+ String graph) {
+ E.checkArgument(manager.graph(graphSpace, graph) != null,
+ "Graph '%s/%s' does not exist", graphSpace, graph);
}
private static class JsonManager implements Checkable {
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/UserAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/UserAPI.java
index de51e6955..7504ad732 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/UserAPI.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/UserAPI.java
@@ -108,9 +108,20 @@ public class UserAPI extends API {
public String list(@Context GraphManager manager,
@Parameter(description = "The graph space name")
@PathParam("graphspace") String graphSpace,
+ @Parameter(description = "Filter by user name")
+ @QueryParam("name") String name,
@Parameter(description = "The limit of results to return")
@QueryParam("limit") @DefaultValue("100") long limit) {
- LOG.debug("GraphSpace [{}] list users", graphSpace);
+ LOG.debug("GraphSpace [{}] list users, name={}", graphSpace, name);
+
+ if (StringUtils.isNotEmpty(name)) {
+ HugeUser user = manager.authManager().findUser(name);
+ if (user == null) {
+ throw new NotFoundException("Can't find user with name '%s'",
+ name);
+ }
+ return manager.serializer().writeAuthElement(user);
+ }
List<HugeUser> users = manager.authManager().listAllUsers(limit);
return manager.serializer().writeAuthElements("users", users);
@@ -178,6 +189,9 @@ public class UserAPI extends API {
@JsonProperty("user_password")
@Schema(description = "The user password", required = true)
private String password;
+ @JsonProperty("user_nickname")
+ @Schema(description = "The user nickname")
+ private String nickname;
@JsonProperty("user_phone")
@Schema(description = "The user phone number")
private String phone;
@@ -197,6 +211,9 @@ public class UserAPI extends API {
if (this.password != null) {
user.password(StringEncoding.hashPassword(this.password));
}
+ if (this.nickname != null) {
+ user.nickname(this.nickname);
+ }
if (this.phone != null) {
user.phone(this.phone);
}
@@ -215,6 +232,7 @@ public class UserAPI extends API {
public HugeUser build() {
HugeUser user = new HugeUser(this.name);
user.password(StringEncoding.hashPassword(this.password));
+ user.nickname(this.nickname);
user.phone(this.phone);
user.email(this.email);
user.avatar(this.avatar);
@@ -233,10 +251,12 @@ public class UserAPI extends API {
@Override
public void checkUpdate() {
E.checkArgument(!StringUtils.isEmpty(this.password) ||
+ this.nickname != null ||
this.phone != null ||
this.email != null ||
- this.avatar != null,
- "Expect one of user password/phone/email/avatar]");
+ this.avatar != null ||
+ this.description != null,
+ "Expect one of user password/nickname/phone/email/avatar/description");
}
}
}
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java
index 9316d7341..94169e4a9 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java
@@ -18,16 +18,22 @@
package org.apache.hugegraph.api.profile;
import java.io.File;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
-
import org.apache.commons.lang3.StringUtils;
import org.apache.hugegraph.HugeException;
import org.apache.hugegraph.HugeGraph;
import org.apache.hugegraph.api.API;
import org.apache.hugegraph.api.filter.StatusFilter;
+import org.apache.hugegraph.auth.AuthManager;
import org.apache.hugegraph.auth.HugeAuthenticator.RequiredPerm;
import org.apache.hugegraph.auth.HugeGraphAuthProxy;
import org.apache.hugegraph.auth.HugePermission;
@@ -36,10 +42,10 @@ import org.apache.hugegraph.core.GraphManager;
import org.apache.hugegraph.space.GraphSpace;
import org.apache.hugegraph.type.define.GraphMode;
import org.apache.hugegraph.type.define.GraphReadMode;
+import org.apache.hugegraph.util.ConfigUtil;
import org.apache.hugegraph.util.E;
import org.apache.hugegraph.util.JsonUtil;
import org.apache.hugegraph.util.Log;
-import org.apache.logging.log4j.util.Strings;
import org.slf4j.Logger;
import com.codahale.metrics.annotation.Timed;
@@ -74,12 +80,22 @@ public class GraphsAPI extends API {
private static final String CONFIRM_DROP = "I'm sure to drop the graph";
private static final String GRAPH_DESCRIPTION = "description";
private static final String GRAPH_ACTION = "action";
+ private static final String UPDATE = "update";
private static final String GRAPH_ACTION_RELOAD = "reload";
private static Map<String, Object> convConfig(Map<String, Object> config) {
Map<String, Object> result = new HashMap<>(config.size());
for (Map.Entry<String, Object> entry : config.entrySet()) {
- result.put(entry.getKey(), entry.getValue().toString());
+ Object value = entry.getValue();
+ E.checkArgument(value != null,
+ "The config value for '%s' cannot be null",
+ entry.getKey());
+ E.checkArgument(value instanceof CharSequence ||
+ value instanceof Number ||
+ value instanceof Boolean,
+ "The config value for '%s' must be scalar",
+ entry.getKey());
+ result.put(entry.getKey(), value.toString());
}
return result;
}
@@ -101,11 +117,12 @@ public class GraphsAPI extends API {
graphs.size());
// Filter by user role
Set<String> filterGraphs = new HashSet<>();
+ boolean adminManager = isAdminManager(manager);
for (String graph : graphs) {
LOG.debug("Get graph {} and verify auth", graph);
String role = RequiredPerm.roleFor(graphSpace, graph,
HugePermission.READ);
- if (sc.isUserInRole(role)) {
+ if (adminManager || sc.isUserInRole(role)) {
try {
graph(manager, graphSpace, graph);
filterGraphs.add(graph);
@@ -120,6 +137,100 @@ public class GraphsAPI extends API {
return ImmutableMap.of("graphs", filterGraphs);
}
+ @GET
+ @Timed
+ @Path("profile")
+ @Produces(APPLICATION_JSON_WITH_CHARSET)
+ @RolesAllowed({"space_member", "$dynamic"})
+ public Object listProfile(@Context GraphManager manager,
+ @Parameter(description = "The graph space name")
+ @PathParam("graphspace") String graphSpace,
+ @Parameter(description = "Filter graphs by name or nickname prefix")
+ @QueryParam("prefix") String prefix,
+ @Context SecurityContext sc) {
+ LOG.debug("List graph profiles in graph space {}", graphSpace);
+ if (null == manager.graphSpace(graphSpace)) {
+ throw new HugeException("Graphspace not exist!");
+ }
+ GraphSpace gs = manager.graphSpace(graphSpace);
+ // graphSpace.nickname() may be null in non-PD mode (GraphManager returns
+ // a placeholder GraphSpace without a nickname set)
+ String gsNickname = gs.nickname() != null ? gs.nickname() : graphSpace;
+
+ String user = HugeGraphAuthProxy.username();
+ Map<String, Date> defaultGraphs;
+ try {
+ AuthManager authManager = manager.authManager();
+ defaultGraphs = authManager.getDefaultGraph(graphSpace, user);
+ } catch (IllegalStateException ignored) {
+ // authManager is not configured (standalone mode without auth)
+ defaultGraphs = Collections.emptyMap();
+ }
+
+ Set<String> graphs = manager.graphs(graphSpace);
+ List<Map<String, Object>> profiles = new ArrayList<>();
+ List<Map<String, Object>> defaultProfiles = new ArrayList<>();
+ boolean adminManager = isAdminManager(manager);
+ for (String graph : graphs) {
+ String role = RequiredPerm.roleFor(graphSpace, graph,
+ HugePermission.READ);
+ if (!adminManager && !sc.isUserInRole(role)) {
+ continue;
+ }
+ try {
+ HugeGraph hg = graph(manager, graphSpace, graph);
+ HugeConfig config = (HugeConfig) hg.configuration();
+ String configResp = ConfigUtil.writeConfigToString(config);
+ Map<String, Object> profile =
+ JsonUtil.fromJson(configResp, Map.class);
+ profile.put("name", graph);
+ profile.put("nickname", hg.nickname());
+ if (!isPrefix(profile, prefix)) {
+ continue;
+ }
+ profile.put("graphspace_nickname", gsNickname);
+
+ boolean isDefault = defaultGraphs.containsKey(graph);
+ profile.put("default", isDefault);
+ if (isDefault) {
+ Date defaultUpdateTime = defaultGraphs.get(graph);
+ if (defaultUpdateTime != null) {
+ LocalDateTime ldt = defaultUpdateTime.toInstant()
+ .atZone(ZoneId.systemDefault()).toLocalDateTime();
+ profile.put("default_update_time",
+ DATE_FORMATTER.format(ldt));
+ }
+ }
+
+ Date createTime = hg.createTime();
+ if (createTime != null) {
+ LocalDateTime ldt = createTime.toInstant()
+ .atZone(ZoneId.systemDefault()).toLocalDateTime();
+ profile.put("create_time", DATE_FORMATTER.format(ldt));
+ }
+
+ if (isDefault) {
+ defaultProfiles.add(profile);
+ } else {
+ profiles.add(profile);
+ }
+ } catch (ForbiddenException ignored) {
+ // ignore graphs the current user has no access to
+ }
+ }
+ defaultProfiles.addAll(profiles);
+ return defaultProfiles;
+ }
+
+ private static boolean isAdminManager(GraphManager manager) {
+ try {
+ return manager.authManager().isAdminManager(
+ HugeGraphAuthProxy.username());
+ } catch (IllegalStateException ignored) {
+ return false;
+ }
+ }
+
@GET
@Timed
@Path("{name}")
@@ -136,6 +247,80 @@ public class GraphsAPI extends API {
return ImmutableMap.of("name", g.name(), "backend", g.backend());
}
+ @POST
+ @Timed
+ @Path("{name}/default")
+ @Produces(APPLICATION_JSON_WITH_CHARSET)
+ @RolesAllowed({"space_member", "$owner=$name"})
+ public Map<String, Object> setDefault(@Context GraphManager manager,
+ @Parameter(description = "The graph space name")
+ @PathParam("graphspace") String graphSpace,
+ @Parameter(description = "The graph name")
+ @PathParam("name") String name) {
+ LOG.debug("Set default graph '{}' in graph space '{}'", name, graphSpace);
+ E.checkArgument(manager.graphSpace(graphSpace) != null,
+ "The graph space '%s' does not exist", graphSpace);
+ E.checkArgument(manager.graph(graphSpace, name) != null,
+ "Graph '%s/%s' does not exist", graphSpace, name);
+ String user = HugeGraphAuthProxy.username();
+ AuthManager authManager;
+ try {
+ authManager = manager.authManager();
+ } catch (IllegalStateException e) {
+ throw new HugeException(STANDALONE_ERROR);
+ }
+ authManager.setDefaultGraph(graphSpace, name, user);
+ Map<String, Date> defaults = authManager.getDefaultGraph(graphSpace, user);
+ return ImmutableMap.of("default_graph", defaults.keySet());
+ }
+
+ @DELETE
+ @Timed
+ @Path("{name}/default")
+ @Produces(APPLICATION_JSON_WITH_CHARSET)
+ @RolesAllowed({"space_member", "$owner=$name"})
+ public Map<String, Object> unsetDefault(@Context GraphManager manager,
+ @Parameter(description = "The graph space name")
+ @PathParam("graphspace") String graphSpace,
+ @Parameter(description = "The graph name")
+ @PathParam("name") String name) {
+ LOG.debug("Unset default graph '{}' in graph space '{}'", name, graphSpace);
+ E.checkArgument(manager.graphSpace(graphSpace) != null,
+ "The graph space '%s' does not exist", graphSpace);
+ E.checkArgument(manager.graph(graphSpace, name) != null,
+ "Graph '%s/%s' does not exist", graphSpace, name);
+ String user = HugeGraphAuthProxy.username();
+ AuthManager authManager;
+ try {
+ authManager = manager.authManager();
+ } catch (IllegalStateException e) {
+ throw new HugeException(STANDALONE_ERROR);
+ }
+ authManager.unsetDefaultGraph(graphSpace, name, user);
+ Map<String, Date> defaults = authManager.getDefaultGraph(graphSpace, user);
+ return ImmutableMap.of("default_graph", defaults.keySet());
+ }
+
+ @GET
+ @Timed
+ @Path("default")
+ @Produces(APPLICATION_JSON_WITH_CHARSET)
+ @RolesAllowed({"space_member", "$dynamic"})
+ public Map<String, Object> getDefault(@Context GraphManager manager,
+ @Parameter(description = "The graph space name")
+ @PathParam("graphspace") String graphSpace) {
+ LOG.debug("Get default graphs in graph space '{}'", graphSpace);
+ String user = HugeGraphAuthProxy.username();
+ AuthManager authManager;
+ try {
+ authManager = manager.authManager();
+ } catch (IllegalStateException e) {
+ throw new HugeException(STANDALONE_ERROR);
+ }
+ Map<String, Date> defaults = authManager.getDefaultGraph(graphSpace, user);
+ return ImmutableMap.of("default_graph", defaults.keySet());
+ }
+
@DELETE
@Timed
@Path("{name}")
@@ -155,6 +340,89 @@ public class GraphsAPI extends API {
manager.dropGraph(graphSpace, name, true);
}
+ @PUT
+ @Timed
+ @Path("{name}")
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON_WITH_CHARSET)
+ @RolesAllowed({"space"})
+ public Map<String, String> manage(@Context GraphManager manager,
+ @Parameter(description = "The graph space name")
+ @PathParam("graphspace") String graphSpace,
+ @Parameter(description = "The graph name")
+ @PathParam("name") String name,
+ @Parameter(description = "Action map: {'action':'update','update':{...}}")
+ Map<String, Object> actionMap) {
+ LOG.debug("Manage graph '{}' with action '{}'", name, actionMap);
+ E.checkArgument(actionMap != null && actionMap.containsKey(GRAPH_ACTION),
+ "Invalid request body '%s'", actionMap);
+ Object value = actionMap.get(GRAPH_ACTION);
+ E.checkArgument(value instanceof String,
+ "Invalid action type '%s', must be string",
+ value == null ? "null" : value.getClass().getSimpleName());
+ String action = (String) value;
+ switch (action) {
+ case UPDATE:
+ E.checkArgument(actionMap.containsKey(UPDATE),
+ "Please pass '%s' for graph update",
+ UPDATE);
+ value = actionMap.get(UPDATE);
+ E.checkArgument(value instanceof Map,
+ "The '%s' must be map, but got %s",
+ UPDATE,
+ value == null ? "null" : value.getClass().getSimpleName());
+ @SuppressWarnings("unchecked")
+ Map<String, Object> graphMap = (Map<String, Object>) value;
+ Object graphNameValue = graphMap.get("name");
+ E.checkArgument(graphNameValue == null ||
+ graphNameValue instanceof String,
+ "The 'name' must be string, but got %s",
+ graphNameValue == null ? "null" :
+ graphNameValue.getClass().getSimpleName());
+ String graphName = (String) graphNameValue;
+ E.checkArgument(graphName != null && graphName.equals(name),
+ "Different name in update body '%s' with path '%s'",
+ graphName, name);
+ HugeGraph exist = graph(manager, graphSpace, name);
+ Object nicknameValue = graphMap.get("nickname");
+ E.checkArgument(nicknameValue == null ||
+ nicknameValue instanceof String,
+ "The 'nickname' must be string, but got %s",
+ nicknameValue == null ? "null" :
+ nicknameValue.getClass().getSimpleName());
+ String nickname = (String) nicknameValue;
+ if (!StringUtils.isEmpty(nickname)) {
+ GraphManager.checkNickname(nickname);
+ boolean existedNickname;
+ if (manager.isPDEnabled()) {
+ existedNickname = manager.isExistedGraphNickname(graphSpace, nickname);
+ } else {
+ existedNickname = false;
+ for (String existedGraphName : manager.graphs(graphSpace)) {
+ if (name.equals(existedGraphName)) {
+ continue;
+ }
+ HugeGraph graph = manager.graph(graphSpace, existedGraphName);
+ if (graph != null && nickname.equals(graph.nickname())) {
+ existedNickname = true;
+ break;
+ }
+ }
+ }
+ E.checkArgument(!existedNickname || nickname.equals(exist.nickname()),
+ "Nickname '%s' has already existed in graphspace '%s'",
+ nickname, graphSpace);
+ // Delegate to GraphManager: handles both in-memory update and
+ // PD-mode persistence (non-PD mode is in-memory only).
+ manager.updateGraphNickname(graphSpace, name, nickname);
+ }
+ return ImmutableMap.of(name, "updated");
+ default:
+ E.checkArgument(false, "Invalid graph action: '%s'", action);
+ return ImmutableMap.of(name, "invalid");
+ }
+ }
+
@PUT
@Timed
@Path("manage")
@@ -174,8 +442,8 @@ public class GraphsAPI extends API {
manager.reload();
return ImmutableMap.of("graphs", "reloaded");
}
- throw new AssertionError(String.format(
- "Invalid graphs action: '%s'", action));
+ E.checkArgument(false, "Invalid graphs action: '%s'", action);
+ return ImmutableMap.of("graphs", "invalid");
}
@POST
@@ -207,11 +475,32 @@ public class GraphsAPI extends API {
if (StringUtils.isEmpty(clone)) {
// Only check required parameters when creating new graph, not when cloning
E.checkArgument(configs != null, "Config parameters cannot be null");
- String[] requiredKeys = {"backend", "serializer", "store"};
- for (String key : requiredKeys) {
- Object value = configs.get(key);
- E.checkArgument(value instanceof String && !StringUtils.isEmpty((String) value),
- "Required parameter '%s' is missing or empty", key);
+ // Always required by TinkerPop's GraphFactory.open()
+ configs.putIfAbsent("gremlin.graph",
+ "org.apache.hugegraph.HugeFactory");
+ if (manager.isPDEnabled()) {
+ // Auto-fill HStore/PD mode defaults only when in distributed mode
+ configs.putIfAbsent("backend", "hstore");
+ } else {
+ // Auto-fill standalone (RocksDB) mode defaults
+ configs.putIfAbsent("backend", "rocksdb");
+ }
+ configs.putIfAbsent("serializer", "binary");
+ // 'store' is safe to default to graph name in both PD and non-PD modes
+ configs.putIfAbsent("store", name);
+ // Map frontend 'schema' field to backend config key
+ boolean hasSchema = configs.containsKey("schema");
+ Object schema = configs.remove("schema");
+ if (hasSchema) {
+ E.checkArgument(schema != null,
+ "The config value for 'schema' cannot be null");
+ E.checkArgument(schema instanceof CharSequence ||
+ schema instanceof Number ||
+ schema instanceof Boolean,
+ "The config value for 'schema' must be scalar");
+ }
+ if (schema != null && !schema.toString().isEmpty()) {
+ configs.put("schema.init_template", schema.toString());
}
}
@@ -220,15 +509,19 @@ public class GraphsAPI extends API {
if (StringUtils.isNotEmpty(clone)) {
// Clone from existing graph
LOG.debug("Clone graph '{}' to '{}' in graph space '{}'", clone, name, graphSpace);
- graph = manager.cloneGraph(graphSpace, clone, name, convConfig(configs));
+ Map<String, Object> cloneConfigs = configs != null ? configs : new HashMap<>();
+ graph = manager.cloneGraph(graphSpace, clone, name, convConfig(cloneConfigs));
} else {
// Create new graph
graph = manager.createGraph(graphSpace, name, creator,
convConfig(configs), true);
}
- String description = (String) configs.get(GRAPH_DESCRIPTION);
+ Object descriptionValue = (configs != null) ?
+ configs.get(GRAPH_DESCRIPTION) : null;
+ String description = descriptionValue != null ?
+ descriptionValue.toString() : null;
if (description == null) {
- description = Strings.EMPTY;
+ description = StringUtils.EMPTY;
}
Object result = ImmutableMap.of("name", graph.name(),
"nickname", graph.nickname(),
@@ -239,6 +532,37 @@ public class GraphsAPI extends API {
return result;
}
+ /**
+ * Create graph via text/plain (hugegraph-client compatibility).
+ * Client sends: POST /graphspaces/{graphspace}/graphs/{name}
+ * with Content-Type: text/plain and body containing JSON config string.
+ */
+ @POST
+ @Timed
+ @Path("{name}")
+ @StatusFilter.Status(StatusFilter.Status.CREATED)
+ @Consumes("text/plain")
+ @Produces(APPLICATION_JSON_WITH_CHARSET)
+ @RolesAllowed({"space"})
+ public Object createByText(@Context GraphManager manager,
+ @Parameter(description = "The graph space name")
+ @PathParam("graphspace") String graphSpace,
+ @Parameter(description = "The graph name to create")
+ @PathParam("name") String name,
+ @Parameter(description = "The graph name to clone from (optional)")
+ @QueryParam("clone_graph_name") String clone,
+ String configText) {
+ LOG.debug("Create graph {} with text config in graph space '{}'",
+ name, graphSpace);
+ Map<String, Object> configs = null;
+ if (configText != null && !configText.isEmpty()) {
+ configs = JsonUtil.fromJson(configText, Map.class);
+ }
+ return create(manager, graphSpace, name, clone, configs);
+ }
+
+
+
@GET
@Timed
@Path("{name}/conf")
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/GraphSpaceAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/GraphSpaceAPI.java
index 35bc40aed..81f13cf3f 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/GraphSpaceAPI.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/GraphSpaceAPI.java
@@ -1,35 +1,37 @@
/*
- * Copyright 2017 HugeGraph Authors
- *
* Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with this
- * work for additional information regarding copyright ownership. The ASF
- * licenses this file to You under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
package org.apache.hugegraph.api.space;
-import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.codec.digest.DigestUtils;
-import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang3.StringUtils;
+
import org.apache.hugegraph.api.API;
import org.apache.hugegraph.api.filter.StatusFilter.Status;
import org.apache.hugegraph.auth.AuthManager;
+import org.apache.hugegraph.auth.HugeDefaultRole;
import org.apache.hugegraph.auth.HugeGraphAuthProxy;
import org.apache.hugegraph.core.GraphManager;
import org.apache.hugegraph.define.Checkable;
@@ -38,10 +40,10 @@ import org.apache.hugegraph.space.GraphSpace;
import org.apache.hugegraph.util.E;
import org.apache.hugegraph.util.JsonUtil;
import org.apache.hugegraph.util.Log;
-import org.apache.logging.log4j.util.Strings;
import org.slf4j.Logger;
import com.codahale.metrics.annotation.Timed;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.collect.ImmutableMap;
@@ -52,6 +54,7 @@ import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Singleton;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
@@ -103,6 +106,172 @@ public class GraphSpaceAPI extends API {
return gsInfo;
}
+ @POST
+ @Timed
+ @Status(Status.CREATED)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON_WITH_CHARSET)
+ @Path("{graphspace}/role")
+ @RolesAllowed({"admin", "space"})
+ public String setDefaultRole(@Context GraphManager manager,
+ @PathParam("graphspace") String name,
+ JsonDefaultRole jsonRole) {
+ ensurePdModeEnabled(manager);
+ E.checkArgumentNotNull(jsonRole, "Request body cannot be null");
+ E.checkArgument(StringUtils.isNotEmpty(jsonRole.user),
+ "The 'user' field cannot be null or empty");
+ E.checkArgument(StringUtils.isNotEmpty(jsonRole.role),
+ "The 'role' field cannot be null or empty");
+ String user = jsonRole.user;
+ String graph = jsonRole.graph;
+ HugeDefaultRole role;
+ try {
+ role = HugeDefaultRole.valueOf(jsonRole.role.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ E.checkArgument(false, "Invalid role value '%s'", jsonRole.role);
+ role = null; // unreachable, satisfies compiler
+ }
+ validGraphSpace(manager, name);
+ LOG.debug("Create default role: {} {} {}", user, role,
+ name);
+ AuthManager authManager = manager.authManager();
+ String operator = HugeGraphAuthProxy.username();
+ validPermission(hasAdminOrSpaceManagerPerm(manager, name, operator),
+ operator, "default_role.create");
+ E.checkArgument(authManager.findUser(user) != null ||
+ authManager.findGroup(user) != null,
+ "The user or group is not exist");
+ // only admin can set space admin
+ if (!authManager.isAdminManager(operator) && role.equals(HugeDefaultRole.SPACE)) {
+ throw new ForbiddenException("Forbidden to set role " + role.toString());
+ }
+
+ boolean hasGraph = role.equals(HugeDefaultRole.OBSERVER);
+
+ E.checkArgument(!hasGraph || StringUtils.isNotEmpty(graph),
+ "Must set a graph for observer");
+ if (hasGraph) {
+ validGraph(manager, name, graph);
+ }
+
+ Map<String, String> result = new HashMap<>();
+ result.put("user", user);
+ result.put("role", jsonRole.role);
+ result.put("graphSpace", name);
+
+ if (hasGraph) {
+ authManager.createDefaultRole(name, user, role, graph);
+ result.put("graph", graph);
+ } else {
+ authManager.createSpaceDefaultRole(name, user, role);
+ }
+
+ return manager.serializer().writeMap(result);
+ }
+
+ @GET
+ @Timed
+ @Path("{graphspace}/role")
+ @RolesAllowed({"admin", "space"})
+ public String checkDefaultRole(@Context GraphManager manager,
+ @PathParam("graphspace") String name,
+ @QueryParam("user") String user,
+ @QueryParam("role") String role,
+ @QueryParam("graph") String graph) {
+ ensurePdModeEnabled(manager);
+ E.checkArgument(StringUtils.isNotEmpty(user),
+ "The 'user' query param cannot be null or empty");
+ E.checkArgument(StringUtils.isNotEmpty(role),
+ "The 'role' query param cannot be null or empty");
+ LOG.debug("Check space role: {} {} {}", user, role,
+ name);
+ AuthManager authManager = manager.authManager();
+
+ HugeDefaultRole defaultRole;
+ try {
+ defaultRole = HugeDefaultRole.valueOf(role.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ E.checkArgument(false, "Invalid role value '%s'", role);
+ defaultRole = null; // unreachable, satisfies compiler
+ }
+ validGraphSpace(manager, name);
+ String operator = HugeGraphAuthProxy.username();
+ validPermission(hasAdminOrSpaceManagerPerm(manager, name, operator),
+ operator, "default_role.check");
+ // Only admin can inspect the space manager default role.
+ if (!authManager.isAdminManager(operator) &&
+ defaultRole.equals(HugeDefaultRole.SPACE)) {
+ throw new ForbiddenException("Forbidden to check role " + role);
+ }
+ boolean hasGraph = defaultRole.equals(HugeDefaultRole.OBSERVER);
+ E.checkArgument(!hasGraph || StringUtils.isNotEmpty(graph),
+ "Must set a graph for observer");
+ if (hasGraph) {
+ validGraph(manager, name, graph);
+ }
+
+ boolean result;
+ if (hasGraph) {
+ result = authManager.isDefaultRole(name, graph, user,
+ defaultRole);
+ } else {
+ result = authManager.isDefaultRole(name, user,
+ defaultRole);
+ }
+ return manager.serializer().writeMap(ImmutableMap.of("check", result));
+ }
+
+ @DELETE
+ @Timed
+ @Path("{graphspace}/role")
+ @RolesAllowed({"admin", "space"})
+ public void deleteDefaultRole(@Context GraphManager manager,
+ @PathParam("graphspace") String name,
+ @QueryParam("user") String user,
+ @QueryParam("role") String role,
+ @QueryParam("graph") String graph) {
+ ensurePdModeEnabled(manager);
+ E.checkArgument(StringUtils.isNotEmpty(user),
+ "The 'user' query param cannot be null or empty");
+ E.checkArgument(StringUtils.isNotEmpty(role),
+ "The 'role' query param cannot be null or empty");
+ LOG.debug("Delete space role: {} {} {}", user, role,
+ name);
+
+ AuthManager authManager = manager.authManager();
+ validGraphSpace(manager, name);
+ String operator = HugeGraphAuthProxy.username();
+ validPermission(hasAdminOrSpaceManagerPerm(manager, name, operator),
+ operator, "default_role.delete");
+ E.checkArgument(authManager.findUser(user) != null ||
+ authManager.findGroup(user) != null,
+ "The user or group is not exist");
+
+ if (!authManager.isAdminManager(operator) &&
+ role.equalsIgnoreCase(HugeDefaultRole.SPACE.toString())) {
+ throw new ForbiddenException("Forbidden to delete role " + role);
+ }
+
+ HugeDefaultRole defaultRole;
+ try {
+ defaultRole = HugeDefaultRole.valueOf(role.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ E.checkArgument(false, "Invalid role value '%s'", role);
+ defaultRole = null; // unreachable, satisfies compiler
+ }
+ boolean hasGraph = defaultRole.equals(HugeDefaultRole.OBSERVER);
+ E.checkArgument(!hasGraph || StringUtils.isNotEmpty(graph),
+ "Must set a graph for observer");
+ if (hasGraph) {
+ validGraph(manager, name, graph);
+ }
+ if (hasGraph) {
+ authManager.deleteDefaultRole(name, user, defaultRole, graph);
+ } else {
+ authManager.deleteDefaultRole(name, user, defaultRole);
+ }
+ }
+
@GET
@Timed
@Path("profile")
@@ -121,7 +290,6 @@ public class GraphSpaceAPI extends API {
AuthManager authManager = manager.authManager();
// FIXME: defaultSpace related interface is not implemented
// String defaultSpace = authManager.getDefaultSpace(user);
- SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
for (String sp : spaces) {
manager.getSpaceStorage(sp);
GraphSpace gs = space(manager, sp);
@@ -135,8 +303,12 @@ public class GraphSpaceAPI extends API {
gsProfile.put("authed", true);
}
- gsProfile.put("create_time", format.format(gs.createTime()));
- gsProfile.put("update_time", format.format(gs.updateTime()));
+ gsProfile.put("create_time",
+ DATE_FORMATTER.format(LocalDateTime.ofInstant(
+ gs.createTime().toInstant(), ZoneId.systemDefault())));
+ gsProfile.put("update_time",
+ DATE_FORMATTER.format(LocalDateTime.ofInstant(
+ gs.updateTime().toInstant(), ZoneId.systemDefault())));
if (!isPrefix(gsProfile, prefix)) {
continue;
}
@@ -175,15 +347,6 @@ public class GraphSpaceAPI extends API {
return manager.serializer().writeGraphSpace(space);
}
- public boolean isPrefix(Map<String, Object> profile, String prefix) {
- if (StringUtils.isEmpty(prefix)) {
- return true;
- }
- // graph name or nickname is not empty
- String name = profile.get("name").toString();
- String nickname = profile.get("nickname").toString();
- return name.startsWith(prefix) || nickname.startsWith(prefix);
- }
@PUT
@Timed
@@ -225,13 +388,13 @@ public class GraphSpaceAPI extends API {
}
String nickname = (String) graphSpaceMap.get("nickname");
- if (!Strings.isEmpty(nickname)) {
+ if (!StringUtils.isEmpty(nickname)) {
GraphManager.checkNickname(nickname);
exist.nickname(nickname);
}
String description = (String) graphSpaceMap.get("description");
- if (!Strings.isEmpty(description)) {
+ if (!StringUtils.isEmpty(description)) {
exist.description(description);
}
@@ -272,31 +435,31 @@ public class GraphSpaceAPI extends API {
String oltpNamespace =
(String) graphSpaceMap.get("oltp_namespace");
if (oltpNamespace != null &&
- !Strings.isEmpty(oltpNamespace)) {
+ !StringUtils.isEmpty(oltpNamespace)) {
exist.oltpNamespace(oltpNamespace);
}
String olapNamespace =
(String) graphSpaceMap.get("olap_namespace");
if (olapNamespace != null &&
- !Strings.isEmpty(olapNamespace)) {
+ !StringUtils.isEmpty(olapNamespace)) {
exist.olapNamespace(olapNamespace);
}
String storageNamespace =
(String) graphSpaceMap.get("storage_namespace");
if (storageNamespace != null &&
- !Strings.isEmpty(storageNamespace)) {
+ !StringUtils.isEmpty(storageNamespace)) {
exist.storageNamespace(storageNamespace);
}
String operatorImagePath = (String) graphSpaceMap
.getOrDefault("operator_image_path", "");
- if (!Strings.isEmpty(operatorImagePath)) {
+ if (!StringUtils.isEmpty(operatorImagePath)) {
exist.operatorImagePath(operatorImagePath);
}
String internalAlgorithmImageUrl = (String) graphSpaceMap
.getOrDefault("internal_algorithm_image_url", "");
- if (!Strings.isEmpty(internalAlgorithmImageUrl)) {
+ if (!StringUtils.isEmpty(internalAlgorithmImageUrl)) {
exist.internalAlgorithmImageUrl(internalAlgorithmImageUrl);
}
@@ -346,6 +509,18 @@ public class GraphSpaceAPI extends API {
authManager.isSpaceMember(graphSpace, user);
}
+ private static void validGraphSpace(GraphManager manager, String graphSpace) {
+ E.checkArgument(manager.graphSpace(graphSpace) != null,
+ "The graph space '%s' does not exist", graphSpace);
+ }
+
+ private static void validGraph(GraphManager manager, String graphSpace,
+ String graph) {
+ E.checkArgument(manager.graph(graphSpace, graph) != null,
+ "Graph '%s/%s' does not exist", graphSpace, graph);
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
private static class JsonGraphSpace implements Checkable {
@JsonProperty("name")
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/SchemaTemplateAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/SchemaTemplateAPI.java
new file mode 100644
index 000000000..b2c151687
--- /dev/null
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/SchemaTemplateAPI.java
@@ -0,0 +1,221 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hugegraph.api.space;
+
+import java.util.Date;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hugegraph.HugeException;
+import org.apache.hugegraph.api.API;
+import org.apache.hugegraph.api.filter.StatusFilter;
+import org.apache.hugegraph.auth.HugeGraphAuthProxy;
+import org.apache.hugegraph.core.GraphManager;
+import org.apache.hugegraph.define.Checkable;
+import org.apache.hugegraph.space.SchemaTemplate;
+import org.apache.hugegraph.util.E;
+import org.apache.hugegraph.util.Log;
+import org.slf4j.Logger;
+
+import com.codahale.metrics.annotation.Timed;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.ImmutableMap;
+
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.security.RolesAllowed;
+import jakarta.inject.Singleton;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.ForbiddenException;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.SecurityContext;
+
+@Path("graphspaces/{graphspace}/schematemplates")
+@Singleton
+@Tag(name = "SchemaTemplateAPI")
+public class SchemaTemplateAPI extends API {
+
+ private static final Logger LOG = Log.logger(SchemaTemplateAPI.class);
+
+ @GET
+ @Timed
+ @Produces(APPLICATION_JSON_WITH_CHARSET)
+ @RolesAllowed({"space_member", "$dynamic"})
+ public Object list(@Context GraphManager manager,
+ @PathParam("graphspace") String graphSpace) {
+ LOG.debug("List all schema templates for graph space {}", graphSpace);
+ ensurePdModeEnabled(manager);
+
+ Set<String> templates = manager.schemaTemplates(graphSpace);
+ return ImmutableMap.of("schema_templates", templates);
+ }
+
+ @GET
+ @Timed
+ @Path("{name}")
+ @Produces(APPLICATION_JSON_WITH_CHARSET)
+ @RolesAllowed({"space_member", "$dynamic"})
+ public Object get(@Context GraphManager manager,
+ @PathParam("graphspace") String graphSpace,
+ @PathParam("name") String name) {
+ LOG.debug("Get schema template by name '{}' for graph space {}",
+ name, graphSpace);
+ ensurePdModeEnabled(manager);
+
+ return manager.serializer().writeSchemaTemplate(
+ schemaTemplate(manager, graphSpace, name));
+ }
+
+ @POST
+ @Timed
+ @StatusFilter.Status(StatusFilter.Status.CREATED)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON_WITH_CHARSET)
+ @RolesAllowed({"space_member", "$dynamic"})
+ public String create(@Context GraphManager manager,
+ @PathParam("graphspace") String graphSpace,
+ JsonSchemaTemplate jsonSchemaTemplate) {
+ LOG.debug("Create schema template {} for graph space: '{}'",
+ jsonSchemaTemplate, graphSpace);
+ ensurePdModeEnabled(manager);
+ E.checkArgumentNotNull(jsonSchemaTemplate,
+ "Request body cannot be null or empty");
+ jsonSchemaTemplate.checkCreate(false);
+
+ E.checkArgument(manager.graphSpace(graphSpace) != null,
+ "The graph space '%s' is not exist", graphSpace);
+
+ SchemaTemplate template = jsonSchemaTemplate.toSchemaTemplate();
+ template.create(new Date());
+ template.creator(HugeGraphAuthProxy.username());
+ manager.createSchemaTemplate(graphSpace, template);
+ return manager.serializer().writeSchemaTemplate(template);
+ }
+
+ @DELETE
+ @Timed
+ @Path("{name}")
+ @Produces(APPLICATION_JSON_WITH_CHARSET)
+ @RolesAllowed({"space_member", "$dynamic"})
+ public void delete(@Context GraphManager manager,
+ @PathParam("graphspace") String graphSpace,
+ @PathParam("name") String name,
+ @Context SecurityContext sc) {
+ LOG.debug("Remove schema template by name '{}' for graph space '{}'",
+ name, graphSpace);
+ ensurePdModeEnabled(manager);
+ E.checkArgument(manager.graphSpace(graphSpace) != null,
+ "The graph space '%s' is not exist", graphSpace);
+
+ SchemaTemplate st = schemaTemplate(manager, graphSpace, name);
+ E.checkArgument(st != null,
+ "Schema template '%s' does not exist", name);
+
+ String username = HugeGraphAuthProxy.username();
+ boolean isSpace = manager.authManager()
+ .isSpaceManager(graphSpace, username);
+ if (Objects.equals(st.creator(), username) || isSpace) {
+ manager.dropSchemaTemplate(graphSpace, name);
+ } else {
+ throw new ForbiddenException("No permission to delete schema template");
+ }
+ }
+
+ @PUT
+ @Timed
+ @Path("{name}")
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON_WITH_CHARSET)
+ @RolesAllowed({"space_member", "$dynamic"})
+ public String update(@Context GraphManager manager,
+ @PathParam("graphspace") String graphSpace,
+ @PathParam("name") String name,
+ @Context SecurityContext sc,
+ JsonSchemaTemplate jsonSchemaTemplate) {
+ ensurePdModeEnabled(manager);
+ E.checkArgumentNotNull(jsonSchemaTemplate,
+ "Request body cannot be null or empty");
+ jsonSchemaTemplate.checkUpdate();
+
+ SchemaTemplate old = schemaTemplate(manager, graphSpace, name);
+ if (null == old) {
+ throw new HugeException("Schema template '%s' does not exist", name);
+ }
+
+ String username = HugeGraphAuthProxy.username();
+ boolean isSpace = manager.authManager()
+ .isSpaceManager(graphSpace, username);
+ if (Objects.equals(old.creator(), username) || isSpace) {
+ SchemaTemplate template = jsonSchemaTemplate.build(old);
+ template.creator(old.creator());
+ template.create(old.create());
+ template.refreshUpdateTime();
+
+ manager.updateSchemaTemplate(graphSpace, template);
+ return manager.serializer().writeSchemaTemplate(template);
+ }
+ throw new ForbiddenException("No permission to update schema template");
+
+ }
+
+ private static class JsonSchemaTemplate implements Checkable {
+
+ @JsonProperty("name")
+ public String name;
+ @JsonProperty("schema")
+ public String schema;
+
+ @Override
+ public void checkCreate(boolean isBatch) {
+ E.checkArgument(this.name != null && !this.name.isEmpty(),
+ "The name of schema template can't be null or " +
+ "empty");
+
+ E.checkArgument(this.schema != null && !this.schema.isEmpty(),
+ "The schema can't be null or empty");
+ }
+
+ @Override
+ public void checkUpdate() {
+ E.checkArgument(this.schema != null && !this.schema.isEmpty(),
+ "The schema can't be null or empty");
+ }
+
+ public SchemaTemplate toSchemaTemplate() {
+ return new SchemaTemplate(this.name, this.schema);
+ }
+
+ public SchemaTemplate build(SchemaTemplate old) {
+ E.checkArgument(StringUtils.isEmpty(this.name) || old.name().equals(this.name),
+ "The name of template can't be updated");
+ return new SchemaTemplate(old.name(), this.schema);
+ }
+
+ public String toString() {
+ return String.format("JsonSchemaTemplate{name=%s, schema=%s}",
+ this.name, this.schema);
+ }
+ }
+}
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java
index 5b117faae..8dabfe061 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java
@@ -24,6 +24,7 @@ import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -2007,6 +2008,70 @@ public final class HugeGraphAuthProxy implements HugeGraph {
return this.authManager.findGroup(name);
}
+ @Override
+ public void setDefaultGraph(String graphSpace, String graph, String user) {
+ this.authManager.setDefaultGraph(graphSpace, graph, user);
+ }
+
+ @Override
+ public void unsetDefaultGraph(String graphSpace, String graph, String user) {
+ this.authManager.unsetDefaultGraph(graphSpace, graph, user);
+ }
+
+ @Override
+ public Map<String, Date> getDefaultGraph(String graphSpace, String user) {
+ return this.authManager.getDefaultGraph(graphSpace, user);
+ }
+
+ @Override
+ public Id createDefaultRole(String graphSpace, String owner, HugeDefaultRole role,
+ String graph) {
+ try {
+ return this.authManager.createDefaultRole(graphSpace, owner, role, graph);
+ } finally {
+ this.invalidRoleCache();
+ }
+ }
+
+ @Override
+ public Id createSpaceDefaultRole(String graphSpace, String owner, HugeDefaultRole role) {
+ try {
+ return this.authManager.createSpaceDefaultRole(graphSpace, owner, role);
+ } finally {
+ this.invalidRoleCache();
+ }
+ }
+
+ @Override
+ public boolean isDefaultRole(String graphSpace, String owner, HugeDefaultRole role) {
+ return this.authManager.isDefaultRole(graphSpace, owner, role);
+ }
+
+ @Override
+ public boolean isDefaultRole(String graphSpace, String graph, String owner,
+ HugeDefaultRole role) {
+ return this.authManager.isDefaultRole(graphSpace, graph, owner, role);
+ }
+
+ @Override
+ public void deleteDefaultRole(String graphSpace, String owner, HugeDefaultRole role) {
+ try {
+ this.authManager.deleteDefaultRole(graphSpace, owner, role);
+ } finally {
+ this.invalidRoleCache();
+ }
+ }
+
+ @Override
+ public void deleteDefaultRole(String graphSpace, String owner, HugeDefaultRole role,
+ String graph) {
+ try {
+ this.authManager.deleteDefaultRole(graphSpace, owner, role, graph);
+ } finally {
+ this.invalidRoleCache();
+ }
+ }
+
@Override
public String loginUser(String username, String password) {
return this.loginUser(username, password, -1L);
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java
index 1aed0434d..7d706de8f 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java
@@ -659,7 +659,7 @@ public class ServerOptions extends OptionHolder {
"memory_monitor.period",
"The period in ms of JVM memory usage monitoring, in each period we will " +
"detect the jvm memory usage and take corresponding actions.",
- nonNegativeInt(),
+ positiveInt(),
2000
);
public static ConfigOption<String> K8S_INTERNAL_ALGORITHM_IMAGE_URL =
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java
index ff0435964..362a2f9a3 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java
@@ -2110,6 +2110,19 @@ public final class GraphManager {
for (String key : this.metaManager.graphConfigs(graphSpace).keySet()) {
graphs.add(key.split(DELIMITER)[1]);
}
+ if (DEFAULT_GRAPH_SPACE_SERVICE_NAME.equals(graphSpace)) {
+ String prefix = spaceGraphName(graphSpace, "");
+ for (String key : this.graphs.keySet()) {
+ if (!key.startsWith(prefix)) {
+ continue;
+ }
+ String graph = key.substring(prefix.length());
+ if (SYS_GRAPH.equals(graph)) {
+ continue;
+ }
+ graphs.add(graph);
+ }
+ }
return graphs;
}
@@ -2119,7 +2132,14 @@ public final class GraphManager {
}
GraphSpace space = this.graphSpaces.get(name);
if (space == null) {
+ // Cache miss: try to load from etcd and populate the local cache
+ // so that subsequent calls (e.g. validGraphSpace checks) don't fail
+ // due to a race between the graphspace-create event and the listener
+ // updating this.graphSpaces.
space = this.metaManager.graphSpace(name);
+ if (space != null) {
+ this.graphSpaces.putIfAbsent(name, space);
+ }
}
return space;
}
@@ -2147,9 +2167,27 @@ public final class GraphManager {
if (StringUtils.isEmpty(nickname)) {
return false;
}
+ if (!isPDEnabled()) {
+ // In non-PD mode, metaManager is not initialized.
+ // Check nickname against in-memory graphs (same pattern as graphs()).
+ for (Map.Entry<String, Graph> entry : this.graphs.entrySet()) {
+ String key = entry.getKey();
+ String[] parts = key.split(DELIMITER, 2);
+ if (parts.length == 2 && parts[0].equals(graphSpace) &&
+ entry.getValue() instanceof HugeGraph) {
+ HugeGraph hg = (HugeGraph) entry.getValue();
+ if (nickname.equals(hg.nickname())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ // PD mode: check via metaManager
for (Map<String, Object> graphConfig :
this.metaManager.graphConfigs(graphSpace).values()) {
- if (nickname.equals(graphConfig.get("nickname").toString())) {
+ Object nick = graphConfig.get("nickname");
+ if (nick != null && nickname.equals(nick.toString())) {
return true;
}
}
@@ -2185,6 +2223,50 @@ public final class GraphManager {
return this.metaManager.getGraphConfig(graphSpace, graphName);
}
+ /**
+ * Update the nickname of a graph.
+ * In non-PD mode (standalone RocksDB), only the in-memory instance is updated
+ * since local config files cannot be hot-reloaded.
+ * In PD mode, the change is also persisted to the meta storage so it
+ * survives restarts.
+ */
+ public void updateGraphNickname(String graphSpace, String graphName,
+ String nickname) {
+ // Always update the in-memory graph instance first
+ HugeGraph g = this.graph(graphSpace, graphName);
+ // Capture the old nickname so we can restore it on failure
+ String oldNickname = g != null ? g.nickname() : null;
+ if (g != null) {
+ g.nickname(nickname);
+ }
+ if (!isPDEnabled()) {
+ // Non-PD mode: in-memory only, acceptable for standalone RocksDB
+ return;
+ }
+ Map<String, Object> configs;
+ try {
+ configs = this.metaManager.getGraphConfig(graphSpace, graphName);
+ if (configs == null) {
+ return;
+ }
+ configs.put("nickname", nickname);
+ this.metaManager.updateGraphConfig(graphSpace, graphName, configs);
+ } catch (Exception e) {
+ // Roll back only if the PD metadata write did not succeed.
+ if (g != null) {
+ g.nickname(oldNickname);
+ }
+ throw new HugeException("Failed to persist nickname for graph " +
+ "'%s/%s'", e, graphSpace, graphName);
+ }
+ try {
+ this.metaManager.notifyGraphUpdate(graphSpace, graphName);
+ } catch (Exception e) {
+ LOG.warn("Failed to notify graph nickname update for graph '{}/{}'",
+ graphSpace, graphName, e);
+ }
+ }
+
public String pdPeers() {
return this.pdPeers;
}
@@ -2193,6 +2275,43 @@ public final class GraphManager {
return this.cluster;
}
+ public Set<String> schemaTemplates(String graphSpace) {
+ if (!isPDEnabled()) {
+ throw new HugeException("Schema templates are not supported in " +
+ "standalone (non-PD) mode");
+ }
+ return this.metaManager.schemaTemplates(graphSpace);
+ }
+
+ public void createSchemaTemplate(String graphSpace, SchemaTemplate template) {
+ if (!isPDEnabled()) {
+ throw new HugeException("Schema templates are not supported in " +
+ "standalone (non-PD) mode");
+ }
+ checkSchemaTemplateName(template.name());
+ this.metaManager.addSchemaTemplate(graphSpace, template);
+ }
+
+ public void dropSchemaTemplate(String graphSpace, String name) {
+ if (!isPDEnabled()) {
+ throw new HugeException("Schema templates are not supported in " +
+ "standalone (non-PD) mode");
+ }
+ this.metaManager.removeSchemaTemplate(graphSpace, name);
+ }
+
+ public void updateSchemaTemplate(String graphSpace, SchemaTemplate template) {
+ if (!isPDEnabled()) {
+ throw new HugeException("Schema templates are not supported in " +
+ "standalone (non-PD) mode");
+ }
+ this.metaManager.updateSchemaTemplate(graphSpace, template);
+ }
+
+ private static void checkSchemaTemplateName(String name) {
+ checkName(name, "schema template");
+ }
+
private enum PdRegisterType {
NODE_PORT,
DDS
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java
index af0027c02..8b36dd9be 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java
@@ -17,7 +17,9 @@
package org.apache.hugegraph.auth;
+import java.util.Date;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import javax.security.sasl.AuthenticationException;
@@ -164,4 +166,28 @@ public interface AuthManager {
boolean isAdminManager(String user);
HugeGroup findGroup(String name);
+
+ void setDefaultGraph(String graphSpace, String graph, String user);
+
+ void unsetDefaultGraph(String graphSpace, String graph, String user);
+
+ Map<String, Date> getDefaultGraph(String graphSpace, String user);
+
+ Id createDefaultRole(String graphSpace, String owner,
+ HugeDefaultRole role, String graph);
+
+ Id createSpaceDefaultRole(String graphSpace, String owner,
+ HugeDefaultRole role);
+
+ boolean isDefaultRole(String graphSpace, String owner,
+ HugeDefaultRole role);
+
+ boolean isDefaultRole(String graphSpace, String graph, String owner,
+ HugeDefaultRole role);
+
+ void deleteDefaultRole(String graphSpace, String owner,
+ HugeDefaultRole role);
+
+ void deleteDefaultRole(String graphSpace, String owner,
+ HugeDefaultRole role, String graph);
}
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeUser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeUser.java
index 064e32656..8a90de0b1 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeUser.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeUser.java
@@ -146,6 +146,9 @@ public class HugeUser extends Entity {
case P.NAME:
this.name = (String) value;
break;
+ case P.NICKNAME:
+ this.nickname = (String) value;
+ break;
case P.PASSWORD:
this.password = (String) value;
break;
@@ -172,7 +175,7 @@ public class HugeUser extends Entity {
E.checkState(this.name != null, "User name can't be null");
E.checkState(this.password != null, "User password can't be null");
- List<Object> list = new ArrayList<>(18);
+ List<Object> list = new ArrayList<>(20);
list.add(T.label);
list.add(P.USER);
@@ -183,6 +186,11 @@ public class HugeUser extends Entity {
list.add(P.PASSWORD);
list.add(this.password);
+ if (this.nickname != null) {
+ list.add(P.NICKNAME);
+ list.add(this.nickname);
+ }
+
if (this.phone != null) {
list.add(P.PHONE);
list.add(this.phone);
@@ -216,6 +224,10 @@ public class HugeUser extends Entity {
map.put(Hidden.unHide(P.NAME), this.name);
map.put(Hidden.unHide(P.PASSWORD), this.password);
+ if (this.nickname != null) {
+ map.put(Hidden.unHide(P.NICKNAME), this.nickname);
+ }
+
if (this.phone != null) {
map.put(Hidden.unHide(P.PHONE), this.phone);
}
@@ -253,6 +265,7 @@ public class HugeUser extends Entity {
public static final String LABEL = T.label.getAccessor();
public static final String NAME = "~user_name";
+ public static final String NICKNAME = "~user_nickname";
public static final String PASSWORD = "~user_password";
public static final String PHONE = "~user_phone";
public static final String EMAIL = "~user_email";
@@ -277,6 +290,8 @@ public class HugeUser extends Entity {
@Override
public void initSchemaIfNeeded() {
if (this.existVertexLabel(this.label)) {
+ // Schema already exists: do incremental upgrade for new properties
+ upgradeSchemaIfNeeded();
return;
}
@@ -287,16 +302,29 @@ public class HugeUser extends Entity {
.properties(properties)
.usePrimaryKeyId()
.primaryKeys(P.NAME)
- .nullableKeys(P.PHONE, P.EMAIL, P.AVATAR, P.DESCRIPTION)
+ .nullableKeys(P.NICKNAME, P.PHONE, P.EMAIL, P.AVATAR, P.DESCRIPTION)
.enableLabelIndex(true)
.build();
this.graph.schemaTransaction().addVertexLabel(label);
}
+ private void upgradeSchemaIfNeeded() {
+ // Add user_nickname property key if missing (new in this version)
+ if (!this.graph.graph().existsPropertyKey(P.NICKNAME)) {
+ createPropertyKey(P.NICKNAME);
+ VertexLabel vl = this.graph.graph().vertexLabel(this.label);
+ Id nickname = this.graph.graph().propertyKey(P.NICKNAME).id();
+ vl.property(nickname);
+ vl.nullableKey(nickname);
+ this.graph.schemaTransaction().updateVertexLabel(vl);
+ }
+ }
+
private String[] initProperties() {
List<String> props = new ArrayList<>();
props.add(createPropertyKey(P.NAME));
+ props.add(createPropertyKey(P.NICKNAME));
props.add(createPropertyKey(P.PASSWORD));
props.add(createPropertyKey(P.PHONE));
props.add(createPropertyKey(P.EMAIL));
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java
index a3224811f..cac9ba53d 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java
@@ -19,6 +19,7 @@ package org.apache.hugegraph.auth;
import java.time.Duration;
import java.util.ArrayList;
+import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -810,9 +811,205 @@ public class StandardAuthManager implements AuthManager {
@Override
public HugeGroup findGroup(String name) {
+ for (HugeGroup group : this.groups.list(-1L)) {
+ if (name.equals(group.name())) {
+ return group;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * <h3>Design Note: Default graph / default role persistence (workaround)</h3>
+ *
+ * <p>We reuse the existing {@code HugeGroup} / {@code HugeBelong} auth entities as a storage
+ * mechanism to avoid introducing new schema or storage APIs.
+ *
+ * <p><b>How it works:</b>
+ * <ul>
+ * <li>A "marker group" with a special prefixed name (e.g. {@code "~default_graph:gs:g"})
+ * is created in the system graph to represent a (graphSpace, graph) binding.</li>
+ * <li>A {@code HugeBelong} edge from the user vertex to this marker group records which
+ * user has set which graph as their default.</li>
+ * <li>The Belong ID is deterministic: {@code "{userId}->ug->{groupId}"}, which lets us
+ * check existence without a full scan.</li>
+ * </ul>
+ *
+ * <p><b>Known limitations:</b>
+ * <ol>
+ * <li>These marker groups appear in {@code listGroups()} results and may confuse callers.</li>
+ * <li>This mechanism only works in <b>PD mode</b> (system graph backed by HStore).
+ * In non-PD (standalone RocksDB) mode, the API layer degrades gracefully
+ * by returning empty results (see {@code GraphsAPI.setDefault / getDefault}).</li>
+ * <li>The belong ID format depends on the internal convention {@code "{u}->ug->{g}"}.</li>
+ * </ol>
+ *
+ * @todo Consider introducing a dedicated lightweight KV store or a separate
+ * vertex label for user-preference data to avoid polluting the auth graph.
+ */
+ private static final String DEFAULT_GRAPH_MARKER = "~default_graph";
+
+ private String defaultGraphGroupName(String graphSpace, String graph) {
+ return DEFAULT_GRAPH_MARKER + ":" + graphSpace + ":" + graph;
+ }
+
+ @Override
+ public void setDefaultGraph(String graphSpace, String graph, String user) {
+ // Use a special-named HugeGroup as a marker for the default graph,
+ // then create a HugeBelong (user -> marker-group) to persist the binding.
+ String markerName = defaultGraphGroupName(graphSpace, graph);
+ Id groupId = ensureMarkerGroup(markerName, user);
+ createBelongBinding(user, groupId);
+ }
+
+ @Override
+ public void unsetDefaultGraph(String graphSpace, String graph, String user) {
+ String markerName = defaultGraphGroupName(graphSpace, graph);
+ Id groupId = IdGenerator.of(markerName);
+ removeBelongBinding(user, groupId);
+ }
+
+ @Override
+ public Map<String, Date> getDefaultGraph(String graphSpace, String user) {
+ Id userId = IdGenerator.of(user);
+ List<HugeBelong> belongs = this.belong.list(userId, Directions.OUT,
+ HugeBelong.P.BELONG, -1);
+ String prefix = DEFAULT_GRAPH_MARKER + ":" + graphSpace + ":";
+ Map<String, Date> result = new LinkedHashMap<>();
+ for (HugeBelong b : belongs) {
+ String targetName = b.target().asString();
+ if (targetName.startsWith(prefix)) {
+ String graphName = targetName.substring(prefix.length());
+ result.put(graphName, b.update());
+ }
+ }
+ return result;
+ }
+
+ private static final String DEFAULT_ROLE_MARKER = "~default_role";
+
+ /**
+ * Build marker group name for a space-level role.
+ * Format: ~default_role:<graphSpace>:<role>
+ */
+ private String defaultRoleGroupName(String graphSpace, String role) {
+ return DEFAULT_ROLE_MARKER + ":" + graphSpace + ":" + role;
+ }
+
+ /**
+ * Build marker group name for a graph-level role (e.g. OBSERVER).
+ * Format: ~default_role:<graphSpace>:<role>:<graph>
+ */
+ private String defaultRoleGroupName(String graphSpace, String role,
+ String graph) {
+ return DEFAULT_ROLE_MARKER + ":" + graphSpace + ":" + role +
+ ":" + graph;
+ }
+
+ private Id ensureMarkerGroup(String markerName, String creator) {
+ Id groupId = IdGenerator.of(markerName);
+ HugeGroup markerGroup = this.findGroup(markerName);
+ if (markerGroup != null) {
+ return groupId;
+ }
+ markerGroup = new HugeGroup(markerName);
+ markerGroup.creator(creator);
+ this.groups.add(markerGroup);
+ return groupId;
+ }
+
+ private HugeBelong findBelongBinding(String owner, Id groupId) {
+ Id userId = IdGenerator.of(owner);
+ List<HugeBelong> belongs = this.belong.list(userId, Directions.OUT,
+ HugeBelong.P.BELONG, -1);
+ for (HugeBelong b : belongs) {
+ if (groupId.equals(b.target())) {
+ return b;
+ }
+ }
return null;
}
+ private Id createBelongBinding(String owner, Id groupId) {
+ HugeBelong existing = findBelongBinding(owner, groupId);
+ if (existing != null) {
+ return existing.id();
+ }
+ Id userId = IdGenerator.of(owner);
+ HugeBelong belong = new HugeBelong(userId, groupId);
+ belong.creator(owner);
+ Id id = this.belong.add(belong);
+ this.invalidateUserCache();
+ return id;
+ }
+
+ private void removeBelongBinding(String owner, Id groupId) {
+ HugeBelong existing = findBelongBinding(owner, groupId);
+ if (existing != null) {
+ this.belong.delete(existing.id());
+ this.invalidateUserCache();
+ }
+ }
+
+ private boolean existsBelongBinding(String owner, Id groupId) {
+ return findBelongBinding(owner, groupId) != null;
+ }
+
+ @Override
+ public Id createDefaultRole(String graphSpace, String owner,
+ HugeDefaultRole role, String graph) {
+ LOG.debug("Create default role: {} {} {} {}", owner, role,
+ graphSpace, graph);
+ String markerName = defaultRoleGroupName(graphSpace,
+ role.toString(), graph);
+ Id groupId = ensureMarkerGroup(markerName, owner);
+ return createBelongBinding(owner, groupId);
+ }
+
+ @Override
+ public Id createSpaceDefaultRole(String graphSpace, String owner,
+ HugeDefaultRole role) {
+ LOG.debug("Create space default role: {} {} {}", owner, role,
+ graphSpace);
+ String markerName = defaultRoleGroupName(graphSpace, role.toString());
+ Id groupId = ensureMarkerGroup(markerName, owner);
+ return createBelongBinding(owner, groupId);
+ }
+
+ @Override
+ public boolean isDefaultRole(String graphSpace, String owner,
+ HugeDefaultRole role) {
+ String markerName = defaultRoleGroupName(graphSpace, role.toString());
+ Id groupId = IdGenerator.of(markerName);
+ return existsBelongBinding(owner, groupId);
+ }
+
+ @Override
+ public boolean isDefaultRole(String graphSpace, String graph,
+ String owner, HugeDefaultRole role) {
+ String markerName = defaultRoleGroupName(graphSpace,
+ role.toString(), graph);
+ Id groupId = IdGenerator.of(markerName);
+ return existsBelongBinding(owner, groupId);
+ }
+
+ @Override
+ public void deleteDefaultRole(String graphSpace, String owner,
+ HugeDefaultRole role) {
+ String markerName = defaultRoleGroupName(graphSpace, role.toString());
+ Id groupId = IdGenerator.of(markerName);
+ removeBelongBinding(owner, groupId);
+ }
+
+ @Override
+ public void deleteDefaultRole(String graphSpace, String owner,
+ HugeDefaultRole role, String graph) {
+ String markerName = defaultRoleGroupName(graphSpace,
+ role.toString(), graph);
+ Id groupId = IdGenerator.of(markerName);
+ removeBelongBinding(owner, groupId);
+ }
+
public <R> R commit(Callable<R> callable) {
this.groups.autoCommit(false);
this.access.autoCommit(false);
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java
index d2df45626..30cb05c1f 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java
@@ -21,6 +21,7 @@ import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
@@ -110,9 +111,9 @@ public class StandardAuthManagerV2 implements AuthManager {
* Update creator from current context (from TaskManager ThreadLocal or direct call)
*/
private AuthElement updateCreator(AuthElement elem) {
- String username = currentUsername();
- if (username != null && elem.creator() == null) {
- elem.creator(username);
+ if (elem.creator() == null) {
+ String username = currentUsername();
+ elem.creator(username != null ? username : "system");
}
return elem;
}
@@ -200,6 +201,7 @@ public class StandardAuthManagerV2 implements AuthManager {
try {
user.create(user.update());
+ updateCreator(user);
this.metaManager.createUser(user);
// Update cache after successful creation
@@ -374,6 +376,7 @@ public class StandardAuthManagerV2 implements AuthManager {
public Id createGroup(HugeGroup group) {
try {
group.create(group.update());
+ updateCreator(group);
this.metaManager.createGroup(group);
Id result = IdGenerator.of(group.name());
group.id(result);
@@ -461,6 +464,7 @@ public class StandardAuthManagerV2 implements AuthManager {
public Id createTarget(HugeTarget target) {
try {
target.create(target.update());
+ updateCreator(target);
Id result = this.metaManager.createTarget(graphSpace, target);
this.invalidateUserCache();
return result;
@@ -548,6 +552,7 @@ public class StandardAuthManagerV2 implements AuthManager {
public Id createBelong(HugeBelong belong) {
try {
belong.create(belong.update());
+ updateCreator(belong);
this.invalidateUserCache();
return this.metaManager.createBelong(graphSpace, belong);
} catch (IOException e) {
@@ -662,6 +667,7 @@ public class StandardAuthManagerV2 implements AuthManager {
public Id createAccess(HugeAccess access) {
try {
access.create(access.update());
+ updateCreator(access);
Id result = this.metaManager.createAccess(graphSpace, access);
this.invalidateUserCache();
return result;
@@ -1510,6 +1516,218 @@ public class StandardAuthManagerV2 implements AuthManager {
return result;
}
+ @Override
+ public void setDefaultGraph(String graphSpace, String graph, String user) {
+ try {
+ String role = graph + DEFAULT_SETTER_ROLE_KEY;
+ String belongId = this.metaManager.belongId(user, role);
+ Id id = IdGenerator.of(belongId);
+ // Idempotent: if binding already exists, treat as success
+ if (this.metaManager.existBelong(graphSpace, id)) {
+ return;
+ }
+ HugeBelong belong = new HugeBelong(graphSpace,
+ IdGenerator.of(user),
+ IdGenerator.of(graph +
+ DEFAULT_SETTER_ROLE_KEY));
+ this.tryInitDefaultGraph(graphSpace, graph);
+ this.updateCreator(belong);
+ belong.create(belong.update());
+ this.metaManager.createBelong(graphSpace, belong);
+ this.invalidateUserCache();
+ } catch (Exception e) {
+ throw new HugeException("Exception occurs when " +
+ "set default graph", e);
+ }
+ }
+
+ @Override
+ public void unsetDefaultGraph(String graphSpace, String graph, String user) {
+ String role = graph + DEFAULT_SETTER_ROLE_KEY;
+ String belongId = this.metaManager.belongId(user, role);
+ Id id = IdGenerator.of(belongId);
+ // Check if belong exists before attempting to delete to make this operation idempotent
+ if (!this.metaManager.existBelong(graphSpace, id)) {
+ // Already unset, treat as success (idempotent behavior)
+ return;
+ }
+ try {
+ this.metaManager.deleteBelong(graphSpace, id);
+ this.invalidateUserCache();
+ } catch (Exception e) {
+ throw new HugeException("Exception occurs when unset default " +
+ "graph", e);
+ }
+ }
+
+ @Override
+ public Map<String, Date> getDefaultGraph(String graphSpace, String user) {
+ List<HugeBelong> belongs = this.listBelongBySource(graphSpace,
+ IdGenerator.of(user),
+ HugeBelong.UR, -1);
+ Map<String, Date> map = new HashMap<>();
+ for (HugeBelong belong : belongs) {
+ String role = belong.target().asString();
+ if (role.endsWith(DEFAULT_SETTER_ROLE_KEY) &&
+ role.length() != DEFAULT_SETTER_ROLE_KEY.length()) {
+ map.put(role.substring(0, role.lastIndexOf(
+ DEFAULT_SETTER_ROLE_KEY)), belong.update());
+ }
+ }
+ return map;
+ }
+
+ @Override
+ public Id createDefaultRole(String graphSpace, String owner,
+ HugeDefaultRole role, String graph) {
+ String roleName = (role.isGraphRole()) ?
+ getGraphDefaultRole(graph, role.toString()) : role.toString();
+ try {
+ HugeBelong belong;
+ String link;
+ HugeUser user = this.findUser(owner);
+ HugeGroup group = this.findGroup(owner);
+ if (group != null) {
+ belong = new HugeBelong(graphSpace, null,
+ IdGenerator.of(owner),
+ IdGenerator.of(roleName),
+ HugeBelong.GR);
+ link = HugeBelong.GR;
+ } else if (user != null) {
+ belong = new HugeBelong(graphSpace, IdGenerator.of(owner),
+ null, IdGenerator.of(roleName),
+ HugeBelong.UR);
+ link = HugeBelong.UR;
+ } else {
+ throw new HugeException("The user or group is not exist");
+ }
+
+ // Idempotent: if binding already exists, treat as success
+ String belongId = this.metaManager.belongId(owner, roleName, link);
+ if (this.metaManager.existBelong(graphSpace, IdGenerator.of(belongId))) {
+ return IdGenerator.of(belongId);
+ }
+
+ this.tryInitDefaultRole(graphSpace, roleName, graph);
+ this.updateCreator(belong);
+ belong.create(belong.update());
+ Id result = this.metaManager.createBelong(graphSpace, belong);
+ this.invalidateUserCache();
+ return result;
+ } catch (Exception e) {
+ throw new HugeException("Exception occurs when " +
+ "create " + role + ".", e);
+ }
+ }
+
+ @Override
+ public Id createSpaceDefaultRole(String graphSpace, String owner,
+ HugeDefaultRole role) {
+ return createDefaultRole(graphSpace, owner, role, ALL_GRAPHS);
+ }
+
+ @Override
+ public boolean isDefaultRole(String graphSpace, String owner,
+ HugeDefaultRole role) {
+ return isDefaultRole(graphSpace, owner, role.toString());
+ }
+
+ @Override
+ public boolean isDefaultRole(String graphSpace, String graph,
+ String owner, HugeDefaultRole role) {
+ String roleName = getGraphDefaultRole(graph, role.toString());
+ return isDefaultRole(graphSpace, owner, roleName);
+ }
+
+ @Override
+ public void deleteDefaultRole(String graphSpace, String owner,
+ HugeDefaultRole role) {
+ deleteDefaultRoleByName(graphSpace, owner, role.toString());
+ }
+
+ @Override
+ public void deleteDefaultRole(String graphSpace, String owner,
+ HugeDefaultRole role, String graph) {
+ String roleName = getGraphDefaultRole(graph, role.toString());
+ deleteDefaultRoleByName(graphSpace, owner, roleName);
+ }
+
+ private boolean isDefaultRole(String graphSpace, String owner,
+ String role) {
+ try {
+ String belongId;
+ if (this.findGroup(owner) != null) {
+ belongId = this.metaManager.belongId(owner, role,
+ HugeBelong.GR);
+ return this.metaManager.existBelong(graphSpace,
+ IdGenerator.of(belongId));
+ }
+
+ List<HugeGroup> groups = this.listGroupsByUser(owner, -1);
+ for (HugeGroup group : groups) {
+ String belongIdG = this.metaManager.belongId(group.name(),
+ role,
+ HugeBelong.GR);
+ if (this.metaManager.existBelong(graphSpace,
+ IdGenerator.of(belongIdG))) {
+ return true;
+ }
+ }
+
+ belongId = this.metaManager.belongId(owner, role);
+ return this.metaManager.existBelong(graphSpace,
+ IdGenerator.of(belongId));
+ } catch (Exception e) {
+ throw new HugeException("Exception occurs when check if is " +
+ role + ".", e);
+ }
+ }
+
+ private void deleteDefaultRoleByName(String graphSpace, String owner,
+ String role) {
+ try {
+ String belongId;
+ if (this.findGroup(owner) != null) {
+ belongId = this.metaManager.belongId(owner, role,
+ HugeBelong.GR);
+ } else {
+ belongId = this.metaManager.belongId(owner, role,
+ HugeBelong.UR);
+ }
+ Id id = IdGenerator.of(belongId);
+ if (!this.metaManager.existBelong(graphSpace, id)) {
+ return;
+ }
+ this.metaManager.deleteBelong(graphSpace, id);
+ this.invalidateUserCache();
+ } catch (Exception e) {
+ throw new HugeException("Exception occurs when " +
+ "delete " + role + ".", e);
+ }
+ }
+
+ private void tryInitDefaultGraph(String graphSpace, String graph) {
+ try {
+ HugeRole role = this.metaManager.findRole(
+ graphSpace,
+ IdGenerator.of(graph + DEFAULT_SETTER_ROLE_KEY));
+ if (role != null) {
+ return;
+ }
+ role = new HugeRole(graph + DEFAULT_SETTER_ROLE_KEY, graphSpace);
+ this.updateCreator(role);
+ role.create(role.update());
+ this.metaManager.createRole(graphSpace, role);
+ } catch (Exception e) {
+ throw new HugeException("Exception occurs when " +
+ "init default graph role", e);
+ }
+ }
+
+ public String getGraphDefaultRole(String graph, String role) {
+ return graph + "_" + role;
+ }
+
private void tryInitDefaultRole(String graphSpace,
String roleName,
String graph) {
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedSchemaTransactionV2.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedSchemaTransactionV2.java
index b6c47e705..99a393f6b 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedSchemaTransactionV2.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedSchemaTransactionV2.java
@@ -47,8 +47,14 @@ import com.google.common.collect.ImmutableSet;
public class CachedSchemaTransactionV2 extends SchemaTransactionV2 {
- private static final String ID_CACHE_PREFIX = "schema-id";
- private static final String NAME_CACHE_PREFIX = "schema-name";
+ /*
+ * V1 and V2 transactions can coexist in the same JVM for graphs with the
+ * same name but different backends. Keep their cache entries isolated:
+ * besides holding different schema data, their attachments use different
+ * SchemaCaches classes and therefore can't be shared safely.
+ */
+ private static final String ID_CACHE_PREFIX = "schema-v2-id";
+ private static final String NAME_CACHE_PREFIX = "schema-v2-name";
// MetaDriver doesn't expose unlisten, register the meta listener once.
// Lifecycle: this JVM-global flag is intentionally never reset by
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/ConfigUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/ConfigUtil.java
index 8df1b3064..54b539237 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/ConfigUtil.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/ConfigUtil.java
@@ -23,6 +23,8 @@ import java.io.Reader;
import java.io.StringReader;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -188,4 +190,41 @@ public final class ConfigUtil {
"Graph name can only contain letters, numbers, hyphens and underscores: %s",
graphName);
}
+
+ /**
+ * Serialize a HugeConfig to a JSON string for frontend consumption.
+ * <p>
+ * Sensitive configuration keys (containing keywords such as "password",
+ * "secret", "token", "credential", "auth.key") are excluded to prevent
+ * accidental exposure of credentials through the API response.
+ */
+ public static String writeConfigToString(HugeConfig config) {
+ Map<String, Object> configMap = new HashMap<>();
+ Iterator<String> iterator = config.getKeys();
+ while (iterator.hasNext()) {
+ String key = iterator.next();
+ if (isSensitiveKey(key)) {
+ continue;
+ }
+ configMap.put(key, config.getProperty(key));
+ }
+ return JsonUtil.toJson(configMap);
+ }
+
+ /**
+ * Returns true if the config key looks like it may contain sensitive data.
+ * Uses a case-insensitive keyword blacklist.
+ */
+ private static boolean isSensitiveKey(String key) {
+ if (key == null) {
+ return false;
+ }
+ String lower = key.toLowerCase();
+ return lower.contains("password") ||
+ lower.contains("secret") ||
+ lower.contains("token") ||
+ lower.contains("credential") ||
+ lower.contains("private_key") ||
+ lower.contains("auth.key");
+ }
}
diff --git a/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/HugeGraphServer.java b/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/HugeGraphServer.java
index 64c4c98cf..214a09c3e 100644
--- a/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/HugeGraphServer.java
+++ b/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/HugeGraphServer.java
@@ -54,42 +54,104 @@ public class HugeGraphServer {
// Only switch on security manager after HugeGremlinServer started
SecurityManager securityManager = System.getSecurityManager();
System.setSecurityManager(null);
+ RestServer restServer = null;
+ GremlinServer preparedGremlinServer = null;
+ GremlinServer gremlinServer = null;
+ MemoryMonitor memoryMonitor = null;
+ try {
+ ConfigUtil.checkGremlinConfig(gremlinServerConf);
+ HugeConfig restServerConfig = new HugeConfig(restServerConf);
+ String graphsDir = restServerConfig.get(ServerOptions.GRAPHS);
+ EventHub hub = new EventHub("gremlin=>hub<=rest");
+
+ PdMetaDriver.PDAuthConfig.setAuthority(
+ ServiceConstant.SERVICE_NAME,
+ ServiceConstant.AUTHORITY);
+
+ // Prepare GremlinServer (registers GRAPH_CREATE listener) BEFORE
+ // RestServer starts loading graphs from PD/meta. This ensures that
+ // graphs loaded during RestServer initialization are captured by
+ // ContextGremlinServer's listener and injected into Gremlin's bindings.
+ try {
+ preparedGremlinServer = HugeGremlinServer.prepare(
+ gremlinServerConf, graphsDir, hub);
+ } catch (Throwable e) {
+ LOG.error("HugeGremlinServer prepare error: ", e);
+ throw e;
+ }
- ConfigUtil.checkGremlinConfig(gremlinServerConf);
- HugeConfig restServerConfig = new HugeConfig(restServerConf);
- String graphsDir = restServerConfig.get(ServerOptions.GRAPHS);
- EventHub hub = new EventHub("gremlin=>hub<=rest");
+ try {
+ // Start HugeRestServer (loads graphs from PD; events go to
+ // ContextGremlinServer which is already listening)
+ restServer = HugeRestServer.start(restServerConf, hub);
+ } catch (Throwable e) {
+ LOG.error("HugeRestServer start error: ", e);
+ stopPreparedGremlinServer(preparedGremlinServer);
+ throw e;
+ }
- PdMetaDriver.PDAuthConfig.setAuthority(
- ServiceConstant.SERVICE_NAME,
- ServiceConstant.AUTHORITY);
- try {
- // Start HugeRestServer
- this.restServer = HugeRestServer.start(restServerConf, hub);
- } catch (Throwable e) {
- LOG.error("HugeRestServer start error: ", e);
- throw e;
+ try {
+ // Now start the pre-prepared GremlinServer
+ gremlinServer = HugeGremlinServer.startPrepared(
+ preparedGremlinServer);
+ } catch (Throwable e) {
+ LOG.error("HugeGremlinServer start error: ", e);
+ try {
+ restServer.shutdown().get();
+ } catch (Throwable t) {
+ LOG.error("HugeRestServer stop error: ", t);
+ }
+ throw e;
+ }
+
+ try {
+ // Start (In-Heap) Memory Monitor
+ memoryMonitor = new MemoryMonitor(restServerConf);
+ memoryMonitor.start();
+ } catch (Throwable e) {
+ LOG.error("MemoryMonitor start error: ", e);
+ rollbackStartup(memoryMonitor, gremlinServer, restServer);
+ throw e;
+ }
+ } finally {
+ System.setSecurityManager(securityManager);
}
+ this.restServer = restServer;
+ this.gremlinServer = gremlinServer;
+ this.memoryMonitor = memoryMonitor;
+ }
+ private static void stopPreparedGremlinServer(GremlinServer server) {
+ if (server == null) {
+ return;
+ }
try {
- // Start GremlinServer
- this.gremlinServer = HugeGremlinServer.start(gremlinServerConf,
- graphsDir, hub);
- } catch (Throwable e) {
- LOG.error("HugeGremlinServer start error: ", e);
+ server.stop().get();
+ } catch (Throwable t) {
+ LOG.error("HugeGremlinServer stop error: ", t);
+ }
+ }
+
+ static void rollbackStartup(MemoryMonitor monitor,
+ GremlinServer gremlinServer,
+ RestServer restServer) {
+ if (monitor != null) {
try {
- this.restServer.shutdown().get();
+ monitor.stop();
} catch (Throwable t) {
- LOG.error("HugeRestServer stop error: ", t);
+ LOG.error("MemoryMonitor stop error: ", t);
}
- throw e;
- } finally {
- System.setSecurityManager(securityManager);
}
- // Start (In-Heap) Memory Monitor
- this.memoryMonitor = new MemoryMonitor(restServerConf);
- this.memoryMonitor.start();
+ stopPreparedGremlinServer(gremlinServer);
+
+ if (restServer != null) {
+ try {
+ restServer.shutdown().get();
+ } catch (Throwable t) {
+ LOG.error("HugeRestServer stop error: ", t);
+ }
+ }
}
public void stop() {
diff --git a/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/HugeGremlinServer.java b/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/HugeGremlinServer.java
index b3f397f54..d03f8172d 100644
--- a/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/HugeGremlinServer.java
+++ b/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/HugeGremlinServer.java
@@ -36,9 +36,16 @@ public class HugeGremlinServer {
private static final Logger LOG = Log.logger(HugeGremlinServer.class);
- public static GremlinServer start(String conf, String graphsDir,
- EventHub hub) throws Exception {
- // Start GremlinServer with inject traversal source
+ /**
+ * Construct a ContextGremlinServer and register its EventHub listeners
+ * WITHOUT starting it yet. This allows the GRAPH_CREATE listener to be
+ * in place before RestServer loads graphs from PD/meta, so that every
+ * graph gets injected into Gremlin's global bindings automatically.
+ *
+ * Call {@link #startPrepared(GremlinServer)} afterwards to actually start.
+ */
+ public static GremlinServer prepare(String conf, String graphsDir,
+ EventHub hub) throws Exception {
LOG.info(GremlinServer.getHeader());
final Settings settings;
try {
@@ -48,7 +55,6 @@ public class HugeGremlinServer {
"being parsed properly. [{}]", conf, e.getMessage());
throw e;
}
- // Scan graph confs and inject into gremlin server context
E.checkState(settings.graphs != null,
"The GremlinServer's settings.graphs is null");
if (graphsDir != null) {
@@ -61,18 +67,37 @@ public class HugeGremlinServer {
}
LOG.info("Configuring Gremlin Server from {}", conf);
+ // Constructing ContextGremlinServer registers GRAPH_CREATE/GRAPH_DROP
+ // listeners on the hub immediately, before any graphs are loaded.
ContextGremlinServer server = new ContextGremlinServer(settings, hub);
- // Inject customized traversal source
+ // Inject traversal sources for graphs in static config files
server.injectTraversalSource();
+ return server;
+ }
+
+ /**
+ * Start a ContextGremlinServer that was previously prepared via
+ * {@link #prepare(String, String, EventHub)}.
+ */
+ public static GremlinServer startPrepared(GremlinServer server)
+ throws Exception {
server.start().exceptionally(t -> {
LOG.error("Gremlin Server was unable to start and will " +
"shutdown now: {}", t.getMessage());
server.stop().join();
throw new HugeException("Failed to start Gremlin Server");
}).join();
-
return server;
}
+
+ /**
+ * Convenience method: prepare and start in one call (original behavior,
+ * kept for backward compatibility with any other callers).
+ */
+ public static GremlinServer start(String conf, String graphsDir,
+ EventHub hub) throws Exception {
+ return startPrepared(prepare(conf, graphsDir, hub));
+ }
}
diff --git a/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/MemoryMonitor.java b/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/MemoryMonitor.java
index 18b7cc170..f197bc65e 100644
--- a/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/MemoryMonitor.java
+++ b/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/MemoryMonitor.java
@@ -102,7 +102,7 @@ public class MemoryMonitor {
public void start() {
if (MEMORY_MONITOR_THRESHOLD >= 1.0) {
- LOG.info("Invalid parameter, MEMORY_MONITOR_THRESHOLD should ≤ 1.0.");
+ LOG.info("Memory monitoring is disabled by threshold setting.");
return;
}
this.scheduler.scheduleAtFixedRate(this::runMemoryDetect, 0, MEMORY_MONITOR_DETECT_PERIOD,
diff --git a/hugegraph-server/hugegraph-test/pom.xml b/hugegraph-server/hugegraph-test/pom.xml
index 7a5daeec5..b3fc645f7 100644
--- a/hugegraph-server/hugegraph-test/pom.xml
+++ b/hugegraph-server/hugegraph-test/pom.xml
@@ -170,6 +170,12 @@
<execution>
<id>api-test</id>
<configuration>
+ <!-- FIXME: HStore core guards and capability failures are tracked in
+ https://github.com/apache/hugegraph/issues/3090.
+ Keep backend scoped to API tests until the issue is resolved. -->
+ <systemPropertyVariables>
+ <backend>${backend}</backend>
+ </systemPropertyVariables>
<testSourceDirectory>${basedir}/src/main/java/
</testSourceDirectory>
<testClassesDirectory>${basedir}/target/classes/
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/ApiTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/ApiTestSuite.java
index 1fe8fc45f..f4d74e49b 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/ApiTestSuite.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/ApiTestSuite.java
@@ -44,6 +44,9 @@ import org.junit.runners.Suite;
GraphSpaceApiTest.class,
GraphSpaceApiStandaloneTest.class,
ManagerApiStandaloneTest.class,
+ GraphsApiStandaloneTest.class,
+ SchemaTemplateApiTest.class,
+ DefaultRoleApiStandaloneTest.class,
})
public class ApiTestSuite {
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/BaseApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/BaseApiTest.java
index a6f84de33..abe4b6839 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/BaseApiTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/BaseApiTest.java
@@ -707,7 +707,7 @@ public class BaseApiTest {
public static RestClient userClient(String username) {
String user1 = "{\"user_name\":\"%s\"," +
"\"user_password\":\"%s\"}";
- Response r = client.post("auth/users",
+ Response r = client.post("graphspaces/DEFAULT/auth/users",
String.format(user1, username, username));
assertResponseStatus(201, r);
@@ -721,11 +721,12 @@ public class BaseApiTest {
String spaceBody = "{\n" +
" \"user\": \"%s\",\n" +
- " \"type\": \"SPACE\",\n" +
- " \"graphspace\": \"%s\"\n" +
+ " \"type\": \"SPACE\"\n" +
"}";
- client.post("auth/managers", String.format(spaceBody, username,
- graphSpace));
+ Response r = client.post(String.format("graphspaces/%s/auth/managers",
+ graphSpace),
+ String.format(spaceBody, username));
+ assertResponseStatus(201, r);
return spaceClient;
}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/DefaultRoleApiStandaloneTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/DefaultRoleApiStandaloneTest.java
new file mode 100644
index 000000000..bab36ab9e
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/DefaultRoleApiStandaloneTest.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hugegraph.api;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+import jakarta.ws.rs.core.Response;
+
+/**
+ * Tests for the Hubble 2.0 default-role APIs added to GraphSpaceAPI and ManagerAPI
+ * in standalone (RocksDB) mode.
+ *
+ * In standalone mode these endpoints should return HTTP 400 with a
+ * friendly "not supported" error, because role/graphspace management
+ * requires the PD/hstore backend.
+ *
+ * Covered endpoints:
+ * POST /graphspaces/{gs}/role
+ * GET /graphspaces/{gs}/role?user=&role=
+ * DELETE /graphspaces/{gs}/role?user=&role=
+ * GET /graphspaces/{gs}/auth/managers/default?role=
+ */
+public class
+DefaultRoleApiStandaloneTest extends BaseApiTest {
+
+ private static final String SPACE = "DEFAULT";
+
+ private static String rolePath(String graphspace) {
+ return String.format("graphspaces/%s/role", graphspace);
+ }
+
+ private static String managerDefaultPath(String graphspace) {
+ return String.format("graphspaces/%s/auth/managers/default", graphspace);
+ }
+
+ @Before
+ public void assumeStandalone() {
+ assumeStandaloneMode();
+ }
+
+ // ------------------------------------------------------------------ //
+ // POST /graphspaces/{gs}/role
+ // ------------------------------------------------------------------ //
+
+ @Test
+ public void testCreateAnalystRoleReturnsFriendlyError() {
+ String body = "{\"user\":\"admin\",\"role\":\"ANALYST\"}";
+ Response r = client().post(rolePath(SPACE), body);
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue("should contain standalone error message",
+ content.contains(STANDALONE_ERROR));
+ }
+
+ @Test
+ public void testCreateObserverRoleReturnsFriendlyError() {
+ // OBSERVER requires a 'graph' field
+ String body = "{\"user\":\"admin\",\"role\":\"OBSERVER\",\"graph\":\"hugegraph\"}";
+ Response r = client().post(rolePath(SPACE), body);
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue(content.contains(STANDALONE_ERROR));
+ }
+
+ @Test
+ public void testCreateSpaceRoleReturnsFriendlyError() {
+ String body = "{\"user\":\"admin\",\"role\":\"SPACE\"}";
+ Response r = client().post(rolePath(SPACE), body);
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue(content.contains(STANDALONE_ERROR));
+ }
+
+ // ------------------------------------------------------------------ //
+ // GET /graphspaces/{gs}/role?user=&role=
+ // ------------------------------------------------------------------ //
+
+ @Test
+ public void testCheckAnalystRoleReturnsFriendlyError() {
+ Response r = client().get(rolePath(SPACE),
+ ImmutableMap.of("user", "admin", "role", "ANALYST"));
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue(content.contains(STANDALONE_ERROR));
+ }
+
+ @Test
+ public void testCheckObserverRoleReturnsFriendlyError() {
+ Response r = client().get(rolePath(SPACE),
+ ImmutableMap.of("user", "admin",
+ "role", "OBSERVER",
+ "graph", "hugegraph"));
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue(content.contains(STANDALONE_ERROR));
+ }
+
+ // ------------------------------------------------------------------ //
+ // DELETE /graphspaces/{gs}/role?user=&role=
+ // ------------------------------------------------------------------ //
+
+ @Test
+ public void testDeleteAnalystRoleReturnsFriendlyError() {
+ Response r = client().delete(rolePath(SPACE),
+ ImmutableMap.of("user", "admin", "role", "ANALYST"));
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue(content.contains(STANDALONE_ERROR));
+ }
+
+ @Test
+ public void testDeleteObserverRoleReturnsFriendlyError() {
+ Response r = client().delete(rolePath(SPACE),
+ ImmutableMap.of("user", "admin",
+ "role", "OBSERVER",
+ "graph", "hugegraph"));
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue(content.contains(STANDALONE_ERROR));
+ }
+
+ // ------------------------------------------------------------------ //
+ // GET /graphspaces/{gs}/auth/managers/default?role=
+ // ------------------------------------------------------------------ //
+
+ @Test
+ public void testCheckDefaultAnalystRoleReturnsFriendlyError() {
+ Response r = client().get(managerDefaultPath(SPACE),
+ ImmutableMap.of("role", "ANALYST"));
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue(content.contains(STANDALONE_ERROR));
+ }
+
+ @Test
+ public void testCheckDefaultObserverRoleReturnsFriendlyError() {
+ Response r = client().get(managerDefaultPath(SPACE),
+ ImmutableMap.of("role", "OBSERVER",
+ "graph", "hugegraph"));
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue(content.contains(STANDALONE_ERROR));
+ }
+
+ // ------------------------------------------------------------------ //
+ // Non-existent graphspace — should also return 400 in standalone
+ // ------------------------------------------------------------------ //
+
+ @Test
+ public void testCreateRoleNonExistentSpaceReturnsFriendlyError() {
+ String body = "{\"user\":\"admin\",\"role\":\"ANALYST\"}";
+ Response r = client().post(rolePath("nonexistent"), body);
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue(content.contains(STANDALONE_ERROR));
+ }
+
+ @Test
+ public void testCheckRoleNonExistentSpaceReturnsFriendlyError() {
+ Response r = client().get(rolePath("nonexistent"),
+ ImmutableMap.of("user", "admin", "role", "ANALYST"));
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue(content.contains(STANDALONE_ERROR));
+ }
+
+ @Test
+ public void testDeleteRoleNonExistentSpaceReturnsFriendlyError() {
+ Response r = client().delete(rolePath("nonexistent"),
+ ImmutableMap.of("user", "admin", "role", "ANALYST"));
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue(content.contains(STANDALONE_ERROR));
+ }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/GraphSpaceApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/GraphSpaceApiTest.java
index 1c3eb7799..98240e76f 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/GraphSpaceApiTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/GraphSpaceApiTest.java
@@ -41,8 +41,11 @@ public class GraphSpaceApiTest extends BaseApiTest {
Objects.equals("hstore", System.getProperty("backend")));
Response r = this.client().get(PATH);
String result = r.readEntity(String.class);
+ Assert.assertEquals(result, 200, r.getStatus());
Map<String, Object> resultMap = JsonUtil.fromJson(result, Map.class);
List<String> spaces = (List<String>)resultMap.get("graphSpaces");
+ Assert.assertNotNull("graphspace API should be available in hstore",
+ spaces);
for (String space : spaces) {
if (!"DEFAULT".equals(space)) {
this.client().delete(PATH, space);
@@ -431,4 +434,157 @@ public class GraphSpaceApiTest extends BaseApiTest {
}
Assert.assertTrue("auth_test_space not found in profile list", found);
}
+
+ @Test
+ public void testDefaultGraphLifecycle() {
+ String space = "default_graph_space";
+ String graph = "default_graph_test";
+ createSpace(space, true);
+ Response r = createGraph(space, graph, "DefaultGraphNick");
+ assertResponseStatus(201, r);
+
+ String graphsPath = String.format("graphspaces/%s/graphs", space);
+ r = this.client().post(graphsPath + "/" + graph + "/default", "");
+ String result = assertResponseStatus(200, r);
+ Assert.assertTrue(result.contains(graph));
+
+ r = this.client().get(graphsPath + "/default");
+ result = assertResponseStatus(200, r);
+ Assert.assertTrue(result.contains(graph));
+
+ r = this.client().get(graphsPath + "/profile");
+ result = assertResponseStatus(200, r);
+ @SuppressWarnings("unchecked")
+ List<Map<String, Object>> profiles = JsonUtil.fromJson(result, List.class);
+ Map<String, Object> profile = profiles.stream()
+ .filter(p -> graph.equals(p.get("name")))
+ .findFirst()
+ .orElse(null);
+ Assert.assertNotNull("Default graph should appear in profile list", profile);
+ Assert.assertEquals(Boolean.TRUE, profile.get("default"));
+
+ r = this.client().delete(graphsPath + "/" + graph + "/default",
+ ImmutableMap.of());
+ result = assertResponseStatus(200, r);
+ Assert.assertFalse(result.contains(graph));
+ }
+
+ @Test
+ public void testDefaultRoleLifecycle() {
+ String space = "default_role_space";
+ createSpace(space, true);
+
+ String rolePath = String.format("graphspaces/%s/role", space);
+ String body = "{\"user\":\"admin\",\"role\":\"ANALYST\"}";
+ Response r = this.client().post(rolePath, body);
+ assertResponseStatus(201, r);
+
+ r = this.client().get(rolePath,
+ ImmutableMap.of("user", "admin",
+ "role", "ANALYST"));
+ String result = assertResponseStatus(200, r);
+ Assert.assertTrue(result.contains("true"));
+
+ r = this.client().delete(rolePath,
+ ImmutableMap.of("user", "admin",
+ "role", "ANALYST"));
+ assertResponseStatus(204, r);
+
+ r = this.client().get(rolePath,
+ ImmutableMap.of("user", "admin",
+ "role", "ANALYST"));
+ result = assertResponseStatus(200, r);
+ Assert.assertTrue(result.contains("false"));
+ }
+
+ @Test
+ public void testDefaultRoleMutationRequiresSpaceManager() {
+ String space = "default_role_auth_space";
+ String analyst = "default_role_analyst";
+ String target = "default_role_target";
+ String manager = "default_role_manager";
+ createSpace(space, true);
+
+ RestClient analystClient = userClient(analyst);
+ userClient(target);
+ RestClient managerClient = spaceManagerClient(space, manager);
+
+ String rolePath = String.format("graphspaces/%s/role", space);
+ String analystBody = String.format("{\"user\":\"%s\",\"role\":\"ANALYST\"}",
+ analyst);
+ Response r = this.client().post(rolePath, analystBody);
+ assertResponseStatus(201, r);
+
+ String grantTargetBody = String.format("{\"user\":\"%s\",\"role\":\"ANALYST\"}",
+ target);
+ r = analystClient.post(rolePath, grantTargetBody);
+ assertResponseStatus(403, r);
+
+ r = analystClient.get(rolePath,
+ ImmutableMap.of("user", target,
+ "role", "ANALYST"));
+ assertResponseStatus(403, r);
+
+ r = analystClient.delete(rolePath,
+ ImmutableMap.of("user", analyst,
+ "role", "ANALYST"));
+ assertResponseStatus(403, r);
+
+ r = managerClient.post(rolePath, grantTargetBody);
+ assertResponseStatus(201, r);
+
+ r = managerClient.get(rolePath,
+ ImmutableMap.of("user", target,
+ "role", "ANALYST"));
+ String result = assertResponseStatus(200, r);
+ Assert.assertTrue(result.contains("true"));
+
+ String spaceRoleBody = String.format("{\"user\":\"%s\",\"role\":\"SPACE\"}",
+ target);
+ r = managerClient.post(rolePath, spaceRoleBody);
+ assertResponseStatus(403, r);
+
+ r = managerClient.delete(rolePath,
+ ImmutableMap.of("user", target,
+ "role", "ANALYST"));
+ assertResponseStatus(204, r);
+ }
+
+ @Test
+ public void testSchemaTemplateLifecycle() {
+ String space = "schema_template_space";
+ String template = "hubble_schema_template";
+ String schema = "schema.propertyKey('name').asText().ifNotExist().create();";
+ String path = String.format("graphspaces/%s/schematemplates", space);
+ createSpace(space, true);
+
+ String body = String.format("{\"name\":\"%s\",\"schema\":\"%s\"}",
+ template, schema);
+ Response r = this.client().post(path, body);
+ String result = assertResponseStatus(201, r);
+ Assert.assertTrue(result.contains(template));
+
+ r = this.client().get(path, template);
+ result = assertResponseStatus(200, r);
+ Assert.assertTrue(result.contains(template));
+
+ r = this.client().get(path);
+ result = assertResponseStatus(200, r);
+ Assert.assertTrue(result.contains(template));
+
+ String updated = "schema.propertyKey('age').asInt().ifNotExist().create();";
+ body = String.format("{\"schema\":\"%s\"}", updated);
+ r = this.client().target(baseUrl())
+ .path(path + "/" + template)
+ .request()
+ .put(jakarta.ws.rs.client.Entity.json(body));
+ result = assertResponseStatus(200, r);
+ Assert.assertTrue(result.contains("age"));
+
+ r = this.client().delete(path, template);
+ assertResponseStatus(204, r);
+
+ r = this.client().get(path, template);
+ assertResponseStatus(404, r);
+ }
}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/GraphsApiStandaloneTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/GraphsApiStandaloneTest.java
new file mode 100644
index 000000000..bdd65be9a
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/GraphsApiStandaloneTest.java
@@ -0,0 +1,306 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hugegraph.api;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hugegraph.util.JsonUtil;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+import jakarta.ws.rs.core.Response;
+
+/**
+ * Tests for Hubble-compatible graph profile and default graph APIs
+ * in standalone (RocksDB) mode:
+ * - GET /graphspaces/{gs}/graphs/profile — works with default graph
+ * - POST /graphspaces/{gs}/graphs/{name}/default — sets default graph
+ * - DELETE /graphspaces/{gs}/graphs/{name}/default — unsets default graph
+ * - GET /graphspaces/{gs}/graphs/default — gets default graph
+ * - PUT /graphspaces/{gs}/graphs/{name} — update nickname
+ */
+public class GraphsApiStandaloneTest extends BaseApiTest {
+
+ private static final String SPACE = "DEFAULT";
+ private static final String GRAPHS_PATH = "graphspaces/DEFAULT/graphs";
+ private static final String PROFILE_PATH = GRAPHS_PATH + "/profile";
+ private static final String DEFAULT_PATH = GRAPHS_PATH + "/default";
+
+ /** Graph names created per test — cleaned up in teardown */
+ private static final String TEST_GRAPH = "gst_test_graph";
+ private static final String TEST_GRAPH2 = "gst_test_graph2";
+
+ @Before
+ public void assumeStandalone() {
+ assumeStandaloneMode();
+ }
+
+ @After
+ @Override
+ public void teardown() throws Exception {
+ client().delete(GRAPHS_PATH + "/" + TEST_GRAPH + "/default",
+ ImmutableMap.of());
+ client().delete(GRAPHS_PATH + "/" + TEST_GRAPH2 + "/default",
+ ImmutableMap.of());
+ Map<String, Object> drop = ImmutableMap.of(
+ "confirm_message", "I'm sure to drop the graph");
+ client().delete(GRAPHS_PATH + "/" + TEST_GRAPH, drop);
+ client().delete(GRAPHS_PATH + "/" + TEST_GRAPH2, drop);
+ super.teardown();
+ }
+
+ // ------------------------------------------------------------------ //
+ // GET /graphspaces/{gs}/graphs/profile
+ // ------------------------------------------------------------------ //
+
+ @Test
+ public void testListProfileReturnsGraphEntries() {
+ Response r = client().get(PROFILE_PATH);
+ String content = assertResponseStatus(200, r);
+
+ @SuppressWarnings("unchecked")
+ List<Map<String, Object>> profiles = JsonUtil.fromJson(content, List.class);
+ Assert.assertFalse("profile list should not be empty", profiles.isEmpty());
+
+ for (Map<String, Object> p : profiles) {
+ Assert.assertTrue("profile entry must have 'name'", p.containsKey("name"));
+ Assert.assertTrue("profile entry must have 'default'", p.containsKey("default"));
+ }
+ }
+
+ @Test
+ public void testListProfileContainsCreatedGraph() {
+ Response r = createGraphInRocksDB(SPACE, TEST_GRAPH, "ProfileTestNick");
+ assertResponseStatus(201, r);
+
+ Response profile = client().get(PROFILE_PATH);
+ String content = assertResponseStatus(200, profile);
+
+ @SuppressWarnings("unchecked")
+ List<Map<String, Object>> profiles = JsonUtil.fromJson(content, List.class);
+ boolean found = profiles.stream()
+ .anyMatch(p -> TEST_GRAPH.equals(p.get("name")));
+ Assert.assertTrue("Newly created graph should appear in profile list", found);
+ }
+
+ @Test
+ public void testListProfileWithPrefixFilter() {
+ Response r1 = createGraphInRocksDB(SPACE, TEST_GRAPH, "PrefixNick");
+ assertResponseStatus(201, r1);
+ Response r2 = createGraphInRocksDB(SPACE, TEST_GRAPH2, "OtherNick");
+ assertResponseStatus(201, r2);
+
+ Response profile = client().get(PROFILE_PATH,
+ ImmutableMap.of("prefix", TEST_GRAPH));
+ String content = assertResponseStatus(200, profile);
+
+ @SuppressWarnings("unchecked")
+ List<Map<String, Object>> profiles = JsonUtil.fromJson(content, List.class);
+ for (Map<String, Object> p : profiles) {
+ String name = String.valueOf(p.get("name"));
+ Object nickObj = p.get("nickname");
+ String nick = nickObj != null ? String.valueOf(nickObj) : "";
+ boolean matches = name.startsWith(TEST_GRAPH) ||
+ nick.startsWith(TEST_GRAPH);
+ Assert.assertTrue("All returned profiles should match prefix: " + name,
+ matches);
+ }
+ }
+
+ @Test
+ public void testListProfileNicknameField() {
+ Response r = createGraphInRocksDB(SPACE, TEST_GRAPH, "MyNickname");
+ assertResponseStatus(201, r);
+
+ Response profile = client().get(PROFILE_PATH);
+ String content = assertResponseStatus(200, profile);
+
+ @SuppressWarnings("unchecked")
+ List<Map<String, Object>> profiles = JsonUtil.fromJson(content, List.class);
+ Map<String, Object> entry = profiles.stream()
+ .filter(p -> TEST_GRAPH.equals(p.get("name")))
+ .findFirst()
+ .orElse(null);
+ Assert.assertNotNull("Graph should be present in profile list", entry);
+ Assert.assertTrue("Profile entry should contain 'nickname'",
+ entry.containsKey("nickname"));
+ Assert.assertEquals("MyNickname", entry.get("nickname"));
+ }
+
+ @Test
+ public void testListProfileContainsDefaultGraph() {
+ Response r = createGraphInRocksDB(SPACE, TEST_GRAPH);
+ assertResponseStatus(201, r);
+ Response setR = client().post(GRAPHS_PATH + "/" + TEST_GRAPH + "/default", "");
+ assertResponseStatus(200, setR);
+
+ Response profile = client().get(PROFILE_PATH);
+ String content = assertResponseStatus(200, profile);
+
+ @SuppressWarnings("unchecked")
+ List<Map<String, Object>> profiles = JsonUtil.fromJson(content, List.class);
+ Map<String, Object> entry = profiles.stream()
+ .filter(p -> TEST_GRAPH.equals(p.get("name")))
+ .findFirst()
+ .orElse(null);
+ Assert.assertNotNull(entry);
+ Assert.assertEquals("default graph should be marked in standalone mode",
+ Boolean.TRUE, entry.get("default"));
+ }
+
+ // ------------------------------------------------------------------ //
+ // Canonical default graph operations work in standalone mode
+ // ------------------------------------------------------------------ //
+
+ @Test
+ public void testSetDefaultGraph() {
+ Response r = createGraphInRocksDB(SPACE, TEST_GRAPH);
+ assertResponseStatus(201, r);
+
+ Response setR = client().post(GRAPHS_PATH + "/" + TEST_GRAPH + "/default", "");
+ String content = assertResponseStatus(200, setR);
+ Assert.assertTrue(content.contains(TEST_GRAPH));
+
+ Response repeatedSetR = client().post(GRAPHS_PATH + "/" + TEST_GRAPH +
+ "/default", "");
+ content = assertResponseStatus(200, repeatedSetR);
+ Assert.assertTrue(content.contains(TEST_GRAPH));
+ }
+
+ @Test
+ public void testUnsetDefaultGraph() {
+ Response r = createGraphInRocksDB(SPACE, TEST_GRAPH);
+ assertResponseStatus(201, r);
+
+ Response setR = client().post(GRAPHS_PATH + "/" + TEST_GRAPH + "/default", "");
+ assertResponseStatus(200, setR);
+
+ Response unsetR = client().delete(GRAPHS_PATH + "/" + TEST_GRAPH + "/default",
+ ImmutableMap.of());
+ String content = assertResponseStatus(200, unsetR);
+ Assert.assertFalse(content.contains(TEST_GRAPH));
+ }
+
+ @Test
+ public void testSetDefaultGraphWithGetIsNotAllowed() {
+ Response r = createGraphInRocksDB(SPACE, TEST_GRAPH);
+ assertResponseStatus(201, r);
+
+ Response setR = client().get(GRAPHS_PATH + "/" + TEST_GRAPH + "/default");
+ assertResponseStatus(405, setR);
+ }
+
+ @Test
+ public void testUnsetDefaultGraphWithGetIsNotFound() {
+ Response r = createGraphInRocksDB(SPACE, TEST_GRAPH);
+ assertResponseStatus(201, r);
+
+ Response unsetR = client().get(GRAPHS_PATH + "/" + TEST_GRAPH + "/undefault");
+ assertResponseStatus(404, unsetR);
+ }
+
+ @Test
+ public void testGetDefaultGraph() {
+ Response r = createGraphInRocksDB(SPACE, TEST_GRAPH);
+ assertResponseStatus(201, r);
+ Response setR = client().post(GRAPHS_PATH + "/" + TEST_GRAPH + "/default", "");
+ assertResponseStatus(200, setR);
+
+ Response getR = client().get(DEFAULT_PATH);
+ String content = assertResponseStatus(200, getR);
+ Assert.assertTrue(content.contains(TEST_GRAPH));
+ }
+
+ // ------------------------------------------------------------------ //
+ // PUT /graphspaces/{gs}/graphs/{name} — update nickname
+ // ------------------------------------------------------------------ //
+
+ @Test
+ public void testUpdateGraphNickname() {
+ Response r = createGraphInRocksDB(SPACE, TEST_GRAPH, "OldNickname");
+ assertResponseStatus(201, r);
+
+ Response updateR = updateGraph("update", SPACE, TEST_GRAPH, "NewNickname");
+ String content = assertResponseStatus(200, updateR);
+
+ @SuppressWarnings("unchecked")
+ Map<String, String> result = JsonUtil.fromJson(content, Map.class);
+ Assert.assertTrue("Response should contain graph name key",
+ result.containsKey(TEST_GRAPH));
+ Assert.assertEquals("updated", result.get(TEST_GRAPH));
+ }
+
+ @Test
+ public void testUpdateGraphWithInvalidAction() {
+ Response r = createGraphInRocksDB(SPACE, TEST_GRAPH);
+ assertResponseStatus(201, r);
+
+ String badBody = "{\"action\": \"invalid_action\", \"update\": {\"name\": \"" +
+ TEST_GRAPH + "\", \"nickname\": \"X\"}}";
+ Response updateR = client().target(baseUrl())
+ .path(GRAPHS_PATH + "/" + TEST_GRAPH)
+ .request()
+ .put(jakarta.ws.rs.client.Entity.json(badBody));
+ assertResponseStatus(400, updateR);
+ }
+
+ @Test
+ public void testUpdateGraphWithInvalidFieldTypeReturnsBadRequest() {
+ Response r = createGraphInRocksDB(SPACE, TEST_GRAPH);
+ assertResponseStatus(201, r);
+
+ String badNameBody = "{\"action\": \"update\", \"update\": {\"name\": 123, " +
+ "\"nickname\": \"X\"}}";
+ Response updateR = client().target(baseUrl())
+ .path(GRAPHS_PATH + "/" + TEST_GRAPH)
+ .request()
+ .put(jakarta.ws.rs.client.Entity.json(badNameBody));
+ assertResponseStatus(400, updateR);
+
+ String badNicknameBody = "{\"action\": \"update\", \"update\": {\"name\": \"" +
+ TEST_GRAPH + "\", \"nickname\": 123}}";
+ updateR = client().target(baseUrl())
+ .path(GRAPHS_PATH + "/" + TEST_GRAPH)
+ .request()
+ .put(jakarta.ws.rs.client.Entity.json(badNicknameBody));
+ assertResponseStatus(400, updateR);
+ }
+
+ @Test
+ public void testCreateGraphWithInvalidConfigValueReturnsBadRequest() {
+ String nullValueBody = "{\"nickname\":\"NullConfig\",\"search.text_analyzer\":null}";
+ Response createR = client().target(baseUrl())
+ .path(GRAPHS_PATH + "/" + TEST_GRAPH)
+ .request()
+ .post(jakarta.ws.rs.client.Entity.json(nullValueBody));
+ assertResponseStatus(400, createR);
+
+ String objectValueBody = "{\"nickname\":\"ObjectConfig\"," +
+ "\"schema\":{\"name\":\"template\"}}";
+ createR = client().target(baseUrl())
+ .path(GRAPHS_PATH + "/" + TEST_GRAPH2)
+ .request()
+ .post(jakarta.ws.rs.client.Entity.json(objectValueBody));
+ assertResponseStatus(400, createR);
+ }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/SchemaTemplateApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/SchemaTemplateApiTest.java
new file mode 100644
index 000000000..64a63a660
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/SchemaTemplateApiTest.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hugegraph.api;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import jakarta.ws.rs.core.Response;
+
+/**
+ * Tests for Schema Template APIs in standalone (RocksDB) mode.
+ *
+ * SchemaTemplate management requires the PD/hstore backend (it relies on
+ * the distributed meta manager). In standalone mode every endpoint must
+ * return HTTP 400 with a friendly "not supported" error message, matching
+ * the behaviour of other PD-only APIs (ManagerAPI, GraphSpaceAPI roles).
+ *
+ * Covered endpoints:
+ * GET /graphspaces/{gs}/schematemplates
+ * GET /graphspaces/{gs}/schematemplates/{name}
+ * POST /graphspaces/{gs}/schematemplates
+ * PUT /graphspaces/{gs}/schematemplates/{name}
+ * DELETE /graphspaces/{gs}/schematemplates/{name}
+ */
+public class SchemaTemplateApiTest extends BaseApiTest {
+
+ private static final String PATH = "graphspaces/DEFAULT/schematemplates";
+ private static final String TEMPLATE_NAME = "st_test_template";
+ private static final String TEMPLATE_SCHEMA =
+ "schema.propertyKey('name').asText().ifNotExist().create();";
+
+ @Before
+ public void assumeStandalone() {
+ assumeStandaloneMode();
+ }
+
+ // ------------------------------------------------------------------ //
+ // GET /graphspaces/{gs}/schematemplates
+ // ------------------------------------------------------------------ //
+
+ @Test
+ public void testListSchemaTemplatesReturnsFriendlyError() {
+ Response r = client().get(PATH);
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue(content.contains(STANDALONE_ERROR));
+ }
+
+ // ------------------------------------------------------------------ //
+ // GET /graphspaces/{gs}/schematemplates/{name}
+ // ------------------------------------------------------------------ //
+
+ @Test
+ public void testGetSchemaTemplateReturnsFriendlyError() {
+ Response r = client().get(PATH, TEMPLATE_NAME);
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue(content.contains(STANDALONE_ERROR));
+ }
+
+ // ------------------------------------------------------------------ //
+ // POST /graphspaces/{gs}/schematemplates
+ // ------------------------------------------------------------------ //
+
+ @Test
+ public void testCreateSchemaTemplateReturnsFriendlyError() {
+ String body = String.format("{\"name\":\"%s\",\"schema\":\"%s\"}",
+ TEMPLATE_NAME, TEMPLATE_SCHEMA);
+ Response r = client().post(PATH, body);
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue(content.contains(STANDALONE_ERROR));
+ }
+
+ // ------------------------------------------------------------------ //
+ // PUT /graphspaces/{gs}/schematemplates/{name}
+ // ------------------------------------------------------------------ //
+
+ @Test
+ public void testUpdateSchemaTemplateReturnsFriendlyError() {
+ String body = String.format("{\"schema\":\"%s\"}", TEMPLATE_SCHEMA);
+ Response r = client().target(baseUrl())
+ .path(PATH + "/" + TEMPLATE_NAME)
+ .request()
+ .put(jakarta.ws.rs.client.Entity.json(body));
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue(content.contains(STANDALONE_ERROR));
+ }
+
+ // ------------------------------------------------------------------ //
+ // DELETE /graphspaces/{gs}/schematemplates/{name}
+ // ------------------------------------------------------------------ //
+
+ @Test
+ public void testDeleteSchemaTemplateReturnsFriendlyError() {
+ Response r = client().delete(PATH, TEMPLATE_NAME);
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue(content.contains(STANDALONE_ERROR));
+ }
+
+ // ------------------------------------------------------------------ //
+ // Non-existent graphspace — also returns 400 in standalone
+ // ------------------------------------------------------------------ //
+
+ @Test
+ public void testListSchemaTemplatesNonExistentSpaceReturnsFriendlyError() {
+ Response r = client().get("graphspaces/nonexistent/schematemplates");
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue(content.contains(STANDALONE_ERROR));
+ }
+
+ @Test
+ public void testCreateSchemaTemplateNonExistentSpaceReturnsFriendlyError() {
+ String body = String.format("{\"name\":\"%s\",\"schema\":\"%s\"}",
+ TEMPLATE_NAME, TEMPLATE_SCHEMA);
+ Response r = client().post("graphspaces/nonexistent/schematemplates", body);
+ String content = assertResponseStatus(400, r);
+ Assert.assertTrue(content.contains(STANDALONE_ERROR));
+ }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java
index bbf7db656..eadc27713 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java
@@ -3584,7 +3584,8 @@ public class EdgeCoreTest extends BaseCoreTest {
@Test
public void testQueryOutEdgesOfVertexBySortkeyWithRange() {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
@@ -3682,7 +3683,8 @@ public class EdgeCoreTest extends BaseCoreTest {
@Test
public void testQueryOutEdgesOfVertexBySortkeyWithPrefix() {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
@@ -3780,7 +3782,8 @@ public class EdgeCoreTest extends BaseCoreTest {
@Test
public void testQueryOutEdgesOfVertexBySortkeyWithPrefixInPage() {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
@@ -3895,7 +3898,8 @@ public class EdgeCoreTest extends BaseCoreTest {
@Test
public void testQueryOutEdgesOfVertexBySortkeyWithMoreFields() {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
@@ -4116,7 +4120,8 @@ public class EdgeCoreTest extends BaseCoreTest {
@Test
public void testQueryOutEdgesOfVertexBySortkeyWithMoreFieldsInPage() {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
@@ -5244,7 +5249,8 @@ public class EdgeCoreTest extends BaseCoreTest {
@Test
public void testScanEdgeInPaging() {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/MultiGraphsTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/MultiGraphsTest.java
index 2c9c11021..70aa5df7d 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/MultiGraphsTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/MultiGraphsTest.java
@@ -83,7 +83,8 @@ public class MultiGraphsTest extends BaseCoreTest {
@Test
public void testCopySchemaWithMultiGraphs() {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
@@ -292,7 +293,8 @@ public class MultiGraphsTest extends BaseCoreTest {
@Test
public void testCreateGraphWithSameNameDifferentBackends() throws Exception {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/RamTableTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/RamTableTest.java
index 54d1bd7bc..8a1f6c491 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/RamTableTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/RamTableTest.java
@@ -92,7 +92,8 @@ public class RamTableTest extends BaseCoreTest {
@Test
public void testReloadAndQuery() throws Exception {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
@@ -143,7 +144,8 @@ public class RamTableTest extends BaseCoreTest {
@Test
public void testReloadFromFileAndQuery() throws Exception {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
@@ -221,7 +223,8 @@ public class RamTableTest extends BaseCoreTest {
@Test
public void testReloadAndQueryWithMultiEdges() throws Exception {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
@@ -352,7 +355,8 @@ public class RamTableTest extends BaseCoreTest {
@Test
public void testReloadAndQueryWithBigVertex() throws Exception {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
@@ -420,7 +424,8 @@ public class RamTableTest extends BaseCoreTest {
@Test
public void testReloadAndQueryWithProperty() throws Exception {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java
index d33f9bb07..4b7ec65bc 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java
@@ -2241,7 +2241,8 @@ public class VertexCoreTest extends BaseCoreTest {
@Test
public void testAddOlapSecondaryProperties() {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
@@ -2332,7 +2333,8 @@ public class VertexCoreTest extends BaseCoreTest {
@Test
public void testAddOlapRangeProperties() {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
@@ -2458,7 +2460,8 @@ public class VertexCoreTest extends BaseCoreTest {
@Test
public void testAddOlapRangeAndOlapSecondaryProperties() {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
@@ -2632,7 +2635,8 @@ public class VertexCoreTest extends BaseCoreTest {
@Test
public void testQueryOlapRangeAndRegularSecondaryProperties() {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
@@ -2761,7 +2765,8 @@ public class VertexCoreTest extends BaseCoreTest {
@Test
public void testQueryOlapWithUpdates() {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
@@ -7734,7 +7739,8 @@ public class VertexCoreTest extends BaseCoreTest {
@Test
public void testScanVertexInPaging() {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
@@ -8480,7 +8486,8 @@ public class VertexCoreTest extends BaseCoreTest {
@Test
public void testQueryByPropertyInPageWithLimitGtPageSize() {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
@@ -8560,7 +8567,8 @@ public class VertexCoreTest extends BaseCoreTest {
@Test
public void testQueryBySingleRangePropertyInPage() {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
@@ -8709,7 +8717,8 @@ public class VertexCoreTest extends BaseCoreTest {
@Test
public void testQueryByRangeIndexInPage() {
- // FIXME: skip this test for hstore
+ // FIXME: The legacy HStore guard and related coverage debt are tracked in
+ // https://github.com/apache/hugegraph/issues/3090
Assume.assumeTrue("skip this test for hstore",
Objects.equals("hstore", System.getProperty("backend")));
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
index f7b2729fe..433e75a81 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
@@ -24,6 +24,7 @@ import org.apache.hugegraph.traversal.optimize.TraversalUtilOptimizeTest;
import org.apache.hugegraph.unit.api.filter.LoadDetectFilterTest;
import org.apache.hugegraph.unit.api.filter.PathFilterTest;
import org.apache.hugegraph.unit.api.gremlin.GremlinQueryAPITest;
+import org.apache.hugegraph.unit.api.space.GraphSpaceAPITest;
import org.apache.hugegraph.unit.auth.HugeGraphAuthProxyTest;
import org.apache.hugegraph.unit.cache.CacheManagerTest;
import org.apache.hugegraph.unit.cache.CacheTest;
@@ -95,6 +96,9 @@ import org.junit.runners.Suite;
/* api gremlin */
GremlinQueryAPITest.class,
+ /* api space */
+ GraphSpaceAPITest.class,
+
/* cache */
CacheTest.RamCacheTest.class,
CacheTest.OffheapCacheTest.class,
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/space/GraphSpaceAPITest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/space/GraphSpaceAPITest.java
new file mode 100644
index 000000000..caa659a4d
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/space/GraphSpaceAPITest.java
@@ -0,0 +1,274 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hugegraph.unit.api.space;
+
+import static org.apache.hugegraph.HugeFactory.SYS_GRAPH;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.hugegraph.HugeGraph;
+import org.apache.hugegraph.api.auth.ManagerAPI;
+import org.apache.hugegraph.api.profile.GraphsAPI;
+import org.apache.hugegraph.api.space.GraphSpaceAPI;
+import org.apache.hugegraph.auth.AuthManager;
+import org.apache.hugegraph.auth.HugeAuthenticator;
+import org.apache.hugegraph.auth.HugeDefaultRole;
+import org.apache.hugegraph.auth.HugeGraphAuthProxy;
+import org.apache.hugegraph.auth.HugeUser;
+import org.apache.hugegraph.auth.RolePermission;
+import org.apache.hugegraph.core.GraphManager;
+import org.apache.hugegraph.meta.MetaManager;
+import org.apache.hugegraph.space.GraphSpace;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.testutil.Whitebox;
+import org.apache.hugegraph.unit.BaseUnitTest;
+import org.apache.tinkerpop.gremlin.structure.Graph;
+import org.junit.After;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import jakarta.ws.rs.ForbiddenException;
+import jakarta.ws.rs.core.SecurityContext;
+import sun.misc.Unsafe;
+
+public class GraphSpaceAPITest extends BaseUnitTest {
+
+ private static final String GRAPHSPACE = "default_role_space";
+ private static final String ADMIN = "admin";
+ private static final String OPERATOR = "space_manager";
+ private static final String TARGET = "target_user";
+ private static final String GRAPH = "hugegraph";
+
+ @After
+ public void tearDown() {
+ HugeGraphAuthProxy.resetContext();
+ }
+
+ @Test
+ public void testSpaceManagerCannotCheckSpaceDefaultRole() {
+ GraphSpaceAPI api = new GraphSpaceAPI();
+ GraphManager manager = managerWithDefaultRoleContext(OPERATOR, false);
+ setContext(OPERATOR);
+
+ Assert.assertThrows(ForbiddenException.class, () -> {
+ api.checkDefaultRole(manager, GRAPHSPACE, TARGET, "SPACE", null);
+ });
+ }
+
+ @Test
+ public void testAdminCanCheckSpaceDefaultRole() {
+ GraphSpaceAPI api = new GraphSpaceAPI();
+ GraphManager manager = managerWithDefaultRoleContext(ADMIN, true);
+ setContext(ADMIN);
+
+ String result = api.checkDefaultRole(manager, GRAPHSPACE, TARGET,
+ "SPACE", null);
+
+ Assert.assertContains("\"check\":true", result);
+ }
+
+ @Test
+ public void testManagerDefaultRoleRejectsMissingGraphSpace() {
+ ManagerAPI api = new ManagerAPI();
+ GraphManager manager = managerWithDefaultRoleContext(TARGET, false);
+ Whitebox.setInternalState(manager, "graphSpaces",
+ new ConcurrentHashMap<>());
+ setContext(TARGET);
+
+ Assert.assertThrows(IllegalArgumentException.class, () -> {
+ api.checkDefaultRole(manager, GRAPHSPACE, "SPACE", null);
+ }, e -> {
+ Assert.assertContains("graph space", e.getMessage());
+ Assert.assertContains("does not exist", e.getMessage());
+ });
+ }
+
+ @Test
+ public void testManagerDefaultRoleRejectsMissingObserverGraph() {
+ ManagerAPI api = new ManagerAPI();
+ GraphManager manager = managerWithDefaultRoleContext(TARGET, false);
+ setContext(TARGET);
+
+ Assert.assertThrows(IllegalArgumentException.class, () -> {
+ api.checkDefaultRole(manager, GRAPHSPACE, "OBSERVER", "missing");
+ }, e -> {
+ Assert.assertContains("Graph", e.getMessage());
+ Assert.assertContains("does not exist", e.getMessage());
+ });
+ }
+
+ @Test
+ public void testManagerDefaultRoleAcceptsExistingResources() {
+ ManagerAPI api = new ManagerAPI();
+ GraphManager manager = managerWithDefaultRoleContext(TARGET, false);
+ setContext(TARGET);
+
+ String spaceResult = api.checkDefaultRole(manager, GRAPHSPACE,
+ "SPACE", null);
+ String observerResult = api.checkDefaultRole(manager, GRAPHSPACE,
+ "OBSERVER", GRAPH);
+
+ Assert.assertContains("\"check\":true", spaceResult);
+ Assert.assertContains("\"check\":true", observerResult);
+ }
+
+ @Test
+ public void testSpaceManagerCannotDeleteSpaceDefaultRole() {
+ GraphSpaceAPI api = new GraphSpaceAPI();
+ GraphManager manager = managerWithDefaultRoleContext(OPERATOR, false);
+ setContext(OPERATOR);
+
+ Assert.assertThrows(ForbiddenException.class, () -> {
+ api.deleteDefaultRole(manager, GRAPHSPACE, TARGET, "SPACE", null);
+ });
+ }
+
+ @Test
+ public void testPdGraphsIncludesLoadedLocalConfigGraphs() {
+ GraphManager manager = allocateGraphManager();
+ Whitebox.setInternalState(manager, "PDExist", true);
+ Whitebox.setInternalState(manager, "localGraphs",
+ Collections.emptySet());
+ MetaManager metaManager = Mockito.mock(MetaManager.class);
+ Mockito.when(metaManager.graphConfigs("DEFAULT"))
+ .thenReturn(Collections.emptyMap());
+ Whitebox.setInternalState(manager, "metaManager", metaManager);
+
+ Map<String, Graph> graphs = new ConcurrentHashMap<>();
+ graphs.put("DEFAULT-hugegraph", Mockito.mock(Graph.class));
+ graphs.put("DEFAULT-" + SYS_GRAPH, Mockito.mock(Graph.class));
+ Whitebox.setInternalState(manager, "graphs", graphs);
+
+ Set<String> result = manager.graphs("DEFAULT");
+
+ Assert.assertTrue(result.contains("hugegraph"));
+ Assert.assertFalse(result.contains(SYS_GRAPH));
+ }
+
+ @Test
+ public void testAdminCanListGraphsWithoutGraphReadRole() {
+ GraphManager manager = managerWithLoadedDefaultGraph(ADMIN, true);
+ setContext(ADMIN);
+
+ SecurityContext securityContext = Mockito.mock(SecurityContext.class);
+ Mockito.when(securityContext.isUserInRole(Mockito.anyString()))
+ .thenReturn(false);
+
+ @SuppressWarnings("unchecked")
+ Map<String, Object> result = (Map<String, Object>) new GraphsAPI()
+ .list(manager, "DEFAULT", securityContext);
+
+ Assert.assertEquals(Collections.singleton("hugegraph"),
+ result.get("graphs"));
+ }
+
+ private static GraphManager managerWithDefaultRoleContext(String operator,
+ boolean admin) {
+ AuthManager authManager = Mockito.mock(AuthManager.class);
+ Mockito.when(authManager.isAdminManager(operator)).thenReturn(admin);
+ Mockito.when(authManager.isSpaceManager(GRAPHSPACE, operator))
+ .thenReturn(!admin);
+ Mockito.when(authManager.isDefaultRole(GRAPHSPACE, TARGET,
+ HugeDefaultRole.SPACE))
+ .thenReturn(true);
+ Mockito.when(authManager.findUser(TARGET))
+ .thenReturn(new HugeUser(TARGET));
+
+ HugeAuthenticator authenticator = Mockito.mock(HugeAuthenticator.class);
+ Mockito.when(authenticator.authManager()).thenReturn(authManager);
+
+ GraphManager manager = allocateGraphManager();
+ Whitebox.setInternalState(manager, "PDExist", true);
+ Whitebox.setInternalState(manager, "authenticator", authenticator);
+ Map<String, GraphSpace> spaces = new ConcurrentHashMap<>();
+ spaces.put(GRAPHSPACE, new GraphSpace(GRAPHSPACE));
+ Whitebox.setInternalState(manager, "graphSpaces", spaces);
+
+ MetaManager metaManager = Mockito.mock(MetaManager.class);
+ Mockito.when(metaManager.graphConfigs(GRAPHSPACE))
+ .thenReturn(Collections.emptyMap());
+ Whitebox.setInternalState(manager, "metaManager", metaManager);
+
+ Map<String, Graph> graphs = new ConcurrentHashMap<>();
+ graphs.put(GRAPHSPACE + "-" + GRAPH, Mockito.mock(HugeGraph.class));
+ Whitebox.setInternalState(manager, "graphs", graphs);
+
+ Mockito.when(authManager.isDefaultRole(GRAPHSPACE, GRAPH, TARGET,
+ HugeDefaultRole.OBSERVER))
+ .thenReturn(true);
+ return manager;
+ }
+
+ private static GraphManager managerWithLoadedDefaultGraph(String operator,
+ boolean admin) {
+ GraphManager manager = allocateGraphManager();
+ Whitebox.setInternalState(manager, "PDExist", true);
+ Whitebox.setInternalState(manager, "localGraphs",
+ Collections.singleton("hugegraph"));
+ MetaManager metaManager = Mockito.mock(MetaManager.class);
+ Mockito.when(metaManager.graphConfigs("DEFAULT"))
+ .thenReturn(Collections.emptyMap());
+ Whitebox.setInternalState(manager, "metaManager", metaManager);
+
+ AuthManager authManager = Mockito.mock(AuthManager.class);
+ Mockito.when(authManager.isAdminManager(operator)).thenReturn(admin);
+ HugeAuthenticator authenticator = Mockito.mock(HugeAuthenticator.class);
+ Mockito.when(authenticator.authManager()).thenReturn(authManager);
+ Whitebox.setInternalState(manager, "authenticator", authenticator);
+
+ Map<String, GraphSpace> spaces = new ConcurrentHashMap<>();
+ spaces.put("DEFAULT", new GraphSpace("DEFAULT"));
+ Whitebox.setInternalState(manager, "graphSpaces", spaces);
+
+ Map<String, Graph> graphs = new ConcurrentHashMap<>();
+ graphs.put("DEFAULT-hugegraph", Mockito.mock(HugeGraph.class));
+ Whitebox.setInternalState(manager, "graphs", graphs);
+ return manager;
+ }
+
+ private static GraphManager allocateGraphManager() {
+ try {
+ Field field = Unsafe.class.getDeclaredField("theUnsafe");
+ field.setAccessible(true);
+ Unsafe unsafe = (Unsafe) field.get(null);
+ return (GraphManager) unsafe.allocateInstance(GraphManager.class);
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private static void setContext(String username) {
+ try {
+ HugeAuthenticator.User user = new HugeAuthenticator.User(
+ username, RolePermission.admin());
+ HugeGraphAuthProxy.Context context =
+ new HugeGraphAuthProxy.Context(user);
+ Method method = HugeGraphAuthProxy.class.getDeclaredMethod(
+ "setContext", HugeGraphAuthProxy.Context.class);
+ method.setAccessible(true);
+ method.invoke(null, context);
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java
index 1bff9b29d..e6520b018 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java
@@ -19,14 +19,23 @@ package org.apache.hugegraph.unit.auth;
import java.lang.reflect.Method;
+import org.apache.hugegraph.HugeGraph;
+import org.apache.hugegraph.auth.AuthManager;
import org.apache.hugegraph.auth.HugeAuthenticator;
+import org.apache.hugegraph.auth.HugeDefaultRole;
import org.apache.hugegraph.auth.HugeGraphAuthProxy;
import org.apache.hugegraph.auth.RolePermission;
+import org.apache.hugegraph.auth.UserWithRole;
+import org.apache.hugegraph.backend.id.IdGenerator;
+import org.apache.hugegraph.config.AuthOptions;
+import org.apache.hugegraph.config.HugeConfig;
import org.apache.hugegraph.task.TaskManager;
+import org.apache.hugegraph.task.TaskScheduler;
import org.apache.hugegraph.testutil.Assert;
import org.apache.hugegraph.unit.BaseUnitTest;
import org.junit.After;
import org.junit.Test;
+import org.mockito.Mockito;
public class HugeGraphAuthProxyTest extends BaseUnitTest {
@@ -195,4 +204,44 @@ public class HugeGraphAuthProxyTest extends BaseUnitTest {
// username() should now return "anonymous"
Assert.assertEquals("anonymous", HugeGraphAuthProxy.username());
}
+
+ @Test
+ public void testDefaultRoleMutationInvalidatesUserRoleCache()
+ throws Exception {
+ HugeGraph graph = Mockito.mock(HugeGraph.class);
+ HugeConfig config = Mockito.mock(HugeConfig.class);
+ AuthManager authManager = Mockito.mock(AuthManager.class);
+ TaskScheduler scheduler = Mockito.mock(TaskScheduler.class);
+
+ Mockito.when(graph.spaceGraphName()).thenReturn("hugegraph");
+ Mockito.when(graph.configuration()).thenReturn(config);
+ Mockito.when(graph.authManager()).thenReturn(authManager);
+ Mockito.when(graph.taskScheduler()).thenReturn(scheduler);
+ Mockito.when(config.get(AuthOptions.AUTH_CACHE_EXPIRE))
+ .thenReturn(3600L);
+ Mockito.when(config.get(AuthOptions.AUTH_CACHE_CAPACITY))
+ .thenReturn(100L);
+ Mockito.when(config.get(AuthOptions.AUTH_AUDIT_LOG_RATE))
+ .thenReturn(1000D);
+ Mockito.when(authManager.validateUser("cache_user", "pass"))
+ .thenReturn(new UserWithRole("cache_user"));
+ Mockito.when(authManager.createDefaultRole("DEFAULT", "cache_user",
+ HugeDefaultRole.ANALYST,
+ "hugegraph"))
+ .thenReturn(IdGenerator.of("default-role"));
+
+ HugeGraphAuthProxy proxy = new HugeGraphAuthProxy(graph);
+ AuthManager proxyAuthManager = proxy.authManager();
+
+ proxyAuthManager.validateUser("cache_user", "pass");
+ proxyAuthManager.validateUser("cache_user", "pass");
+ Mockito.verify(authManager, Mockito.times(1))
+ .validateUser("cache_user", "pass");
+
+ proxyAuthManager.createDefaultRole("DEFAULT", "cache_user",
+ HugeDefaultRole.ANALYST, "hugegraph");
+ proxyAuthManager.validateUser("cache_user", "pass");
+ Mockito.verify(authManager, Mockito.times(2))
+ .validateUser("cache_user", "pass");
+ }
}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CachedSchemaTransactionTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CachedSchemaTransactionTest.java
index 9921670ee..9e7aec384 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CachedSchemaTransactionTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CachedSchemaTransactionTest.java
@@ -397,16 +397,15 @@ public class CachedSchemaTransactionTest extends BaseUnitTest {
String graphName = "DEFAULT-unit-test-v2";
String otherGraphName = "DEFAULT-other-v2";
- Cache<Id, Object> idCache = CacheManager.instance()
- .cache("schema-id-" +
- graphName, 10L);
- Cache<Id, Object> nameCache = CacheManager.instance()
- .cache("schema-name-" +
- graphName, 10L);
- Cache<Id, Object> otherIdCache = CacheManager.instance()
- .cache("schema-id-" +
- otherGraphName,
- 10L);
+ Cache<Id, Object> idCache = v2IdCache(graphName);
+ Cache<Id, Object> nameCache = v2NameCache(graphName);
+ Cache<Id, Object> otherIdCache = v2IdCache(otherGraphName);
+ Cache<Id, Object> v1IdCache = CacheManager.instance()
+ .cache("schema-id-" +
+ graphName, 10L);
+ Cache<Id, Object> v1NameCache = CacheManager.instance()
+ .cache("schema-name-" +
+ graphName, 10L);
Object arrayCaches = idCache.attachment(newV2SchemaCaches(10));
Id arrayCacheId = IdGenerator.of(1);
SchemaElement arrayCacheSchema =
@@ -420,10 +419,16 @@ public class CachedSchemaTransactionTest extends BaseUnitTest {
idCache.update(IdGenerator.of(1), "fake-pk-by-id");
nameCache.update(IdGenerator.of("fake-pk"), "fake-pk-by-name");
otherIdCache.update(IdGenerator.of(2), "other-pk-by-id");
+ v1IdCache.update(IdGenerator.of(3), "v1-pk-by-id");
+ v1NameCache.update(IdGenerator.of("v1-pk"), "v1-pk-by-name");
+ Assert.assertNotSame(v1IdCache, idCache);
+ Assert.assertNotSame(v1NameCache, nameCache);
Assert.assertEquals(1L, idCache.size());
Assert.assertEquals(1L, nameCache.size());
Assert.assertEquals(1L, otherIdCache.size());
+ Assert.assertEquals(1L, v1IdCache.size());
+ Assert.assertEquals(1L, v1NameCache.size());
Assert.assertSame(arrayCacheSchema,
getV2SchemaCache(arrayCaches,
HugeType.PROPERTY_KEY,
@@ -436,6 +441,8 @@ public class CachedSchemaTransactionTest extends BaseUnitTest {
Assert.assertEquals(0L, idCache.size());
Assert.assertEquals(0L, nameCache.size());
Assert.assertEquals(1L, otherIdCache.size());
+ Assert.assertEquals(1L, v1IdCache.size());
+ Assert.assertEquals(1L, v1NameCache.size());
Assert.assertNull(getV2SchemaCache(arrayCaches,
HugeType.PROPERTY_KEY,
arrayCacheId));
@@ -444,9 +451,28 @@ public class CachedSchemaTransactionTest extends BaseUnitTest {
idCache.clear();
nameCache.clear();
otherIdCache.clear();
+ v1IdCache.clear();
+ v1NameCache.clear();
}
}
+ private static Cache<Id, Object> v2IdCache(String graphName) {
+ return v2Cache("ID_CACHE_PREFIX", graphName);
+ }
+
+ private static Cache<Id, Object> v2NameCache(String graphName) {
+ return v2Cache("NAME_CACHE_PREFIX", graphName);
+ }
+
+ private static Cache<Id, Object> v2Cache(String prefixField,
+ String graphName) {
+ // Keep test fixtures aligned with the private cache namespace used by
+ // CachedSchemaTransactionV2 without duplicating production literals.
+ String prefix = Whitebox.getInternalState(
+ CachedSchemaTransactionV2.class, prefixField);
+ return CacheManager.instance().cache(prefix + "-" + graphName, 10L);
+ }
+
private static Object newV2SchemaCaches(int size) {
for (Class<?> clazz :
CachedSchemaTransactionV2.class.getDeclaredClasses()) {
@@ -534,10 +560,8 @@ public class CachedSchemaTransactionTest extends BaseUnitTest {
// IntObjectMap (pks/vls/els/ils) inside the array attachment so
// stale entries are not served after a meta event.
String graphName = "DEFAULT-unit-test-v2-array";
- Cache<Id, Object> idCache =
- CacheManager.instance().cache("schema-id-" + graphName, 10L);
- Cache<Id, Object> nameCache =
- CacheManager.instance().cache("schema-name-" + graphName, 10L);
+ Cache<Id, Object> idCache = v2IdCache(graphName);
+ Cache<Id, Object> nameCache = v2NameCache(graphName);
// Size must comfortably exceed the largest id below: IntObjectMap
// grows by doubling and refuses to write past currentSize even after
// a single expansion, so a small capacity rejects mid-range keys.
@@ -598,11 +622,8 @@ public class CachedSchemaTransactionTest extends BaseUnitTest {
public void testHandleSchemaCacheClearEventSkipsLocalSource()
throws Exception {
String graphName = "DEFAULT-meta-local-source-v2";
- Cache<Id, Object> idCache =
- CacheManager.instance().cache("schema-id-" + graphName, 10L);
- Cache<Id, Object> nameCache =
- CacheManager.instance()
- .cache("schema-name-" + graphName, 10L);
+ Cache<Id, Object> idCache = v2IdCache(graphName);
+ Cache<Id, Object> nameCache = v2NameCache(graphName);
MetaDriver mockDriver = Mockito.mock(MetaDriver.class);
Object localResponse = new Object();
@@ -658,13 +679,9 @@ public class CachedSchemaTransactionTest extends BaseUnitTest {
String targetGraph = "DEFAULT-meta-target-v2";
String otherGraph = "DEFAULT-meta-other-v2";
- Cache<Id, Object> targetIdCache =
- CacheManager.instance().cache("schema-id-" + targetGraph, 10L);
- Cache<Id, Object> targetNameCache =
- CacheManager.instance()
- .cache("schema-name-" + targetGraph, 10L);
- Cache<Id, Object> otherIdCache =
- CacheManager.instance().cache("schema-id-" + otherGraph, 10L);
+ Cache<Id, Object> targetIdCache = v2IdCache(targetGraph);
+ Cache<Id, Object> targetNameCache = v2NameCache(targetGraph);
+ Cache<Id, Object> otherIdCache = v2IdCache(otherGraph);
MetaDriver mockDriver = Mockito.mock(MetaDriver.class);
Object response = new Object();
@@ -703,8 +720,7 @@ public class CachedSchemaTransactionTest extends BaseUnitTest {
// A response that yields no graph names (extractor returns null) must
// be a strict noop: caches stay populated.
String graphName = "DEFAULT-meta-noop-v2";
- Cache<Id, Object> idCache =
- CacheManager.instance().cache("schema-id-" + graphName, 10L);
+ Cache<Id, Object> idCache = v2IdCache(graphName);
MetaDriver mockDriver = Mockito.mock(MetaDriver.class);
Object response = new Object();
@@ -735,10 +751,8 @@ public class CachedSchemaTransactionTest extends BaseUnitTest {
// them must have its V2 caches cleared.
String graphA = "DEFAULT-meta-multi-a";
String graphB = "DEFAULT-meta-multi-b";
- Cache<Id, Object> idA =
- CacheManager.instance().cache("schema-id-" + graphA, 10L);
- Cache<Id, Object> idB =
- CacheManager.instance().cache("schema-id-" + graphB, 10L);
+ Cache<Id, Object> idA = v2IdCache(graphA);
+ Cache<Id, Object> idB = v2IdCache(graphB);
MetaDriver mockDriver = Mockito.mock(MetaDriver.class);
Object response = new Object();
@@ -811,11 +825,8 @@ public class CachedSchemaTransactionTest extends BaseUnitTest {
// MetaDriver, then invoke it as the watch would and assert the V2
// caches for the named graph are cleared.
String graphName = "DEFAULT-end-to-end-v2";
- Cache<Id, Object> idCache =
- CacheManager.instance().cache("schema-id-" + graphName, 10L);
- Cache<Id, Object> nameCache =
- CacheManager.instance()
- .cache("schema-name-" + graphName, 10L);
+ Cache<Id, Object> idCache = v2IdCache(graphName);
+ Cache<Id, Object> nameCache = v2NameCache(graphName);
MetaDriver mockDriver = Mockito.mock(MetaDriver.class);
Object response = new Object();
diff --git a/hugegraph-server/hugegraph-test/src/test/java/org/apache/hugegraph/dist/HugeGraphServerTest.java b/hugegraph-server/hugegraph-test/src/test/java/org/apache/hugegraph/dist/HugeGraphServerTest.java
new file mode 100644
index 000000000..dafe38caa
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/test/java/org/apache/hugegraph/dist/HugeGraphServerTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hugegraph.dist;
+
+import java.util.concurrent.CompletableFuture;
+
+import org.apache.hugegraph.server.RestServer;
+import org.apache.tinkerpop.gremlin.server.GremlinServer;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class HugeGraphServerTest {
+
+ @Test
+ public void testRollbackStartupStopsAllStartedResources() {
+ MemoryMonitor monitor = Mockito.mock(MemoryMonitor.class);
+ GremlinServer gremlinServer = Mockito.mock(GremlinServer.class);
+ RestServer restServer = Mockito.mock(RestServer.class);
+ Mockito.when(gremlinServer.stop()).thenReturn(
+ CompletableFuture.completedFuture(null));
+ Mockito.when(restServer.shutdown()).thenReturn(
+ CompletableFuture.completedFuture(null));
+
+ HugeGraphServer.rollbackStartup(monitor, gremlinServer, restServer);
+
+ Mockito.verify(monitor).stop();
+ Mockito.verify(gremlinServer).stop();
+ Mockito.verify(restServer).shutdown();
+ }
+
+ @Test
+ public void testRollbackStartupAfterMonitorConstructionFailure() {
+ GremlinServer gremlinServer = Mockito.mock(GremlinServer.class);
+ RestServer restServer = Mockito.mock(RestServer.class);
+ Mockito.when(gremlinServer.stop()).thenReturn(
+ CompletableFuture.completedFuture(null));
+ Mockito.when(restServer.shutdown()).thenReturn(
+ CompletableFuture.completedFuture(null));
+
+ HugeGraphServer.rollbackStartup(null, gremlinServer, restServer);
+
+ Mockito.verify(gremlinServer).stop();
+ Mockito.verify(restServer).shutdown();
+ }
+
+ @Test
+ public void testRollbackStartupContinuesAfterStopFailure() {
+ MemoryMonitor monitor = Mockito.mock(MemoryMonitor.class);
+ GremlinServer gremlinServer = Mockito.mock(GremlinServer.class);
+ RestServer restServer = Mockito.mock(RestServer.class);
+ Mockito.doThrow(new RuntimeException("monitor stop failed"))
+ .when(monitor).stop();
+ Mockito.when(gremlinServer.stop())
+ .thenThrow(new RuntimeException("gremlin stop failed"));
+ Mockito.when(restServer.shutdown()).thenReturn(
+ CompletableFuture.completedFuture(null));
+
+ HugeGraphServer.rollbackStartup(monitor, gremlinServer, restServer);
+
+ Mockito.verify(monitor).stop();
+ Mockito.verify(gremlinServer).stop();
+ Mockito.verify(restServer).shutdown();
+ }
+}
diff --git a/hugegraph-server/hugegraph-test/src/test/java/org/apache/hugegraph/unit/config/ServerOptionsTest.java b/hugegraph-server/hugegraph-test/src/test/java/org/apache/hugegraph/unit/config/ServerOptionsTest.java
index 6113c748b..53a9a7f83 100644
--- a/hugegraph-server/hugegraph-test/src/test/java/org/apache/hugegraph/unit/config/ServerOptionsTest.java
+++ b/hugegraph-server/hugegraph-test/src/test/java/org/apache/hugegraph/unit/config/ServerOptionsTest.java
@@ -18,6 +18,7 @@
package org.apache.hugegraph.unit.config;
import org.apache.commons.configuration2.PropertiesConfiguration;
+import org.apache.hugegraph.config.ConfigException;
import org.apache.hugegraph.config.HugeConfig;
import org.apache.hugegraph.config.OptionSpace;
import org.apache.hugegraph.config.ServerOptions;
@@ -119,4 +120,25 @@ public class ServerOptionsTest {
Assert.assertEquals("https://127.0.0.1:8080",
config.get(ServerOptions.REST_SERVER_URL));
}
+
+ @Test
+ public void testMemoryMonitorPeriodMustBePositive() {
+ PropertiesConfiguration conf = new PropertiesConfiguration();
+ conf.setProperty("memory_monitor.period", "0");
+
+ Assert.assertThrows(ConfigException.class, () -> {
+ new HugeConfig(conf);
+ });
+ }
+
+ @Test
+ public void testMemoryMonitorThresholdOneIsValid() {
+ PropertiesConfiguration conf = new PropertiesConfiguration();
+ conf.setProperty("memory_monitor.threshold", "1.0");
+
+ HugeConfig config = new HugeConfig(conf);
+
+ Assert.assertEquals(1.0D,
+ config.get(ServerOptions.JVM_MEMORY_MONITOR_THRESHOLD));
+ }
}