This is an automated email from the ASF dual-hosted git repository.
roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 989ab1f2f7 [#12379] feat(core): Support assignment values in REST and
Java client (#12384)
989ab1f2f7 is described below
commit 989ab1f2f78bff4424d7d764d1f6af0cc6b62fc9
Author: roryqi <[email protected]>
AuthorDate: Wed Aug 12 10:57:43 2026 +0800
[#12379] feat(core): Support assignment values in REST and Java client
(#12384)
### What changes were proposed in this pull request?
Add REST and Java client support for tag assignment values:
- Support creating tags with value constraints.
- Support associating and removing tag assignment values.
- Support retrieving assignment values from associated tags.
- Support listing associated metadata objects by assignment value.
- Use the v2 REST media type for valued tag association requests while
retaining v1 behavior for legacy requests.
### Why are the changes needed?
The tag assignment value APIs added in the API and core layers need
corresponding REST endpoints and Java client implementations.
Fix: #12379
### Does this PR introduce _any_ user-facing change?
Yes. Java client users can create tags with value constraints, associate
values with tags, retrieve assigned values, and filter associated
objects by value.
### How was this patch tested?
- Added REST unit tests for version handling, authorization, tag
creation, association, removal, and value filtering.
- Added Java client unit tests for constraints, assignments, valued
requests, and value filtering.
- Added and passed `TagIT.testAssignmentValues`.
- Passed the focused server and Java client regression tests.
---
.../apache/gravitino/client/BaseSchemaCatalog.java | 6 +
.../org/apache/gravitino/client/GenericColumn.java | 6 +
.../apache/gravitino/client/GenericFileset.java | 6 +
.../apache/gravitino/client/GenericFunction.java | 6 +
.../org/apache/gravitino/client/GenericModel.java | 6 +
.../org/apache/gravitino/client/GenericSchema.java | 6 +
.../org/apache/gravitino/client/GenericTag.java | 39 ++++++
.../org/apache/gravitino/client/GenericTopic.java | 6 +
.../org/apache/gravitino/client/GenericView.java | 6 +
.../apache/gravitino/client/GravitinoClient.java | 11 ++
.../apache/gravitino/client/GravitinoMetalake.java | 41 ++++++-
.../client/MetadataObjectTagOperations.java | 26 ++++
.../apache/gravitino/client/RelationalTable.java | 6 +
.../apache/gravitino/client/TestGenericTag.java | 24 ++++
.../gravitino/client/TestGravitinoMetalake.java | 36 ++++++
.../apache/gravitino/client/TestSupportTags.java | 22 ++++
.../gravitino/client/integration/test/TagIT.java | 50 ++++++++
.../dto/requests/TagValuesAssociateRequest.java | 56 +++++++--
.../requests/TestTagValuesAssociateRequest.java | 40 +++++++
.../storage/relational/RelationalEntityStore.java | 36 +++---
.../storage/relational/service/TagMetaService.java | 8 +-
.../java/org/apache/gravitino/tag/TagManager.java | 64 ++++++++--
.../relational/TestRelationalEntityStore.java | 60 ++++++----
.../org/apache/gravitino/tag/TestTagManager.java | 48 ++++++++
design-docs/tag-assignment-values.md | 7 +-
docs/open-api/tags.yaml | 118 ++++++++++++++++++-
.../apache/gravitino/server/web/ApiVersion.java | 11 +-
.../gravitino/server/web/VersioningFilter.java | 80 +++++++++----
.../AssociateTagAuthorizationExecutor.java | 48 ++++++--
.../web/rest/MetadataObjectTagOperations.java | 120 +++++++++++++++----
.../gravitino/server/web/rest/TagOperations.java | 37 +++++-
.../gravitino/server/web/TestApiVersion.java | 11 +-
.../gravitino/server/web/TestVersioningFilter.java | 82 +++++++++++--
.../filter/TestGravitinoInterceptionService.java | 93 +++++++++++++++
.../TestAssociateTagAuthorizationExecutor.java | 128 ++++++++++++++++++++
.../web/rest/TestMetadataObjectTagOperations.java | 131 ++++++++++++++++++++-
.../server/web/rest/TestTagOperations.java | 52 ++++++--
37 files changed, 1377 insertions(+), 156 deletions(-)
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java
index 0553abfc4b..636090dba0 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java
@@ -58,6 +58,7 @@ import org.apache.gravitino.policy.SupportsPolicies;
import org.apache.gravitino.rest.RESTUtils;
import org.apache.gravitino.tag.SupportsTags;
import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagValue;
/**
* BaseSchemaCatalog is the base abstract class for all the catalog with
schema. It provides the
@@ -288,6 +289,11 @@ abstract class BaseSchemaCatalog extends CatalogDTO
return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
}
+ @Override
+ public String[] associateTags(TagValue[] tagsToAdd, TagValue[] tagsToRemove)
{
+ return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
+ }
+
@Override
public String[] listPolicies() {
return objectPolicyOperations.listPolicies();
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericColumn.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericColumn.java
index aacf022e95..c7845db865 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericColumn.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericColumn.java
@@ -29,6 +29,7 @@ import org.apache.gravitino.rel.expressions.Expression;
import org.apache.gravitino.rel.types.Type;
import org.apache.gravitino.tag.SupportsTags;
import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagValue;
/** Represents a generic column. */
public class GenericColumn implements Column, SupportsTags {
@@ -78,6 +79,11 @@ public class GenericColumn implements Column, SupportsTags {
return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
}
+ @Override
+ public String[] associateTags(TagValue[] tagsToAdd, TagValue[] tagsToRemove)
{
+ return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
+ }
+
@Override
public String name() {
return internalColumn.name();
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java
index a6963a6a44..d51e4fea37 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java
@@ -38,6 +38,7 @@ import org.apache.gravitino.policy.Policy;
import org.apache.gravitino.policy.SupportsPolicies;
import org.apache.gravitino.tag.SupportsTags;
import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagValue;
/** Represents a generic fileset. */
class GenericFileset
@@ -136,6 +137,11 @@ class GenericFileset
return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
}
+ @Override
+ public String[] associateTags(TagValue[] tagsToAdd, TagValue[] tagsToRemove)
{
+ return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
+ }
+
@Override
public String[] listPolicies() {
return objectPolicyOperations.listPolicies();
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFunction.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFunction.java
index fd3bcb459b..2bb58b7ec3 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFunction.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFunction.java
@@ -32,6 +32,7 @@ import org.apache.gravitino.function.FunctionDefinition;
import org.apache.gravitino.function.FunctionType;
import org.apache.gravitino.tag.SupportsTags;
import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagValue;
/** Represents a generic function. */
class GenericFunction implements Function, SupportsTags {
@@ -106,6 +107,11 @@ class GenericFunction implements Function, SupportsTags {
return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
}
+ @Override
+ public String[] associateTags(TagValue[] tagsToAdd, TagValue[] tagsToRemove)
{
+ return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
+ }
+
@Override
public boolean equals(Object obj) {
if (this == obj) {
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericModel.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericModel.java
index 076ed38143..14dc61a057 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericModel.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericModel.java
@@ -36,6 +36,7 @@ import org.apache.gravitino.policy.Policy;
import org.apache.gravitino.policy.SupportsPolicies;
import org.apache.gravitino.tag.SupportsTags;
import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagValue;
/** Represents a generic model. */
class GenericModel implements Model, SupportsTags, SupportsPolicies {
@@ -135,6 +136,11 @@ class GenericModel implements Model, SupportsTags,
SupportsPolicies {
return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
}
+ @Override
+ public String[] associateTags(TagValue[] tagsToAdd, TagValue[] tagsToRemove)
{
+ return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
+ }
+
@Override
public String[] listPolicies() {
return objectPolicyOperations.listPolicies();
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java
index bbb5f84300..09ecbc8916 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java
@@ -33,6 +33,7 @@ import org.apache.gravitino.policy.Policy;
import org.apache.gravitino.policy.SupportsPolicies;
import org.apache.gravitino.tag.SupportsTags;
import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagValue;
/** Represents a generic schema. */
class GenericSchema implements Schema, SupportsTags, SupportsRoles,
SupportsPolicies {
@@ -109,6 +110,11 @@ class GenericSchema implements Schema, SupportsTags,
SupportsRoles, SupportsPoli
return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
}
+ @Override
+ public String[] associateTags(TagValue[] tagsToAdd, TagValue[] tagsToRemove)
{
+ return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
+ }
+
@Override
public String[] listPolicies() {
return objectPolicyOperations.listPolicies();
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericTag.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericTag.java
index 52bfc4db37..6bf7921e39 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericTag.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericTag.java
@@ -18,19 +18,26 @@
*/
package org.apache.gravitino.client;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
+import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Audit;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.dto.responses.MetadataObjectListResponse;
import org.apache.gravitino.dto.tag.TagDTO;
import org.apache.gravitino.rest.RESTUtils;
import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagAssignment;
+import org.apache.gravitino.tag.TagValueConstraint;
/** Represents a generic tag. */
class GenericTag implements Tag, Tag.AssociatedObjects {
+ private static final int MAX_TAG_VALUE_LENGTH = 256;
+
private final TagDTO tagDTO;
private final RESTClient restClient;
@@ -58,6 +65,16 @@ class GenericTag implements Tag, Tag.AssociatedObjects {
return tagDTO.properties();
}
+ @Override
+ public TagValueConstraint valueConstraint() {
+ return tagDTO.valueConstraint();
+ }
+
+ @Override
+ public Optional<TagAssignment> assignment() {
+ return tagDTO.assignment();
+ }
+
@Override
public Optional<Boolean> inherited() {
return tagDTO.inherited();
@@ -88,6 +105,28 @@ class GenericTag implements Tag, Tag.AssociatedObjects {
return resp.getMetadataObjects();
}
+ @Override
+ public MetadataObject[] objects(String value) {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(value), "Tag assignment value must not be null
or empty");
+ Preconditions.checkArgument(
+ value.length() <= MAX_TAG_VALUE_LENGTH,
+ "Tag assignment value must not exceed " + MAX_TAG_VALUE_LENGTH + "
characters");
+
+ MetadataObjectListResponse resp =
+ restClient.get(
+ String.format(
+ "api/metalakes/%s/tags/%s/objects",
+ RESTUtils.encodeString(metalake),
RESTUtils.encodeString(name())),
+ ImmutableMap.of("value", value),
+ MetadataObjectListResponse.class,
+ Collections.emptyMap(),
+ ErrorHandlers.tagErrorHandler());
+
+ resp.validate();
+ return resp.getMetadataObjects();
+ }
+
@Override
public boolean equals(Object obj) {
if (this == obj) {
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericTopic.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericTopic.java
index a040bbbd93..a69579d15f 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericTopic.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericTopic.java
@@ -35,6 +35,7 @@ import org.apache.gravitino.policy.Policy;
import org.apache.gravitino.policy.SupportsPolicies;
import org.apache.gravitino.tag.SupportsTags;
import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagValue;
/** Represents a generic topic. */
class GenericTopic implements Topic, SupportsTags, SupportsRoles,
SupportsPolicies {
@@ -113,6 +114,11 @@ class GenericTopic implements Topic, SupportsTags,
SupportsRoles, SupportsPolici
return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
}
+ @Override
+ public String[] associateTags(TagValue[] tagsToAdd, TagValue[] tagsToRemove)
{
+ return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
+ }
+
@Override
public String[] listPolicies() {
return objectPolicyOperations.listPolicies();
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericView.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericView.java
index a38228806a..aa774f7539 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericView.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericView.java
@@ -33,6 +33,7 @@ import org.apache.gravitino.rel.Representation;
import org.apache.gravitino.rel.View;
import org.apache.gravitino.tag.SupportsTags;
import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagValue;
/** Represents a generic view. */
class GenericView implements View, SupportsTags {
@@ -116,6 +117,11 @@ class GenericView implements View, SupportsTags {
return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
}
+ @Override
+ public String[] associateTags(TagValue[] tagsToAdd, TagValue[] tagsToRemove)
{
+ return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
+ }
+
@Override
public boolean equals(Object obj) {
if (this == obj) {
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
index 16b222ba8d..cb8afa01a5 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
@@ -70,6 +70,7 @@ import org.apache.gravitino.policy.PolicyOperations;
import org.apache.gravitino.tag.Tag;
import org.apache.gravitino.tag.TagChange;
import org.apache.gravitino.tag.TagOperations;
+import org.apache.gravitino.tag.TagValueConstraint;
/**
* Apache Gravitino Client for a user to interact with the Gravitino API,
allowing the client to
@@ -595,6 +596,16 @@ public class GravitinoClient extends GravitinoClientBase
return getMetalake().createTag(name, comment, properties);
}
+ @Override
+ public Tag createTag(
+ String name,
+ String comment,
+ Map<String, String> properties,
+ TagValueConstraint valueConstraint)
+ throws TagAlreadyExistsException {
+ return getMetalake().createTag(name, comment, properties, valueConstraint);
+ }
+
@Override
public Tag alterTag(String name, TagChange... changes)
throws NoSuchTagException, IllegalArgumentException,
TagAlreadyExistsException {
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
index f88a33a227..eed967128d 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
@@ -131,6 +131,7 @@ import org.apache.gravitino.rest.RESTUtils;
import org.apache.gravitino.tag.Tag;
import org.apache.gravitino.tag.TagChange;
import org.apache.gravitino.tag.TagOperations;
+import org.apache.gravitino.tag.TagValueConstraint;
/**
* Apache Gravitino Metalake is the top-level metadata repository for users.
It contains a list of
@@ -510,8 +511,46 @@ public class GravitinoMetalake extends MetalakeDTO
@Override
public Tag createTag(String name, String comment, Map<String, String>
properties)
throws TagAlreadyExistsException {
+ return createTag(name, comment, properties, TagValueConstraint.anyValue());
+ }
+
+ /**
+ * Create a tag under the current metalake with an assignment value
constraint.
+ *
+ * @param name The name of the tag.
+ * @param comment The comment of the tag.
+ * @param properties The properties of the tag.
+ * @param valueConstraint The assignment value constraint of the tag.
+ * @return The created tag.
+ * @throws TagAlreadyExistsException If the tag already exists.
+ */
+ @Override
+ public Tag createTag(
+ String name,
+ String comment,
+ Map<String, String> properties,
+ TagValueConstraint valueConstraint)
+ throws TagAlreadyExistsException {
Preconditions.checkArgument(StringUtils.isNotBlank(name), "tag name must
not be null or empty");
- TagCreateRequest req = new TagCreateRequest(name, comment, properties);
+ TagValueConstraint normalizedConstraint =
+ valueConstraint == null ? TagValueConstraint.anyValue() :
valueConstraint;
+ String[] allowedValues;
+ switch (normalizedConstraint.type()) {
+ case ANY_VALUE:
+ allowedValues = null;
+ break;
+ case NO_VALUE:
+ allowedValues = new String[0];
+ break;
+ case ALLOWED_VALUES:
+ allowedValues = normalizedConstraint.allowedValues();
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown tag value constraint: " + normalizedConstraint.type());
+ }
+
+ TagCreateRequest req = new TagCreateRequest(name, comment, properties,
allowedValues);
req.validate();
TagResponse resp =
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/MetadataObjectTagOperations.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/MetadataObjectTagOperations.java
index 29ec00512b..c504c9a212 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/MetadataObjectTagOperations.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/MetadataObjectTagOperations.java
@@ -25,6 +25,7 @@ import java.util.Collections;
import java.util.Locale;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.dto.requests.TagValuesAssociateRequest;
import org.apache.gravitino.dto.requests.TagsAssociateRequest;
import org.apache.gravitino.dto.responses.NameListResponse;
import org.apache.gravitino.dto.responses.TagListResponse;
@@ -33,6 +34,8 @@ import org.apache.gravitino.exceptions.NoSuchTagException;
import org.apache.gravitino.rest.RESTUtils;
import org.apache.gravitino.tag.SupportsTags;
import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagValue;
+import org.apache.hc.core5.http.HttpHeaders;
/**
* The implementation of {@link SupportsTags}. This interface will be
composited into catalog,
@@ -40,6 +43,8 @@ import org.apache.gravitino.tag.Tag;
*/
class MetadataObjectTagOperations implements SupportsTags {
+ private static final String TAG_VALUES_MEDIA_TYPE =
"application/vnd.gravitino.v2+json";
+
private final String metalakeName;
private final RESTClient restClient;
@@ -118,4 +123,25 @@ class MetadataObjectTagOperations implements SupportsTags {
resp.validate();
return resp.getNames();
}
+
+ @Override
+ public String[] associateTags(TagValue[] tagsToAdd, TagValue[] tagsToRemove)
{
+ TagValuesAssociateRequest request = new
TagValuesAssociateRequest(tagsToAdd, tagsToRemove);
+ request.validate();
+
+ NameListResponse resp =
+ restClient.post(
+ tagRequestPath,
+ request,
+ NameListResponse.class,
+ ImmutableMap.of(
+ HttpHeaders.ACCEPT,
+ TAG_VALUES_MEDIA_TYPE,
+ HttpHeaders.CONTENT_TYPE,
+ TAG_VALUES_MEDIA_TYPE),
+ ErrorHandlers.tagErrorHandler());
+
+ resp.validate();
+ return resp.getNames();
+ }
}
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalTable.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalTable.java
index fac38e76ac..4b4b6c0aac 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalTable.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalTable.java
@@ -69,6 +69,7 @@ import org.apache.gravitino.stats.SupportsPartitionStatistics;
import org.apache.gravitino.stats.SupportsStatistics;
import org.apache.gravitino.tag.SupportsTags;
import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagValue;
/** Represents a relational table. */
class RelationalTable
@@ -392,6 +393,11 @@ class RelationalTable
return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
}
+ @Override
+ public String[] associateTags(TagValue[] tagsToAdd, TagValue[] tagsToRemove)
{
+ return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
+ }
+
@Override
public String[] listPolicies() {
return objectPolicyOperations.listPolicies();
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestGenericTag.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestGenericTag.java
index 20463ddb86..5af79c9def 100644
---
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestGenericTag.java
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestGenericTag.java
@@ -19,6 +19,7 @@
package org.apache.gravitino.client;
import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableMap;
import java.time.Instant;
import java.util.Collections;
import org.apache.gravitino.MetadataObject;
@@ -31,6 +32,7 @@ import org.apache.gravitino.dto.tag.MetadataObjectDTO;
import org.apache.gravitino.dto.tag.TagDTO;
import org.apache.gravitino.exceptions.NoSuchMetalakeException;
import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagValueConstraint;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.Method;
import org.junit.jupiter.api.AfterAll;
@@ -47,6 +49,8 @@ public class TestGenericTag extends TestBase {
.withName("tag1")
.withComment("comment1")
.withProperties(Collections.emptyMap())
+ .withAllowedValues(new String[] {"finance", "risk"})
+ .withAssignmentValues(new String[] {"finance"})
.withAudit(AuditDTO.builder().withCreator("test").withCreateTime(Instant.now()).build())
.build();
@@ -79,6 +83,16 @@ public class TestGenericTag extends TestBase {
gravitinoClient.close();
}
+ @Test
+ public void testValueConstraintAndAssignment() {
+ Tag tag = new GenericTag(tagDTO, gravitinoClient.restClient(),
metalakeName);
+
+ Assertions.assertEquals(
+ TagValueConstraint.ofAllowedValues("finance", "risk"),
tag.valueConstraint());
+ Assertions.assertTrue(tag.assignment().isPresent());
+ Assertions.assertArrayEquals(new String[] {"finance"},
tag.assignment().get().values());
+ }
+
@Test
public void testAssociatedObjects() throws JsonProcessingException {
Tag tag = new GenericTag(tagDTO, gravitinoClient.restClient(),
metalakeName);
@@ -121,6 +135,16 @@ public class TestGenericTag extends TestBase {
Assertions.assertEquals(object.type(), actualObject.type());
}
+ buildMockResource(
+ Method.GET, path, ImmutableMap.of("value", "finance"), null, resp,
HttpStatus.SC_OK);
+ MetadataObject[] valueObjects = tag.associatedObjects().objects("finance");
+ Assertions.assertEquals(objects.length, valueObjects.length);
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
tag.associatedObjects().objects(""));
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> tag.associatedObjects().objects(String.join("",
Collections.nCopies(257, "a"))));
+
// Test return empty array
buildMockResource(
Method.GET,
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoMetalake.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoMetalake.java
index 0484cbc6b6..09ba0761b7 100644
---
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoMetalake.java
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoMetalake.java
@@ -74,6 +74,7 @@ import org.apache.gravitino.policy.PolicyChange;
import org.apache.gravitino.policy.PolicyContents;
import org.apache.gravitino.tag.Tag;
import org.apache.gravitino.tag.TagChange;
+import org.apache.gravitino.tag.TagValueConstraint;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.Method;
import org.junit.jupiter.api.AfterAll;
@@ -579,6 +580,41 @@ public class TestGravitinoMetalake extends TestBase {
Assertions.assertNull(tag.comment());
Assertions.assertNull(tag.properties());
+ TagValueConstraint valueConstraint =
TagValueConstraint.ofAllowedValues("finance", "risk");
+ TagCreateRequest constrainedRequest =
+ new TagCreateRequest(tagName, null, null, new String[] {"finance",
"risk"});
+ TagDTO constrainedTagDTO =
+ TagDTO.builder()
+ .withName(tagName)
+ .withAllowedValues(new String[] {"finance", "risk"})
+ .withAudit(
+
AuditDTO.builder().withCreator("creator").withCreateTime(Instant.now()).build())
+ .build();
+ buildMockResource(
+ Method.POST,
+ path,
+ constrainedRequest,
+ new TagResponse(constrainedTagDTO),
+ HttpStatus.SC_OK);
+
+ Tag constrainedTag = gravitinoClient.createTag(tagName, null, null,
valueConstraint);
+ Assertions.assertEquals(valueConstraint, constrainedTag.valueConstraint());
+
+ TagValueConstraint noValueConstraint = TagValueConstraint.noValue();
+ TagCreateRequest noValueRequest = new TagCreateRequest(tagName, null,
null, new String[0]);
+ TagDTO noValueTagDTO =
+ TagDTO.builder()
+ .withName(tagName)
+ .withAllowedValues(new String[0])
+ .withAudit(
+
AuditDTO.builder().withCreator("creator").withCreateTime(Instant.now()).build())
+ .build();
+ buildMockResource(
+ Method.POST, path, noValueRequest, new TagResponse(noValueTagDTO),
HttpStatus.SC_OK);
+
+ Tag noValueTag = gravitinoClient.createTag(tagName, null, null,
noValueConstraint);
+ Assertions.assertEquals(noValueConstraint, noValueTag.valueConstraint());
+
// Test with null name
Throwable ex =
Assertions.assertThrows(
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportTags.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportTags.java
index 7ee356ebae..6f73848ed5 100644
---
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportTags.java
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportTags.java
@@ -42,6 +42,7 @@ import org.apache.gravitino.dto.rel.ColumnDTO;
import org.apache.gravitino.dto.rel.SQLRepresentationDTO;
import org.apache.gravitino.dto.rel.TableDTO;
import org.apache.gravitino.dto.rel.ViewDTO;
+import org.apache.gravitino.dto.requests.TagValuesAssociateRequest;
import org.apache.gravitino.dto.requests.TagsAssociateRequest;
import org.apache.gravitino.dto.responses.ErrorResponse;
import org.apache.gravitino.dto.responses.NameListResponse;
@@ -61,10 +62,14 @@ import org.apache.gravitino.rel.View;
import org.apache.gravitino.rel.types.Types;
import org.apache.gravitino.tag.SupportsTags;
import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagValue;
+import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.Method;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import org.mockserver.model.HttpRequest;
+import org.mockserver.verify.VerificationTimes;
public class TestSupportTags extends TestBase {
@@ -640,6 +645,23 @@ public class TestSupportTags extends TestBase {
String[] actualTags = supportsTags.associateTags(tagsToAdd, tagsToRemove);
Assertions.assertArrayEquals(tagsToAdd, actualTags);
+ TagValue[] tagValuesToAdd =
+ new TagValue[] {TagValue.noValue("tag1"), TagValue.of("tag2",
"finance")};
+ TagValue[] tagValuesToRemove = new TagValue[] {TagValue.of("tag3",
"risk")};
+ TagValuesAssociateRequest valueRequest =
+ new TagValuesAssociateRequest(tagValuesToAdd, tagValuesToRemove);
+ buildMockResource(Method.POST, path, valueRequest, resp, SC_OK);
+
+ String[] actualValueTags = supportsTags.associateTags(tagValuesToAdd,
tagValuesToRemove);
+ Assertions.assertArrayEquals(tagsToAdd, actualValueTags);
+ mockServer.verify(
+ HttpRequest.request(path)
+ .withMethod(Method.POST.name())
+ .withHeader(HttpHeaders.ACCEPT,
"application/vnd.gravitino.v2+json")
+ .withHeader(HttpHeaders.CONTENT_TYPE,
"application/vnd.gravitino.v2+json")
+ .withBody(MAPPER.writeValueAsString(valueRequest)),
+ VerificationTimes.once());
+
// Test throw internal error
ErrorResponse errorResp1 = ErrorResponse.internalError("mock error");
buildMockResource(Method.POST, path, request, errorResp1,
SC_INTERNAL_SERVER_ERROR);
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/TagIT.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/TagIT.java
index ffa59da404..676484a72f 100644
---
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/TagIT.java
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/TagIT.java
@@ -26,6 +26,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import org.apache.gravitino.Catalog;
import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Schema;
import org.apache.gravitino.client.GravitinoMetalake;
@@ -54,6 +55,8 @@ import org.apache.gravitino.rel.View;
import org.apache.gravitino.rel.types.Types;
import org.apache.gravitino.tag.Tag;
import org.apache.gravitino.tag.TagChange;
+import org.apache.gravitino.tag.TagValue;
+import org.apache.gravitino.tag.TagValueConstraint;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
@@ -298,6 +301,53 @@ public class TagIT extends BaseIT {
Assertions.assertEquals(tag2, tag3);
}
+ @Test
+ public void testAssignmentValues() {
+ String tagName = GravitinoITUtils.genRandomName("tag_it_assignment_value");
+ TagValueConstraint constraint =
TagValueConstraint.ofAllowedValues("finance", "risk");
+ Tag tag = metalake.createTag(tagName, "comment", Collections.emptyMap(),
constraint);
+ Assertions.assertEquals(constraint, tag.valueConstraint());
+
+ TagValue[] values =
+ new TagValue[] {TagValue.of(tagName, "finance"), TagValue.of(tagName,
"risk")};
+ Assertions.assertArrayEquals(
+ new String[] {tagName}, table.supportsTags().associateTags(values,
null));
+
+ Tag assignedTag = table.supportsTags().getTag(tagName);
+ Assertions.assertEquals(constraint, assignedTag.valueConstraint());
+ Assertions.assertTrue(assignedTag.assignment().isPresent());
+ Assertions.assertArrayEquals(valuesToStrings(values),
assignedTag.assignment().get().values());
+ Assertions.assertFalse(assignedTag.inherited().get());
+
+ MetadataObject expectedTable =
+ MetadataObjects.of(
+ relationalCatalog.name() + "." + schema.name(),
+ table.name(),
+ MetadataObject.Type.TABLE);
+ MetadataObject[] financeObjects =
tag.associatedObjects().objects("finance");
+ Assertions.assertEquals(1, financeObjects.length);
+ Assertions.assertEquals(expectedTable.fullName(),
financeObjects[0].fullName());
+ Assertions.assertEquals(expectedTable.type(), financeObjects[0].type());
+ Assertions.assertEquals(0,
tag.associatedObjects().objects("unknown").length);
+
+ String noValueTagName =
GravitinoITUtils.genRandomName("tag_it_no_value_assignment");
+ metalake.createTag(noValueTagName, "comment", Collections.emptyMap());
+ TagValue[] noValueValues = new TagValue[]
{TagValue.noValue(noValueTagName)};
+ Assertions.assertDoesNotThrow(() ->
table.supportsTags().associateTags(noValueValues, null));
+ Assertions.assertDoesNotThrow(() ->
table.supportsTags().associateTags(noValueValues, null));
+
+ Tag noValueAssignedTag = table.supportsTags().getTag(noValueTagName);
+ Assertions.assertTrue(noValueAssignedTag.assignment().isPresent());
+ Assertions.assertEquals(0,
noValueAssignedTag.assignment().get().values().length);
+ table.supportsTags().associateTags(null, noValueValues);
+
+ Assertions.assertEquals(0, table.supportsTags().associateTags(null,
values).length);
+ }
+
+ private static String[] valuesToStrings(TagValue[] values) {
+ return Arrays.stream(values).map(value ->
value.value().get()).toArray(String[]::new);
+ }
+
@Test
public void testNullableComment() {
String tagName = GravitinoITUtils.genRandomName("tag_it_tag");
diff --git
a/common/src/main/java/org/apache/gravitino/dto/requests/TagValuesAssociateRequest.java
b/common/src/main/java/org/apache/gravitino/dto/requests/TagValuesAssociateRequest.java
index 8eaf2720bc..56bec53d12 100644
---
a/common/src/main/java/org/apache/gravitino/dto/requests/TagValuesAssociateRequest.java
+++
b/common/src/main/java/org/apache/gravitino/dto/requests/TagValuesAssociateRequest.java
@@ -19,8 +19,12 @@
package org.apache.gravitino.dto.requests;
import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonSetter;
+import com.fasterxml.jackson.annotation.Nulls;
import com.google.common.base.Preconditions;
import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Set;
import javax.annotation.Nullable;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@@ -34,9 +38,11 @@ public class TagValuesAssociateRequest implements
RESTRequest {
private static final int MAX_TAG_VALUE_LENGTH = 256;
@JsonProperty("tagsToAdd")
+ @JsonSetter(nulls = Nulls.AS_EMPTY)
private final RequestTagValue[] tagsToAdd;
@JsonProperty("tagsToRemove")
+ @JsonSetter(nulls = Nulls.AS_EMPTY)
private final RequestTagValue[] tagsToRemove;
/**
@@ -52,8 +58,7 @@ public class TagValuesAssociateRequest implements RESTRequest
{
/** This is the constructor that is used by Jackson deserializer */
public TagValuesAssociateRequest() {
- this.tagsToAdd = null;
- this.tagsToRemove = null;
+ this(null, null);
}
/**
@@ -74,6 +79,24 @@ public class TagValuesAssociateRequest implements
RESTRequest {
return toTagValues(tagsToRemove);
}
+ /**
+ * Returns the tag names to add without validating assignment values.
+ *
+ * @return The tag names to add.
+ */
+ public String[] tagNamesToAdd() {
+ return tagNames(tagsToAdd);
+ }
+
+ /**
+ * Returns the tag names to remove without validating assignment values.
+ *
+ * @return The tag names to remove.
+ */
+ public String[] tagNamesToRemove() {
+ return tagNames(tagsToRemove);
+ }
+
/**
* Validates the request.
*
@@ -82,32 +105,39 @@ public class TagValuesAssociateRequest implements
RESTRequest {
@Override
public void validate() throws IllegalArgumentException {
Preconditions.checkArgument(
- tagsToAdd != null || tagsToRemove != null,
- "tagsToAdd and tagsToRemove cannot both be null");
+ tagsToAdd.length > 0 || tagsToRemove.length > 0,
+ "tagsToAdd and tagsToRemove cannot both be empty");
validateTagValues(tagsToAdd, "tagsToAdd");
validateTagValues(tagsToRemove, "tagsToRemove");
+ validateNoIntersection(tagsToAdd, tagsToRemove);
}
private static RequestTagValue[] toRequestTagValues(TagValue[] tagValues) {
if (tagValues == null) {
- return null;
+ return new RequestTagValue[0];
}
return
Arrays.stream(tagValues).map(RequestTagValue::new).toArray(RequestTagValue[]::new);
}
private static TagValue[] toTagValues(RequestTagValue[] tagValues) {
if (tagValues == null) {
- return null;
+ return new TagValue[0];
}
return
Arrays.stream(tagValues).map(RequestTagValue::toTagValue).toArray(TagValue[]::new);
}
- private static void validateTagValues(RequestTagValue[] tagValues, String
fieldName) {
+ private static String[] tagNames(RequestTagValue[] tagValues) {
if (tagValues == null) {
- return;
+ return new String[0];
}
+ return Arrays.stream(tagValues)
+ .filter(tagValue -> tagValue != null)
+ .map(tagValue -> tagValue.name)
+ .toArray(String[]::new);
+ }
+ private static void validateTagValues(RequestTagValue[] tagValues, String
fieldName) {
for (RequestTagValue tagValue : tagValues) {
Preconditions.checkArgument(
tagValue != null, "%s must not contain null tag values", fieldName);
@@ -128,6 +158,16 @@ public class TagValuesAssociateRequest implements
RESTRequest {
}
}
}
+
+ private static void validateNoIntersection(
+ RequestTagValue[] tagsToAdd, RequestTagValue[] tagsToRemove) {
+ Set<RequestTagValue> tagsToAddSet = new
LinkedHashSet<>(Arrays.asList(tagsToAdd));
+ for (RequestTagValue tagToRemove : tagsToRemove) {
+ Preconditions.checkArgument(
+ !tagsToAddSet.contains(tagToRemove), "tagsToAdd and tagsToRemove
must not overlap");
+ }
+ }
+
/**
* Compares this request with another object.
*
diff --git
a/common/src/test/java/org/apache/gravitino/dto/requests/TestTagValuesAssociateRequest.java
b/common/src/test/java/org/apache/gravitino/dto/requests/TestTagValuesAssociateRequest.java
index 970ca6ca2a..fd5c1b1cb4 100644
---
a/common/src/test/java/org/apache/gravitino/dto/requests/TestTagValuesAssociateRequest.java
+++
b/common/src/test/java/org/apache/gravitino/dto/requests/TestTagValuesAssociateRequest.java
@@ -48,6 +48,26 @@ public class TestTagValuesAssociateRequest {
TagValuesAssociateRequest validRequest =
new TagValuesAssociateRequest(new TagValue[]
{TagValue.of("data_domain", "finance")}, null);
Assertions.assertDoesNotThrow(validRequest::validate);
+ Assertions.assertArrayEquals(new TagValue[0],
validRequest.tagValuesToRemove());
+
+ TagValuesAssociateRequest emptyRequest = new
TagValuesAssociateRequest(null, null);
+ Assertions.assertThrows(IllegalArgumentException.class,
emptyRequest::validate);
+
+ TagValuesAssociateRequest nullFieldRequest =
+ JsonUtils.objectMapper()
+ .readValue(
+
"{\"tagsToAdd\":[{\"name\":\"data_domain\"}],\"tagsToRemove\":null}",
+ TagValuesAssociateRequest.class);
+ Assertions.assertDoesNotThrow(nullFieldRequest::validate);
+ Assertions.assertArrayEquals(new TagValue[0],
nullFieldRequest.tagValuesToRemove());
+
+ TagValuesAssociateRequest nullFieldsRequest =
+ JsonUtils.objectMapper()
+ .readValue(
+ "{\"tagsToAdd\":null,\"tagsToRemove\":null}",
TagValuesAssociateRequest.class);
+ Assertions.assertArrayEquals(new TagValue[0],
nullFieldsRequest.tagValuesToAdd());
+ Assertions.assertArrayEquals(new TagValue[0],
nullFieldsRequest.tagValuesToRemove());
+ Assertions.assertThrows(IllegalArgumentException.class,
nullFieldsRequest::validate);
TagValuesAssociateRequest blankNameRequest =
JsonUtils.objectMapper()
@@ -64,6 +84,26 @@ public class TestTagValuesAssociateRequest {
Assertions.assertThrows(IllegalArgumentException.class,
blankValueRequest::validate);
}
+ @Test
+ public void testTagValuesAssociateRequestRejectsOverlappingPairs() {
+ TagValuesAssociateRequest exactPairOverlap =
+ new TagValuesAssociateRequest(
+ new TagValue[] {TagValue.of("data_domain", "finance"),
TagValue.noValue("owner")},
+ new TagValue[] {TagValue.of("data_domain", "finance")});
+ Assertions.assertThrows(IllegalArgumentException.class,
exactPairOverlap::validate);
+
+ TagValuesAssociateRequest noValueOverlap =
+ new TagValuesAssociateRequest(
+ new TagValue[] {TagValue.noValue("owner")}, new TagValue[]
{TagValue.noValue("owner")});
+ Assertions.assertThrows(IllegalArgumentException.class,
noValueOverlap::validate);
+
+ TagValuesAssociateRequest differentValue =
+ new TagValuesAssociateRequest(
+ new TagValue[] {TagValue.of("data_domain", "finance")},
+ new TagValue[] {TagValue.of("data_domain", "risk")});
+ Assertions.assertDoesNotThrow(differentValue::validate);
+ }
+
@Test
public void testTagValuesAssociateRequestRejectsV1Shape() {
Assertions.assertThrows(
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
index fb175d5a1e..333e88b024 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
@@ -437,13 +437,26 @@ public class RelationalEntityStore
NameIdentifier[] destEntitiesToAdd,
NameIdentifier[] destEntitiesToRemove)
throws IOException, NoSuchEntityException, EntityAlreadyExistsException {
- return updateEntityRelations(
+ RelationUpdate update =
RelationUpdate.of(
relType,
srcEntityIdent,
srcEntityType,
toRelationEdgeTargets(relType, destEntitiesToAdd),
- toRelationEdgeTargets(relType, destEntitiesToRemove)));
+ toRelationEdgeTargets(relType, destEntitiesToRemove));
+ if (relType != SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL) {
+ return updateEntityRelations(update);
+ }
+
+ List<E> result =
+ backend.updateEntityRelations(
+ relType, srcEntityIdent, srcEntityType, destEntitiesToAdd,
destEntitiesToRemove);
+ Entity.EntityType targetEntityType = relationUpdateTargetType(relType);
+ cache.invalidate(srcEntityIdent, srcEntityType);
+ invalidateRelationTargetCache(targetEntityType, update.targetsToAdd());
+ invalidateRelationTargetCache(targetEntityType, update.targetsToRemove());
+
+ return result;
}
@Override
@@ -467,18 +480,7 @@ public class RelationalEntityStore
RelationEdgeTarget[] targetsToAdd = update.targetsToAdd();
RelationEdgeTarget[] targetsToRemove = update.targetsToRemove();
- List<E> result;
- if (update.hasRelationValues()) {
- result = backend.updateEntityRelations(update);
- } else {
- result =
- backend.updateEntityRelations(
- update.relationType(),
- update.sourceIdentifier(),
- update.sourceEntityType(),
- toNameIdentifiers(targetsToAdd),
- toNameIdentifiers(targetsToRemove));
- }
+ List<E> result = backend.updateEntityRelations(update);
// Invalidate after the backend write, not before: invalidating first
opens a window where a
// concurrent read could repopulate the cache with stale pre-commit data.
@@ -540,12 +542,6 @@ public class RelationalEntityStore
.toArray(RelationEdgeTarget[]::new);
}
- private static NameIdentifier[] toNameIdentifiers(RelationEdgeTarget[]
relationTargets) {
- return Arrays.stream(relationTargets)
- .map(RelationEdgeTarget::nameIdentifier)
- .toArray(NameIdentifier[]::new);
- }
-
private static Entity.EntityType relationUpdateTargetType(Type relType) {
switch (relType) {
case POLICY_METADATA_OBJECT_REL:
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/service/TagMetaService.java
b/core/src/main/java/org/apache/gravitino/storage/relational/service/TagMetaService.java
index 36806449c4..92b6362379 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/service/TagMetaService.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/service/TagMetaService.java
@@ -392,9 +392,15 @@ public class TagMetaService {
.remove(tagValueToRemove.value());
tagRelsToRemove.add(
tagRelForValue(tagPO, metadataObjectId, metadataObject,
tagValueToRemove));
- } else {
+ } else if (failOnDuplicateValuelessAssignment) {
activeValuesByTagId.remove(tagPO.getTagId());
tagIdsToRemove.add(tagPO.getTagId());
+ } else {
+ activeValuesByTagId
+ .computeIfAbsent(tagPO.getTagId(), ignored -> new
LinkedHashSet<>())
+ .remove(Optional.empty());
+ tagRelsToRemove.add(
+ tagRelForValue(tagPO, metadataObjectId, metadataObject,
tagValueToRemove));
}
}
diff --git a/core/src/main/java/org/apache/gravitino/tag/TagManager.java
b/core/src/main/java/org/apache/gravitino/tag/TagManager.java
index 3122dd9997..9e2cf23b83 100644
--- a/core/src/main/java/org/apache/gravitino/tag/TagManager.java
+++ b/core/src/main/java/org/apache/gravitino/tag/TagManager.java
@@ -341,13 +341,33 @@ public class TagManager implements TagDispatcher {
String metalake, MetadataObject metadataObject, String[] tagsToAdd,
String[] tagsToRemove)
throws NoSuchMetadataObjectException, TagAlreadyAssociatedException {
return associateTagValuesForMetadataObject(
- metalake, metadataObject, toNoValue(tagsToAdd),
toNoValue(tagsToRemove));
+ metalake,
+ metadataObject,
+ toNoValue(tagsToAdd),
+ toNoValue(tagsToRemove),
+ TagAssociationMode.TAG_NAMES);
}
@Override
public String[] associateTagValuesForMetadataObject(
String metalake, MetadataObject metadataObject, TagValue[] tagsToAdd,
TagValue[] tagsToRemove)
throws NoSuchMetadataObjectException, TagAlreadyAssociatedException {
+ return associateTagValuesForMetadataObject(
+ metalake, metadataObject, tagsToAdd, tagsToRemove,
TagAssociationMode.TAG_VALUES);
+ }
+
+ private enum TagAssociationMode {
+ TAG_NAMES,
+ TAG_VALUES
+ }
+
+ private String[] associateTagValuesForMetadataObject(
+ String metalake,
+ MetadataObject metadataObject,
+ TagValue[] tagsToAdd,
+ TagValue[] tagsToRemove,
+ TagAssociationMode mode)
+ throws NoSuchMetadataObjectException, TagAlreadyAssociatedException {
Preconditions.checkArgument(
SUPPORTED_METADATA_OBJECT_TYPES_FOR_TAGS.contains(metadataObject.type()),
"Cannot associate tags for unsupported metadata object type %s",
@@ -382,17 +402,29 @@ public class TagManager implements TagDispatcher {
LockType.WRITE,
() -> {
try {
- List<TagEntity> tags =
- entityStore
- .relationOperations()
- .updateEntityRelations(
- RelationUpdate.of(
-
SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
- entityIdent,
- entityType,
- toRelationEdgeTargets(metalake,
tagValuesToAdd),
- toRelationEdgeTargets(metalake,
tagValuesToRemove)));
-
+ List<TagEntity> tags;
+ if (mode == TagAssociationMode.TAG_VALUES) {
+ tags =
+ entityStore
+ .relationOperations()
+ .updateEntityRelations(
+ RelationUpdate.of(
+
SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+ entityIdent,
+ entityType,
+ toRelationEdgeTargets(metalake,
tagValuesToAdd),
+ toRelationEdgeTargets(metalake,
tagValuesToRemove)));
+ } else {
+ tags =
+ entityStore
+ .relationOperations()
+ .updateEntityRelations(
+
SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+ entityIdent,
+ entityType,
+ toNameIdentifiers(metalake, tagValuesToAdd),
+ toNameIdentifiers(metalake,
tagValuesToRemove));
+ }
return
tags.stream().map(Tag::name).distinct().toArray(String[]::new);
} catch (NoSuchEntityException e) {
throw new NoSuchMetadataObjectException(
@@ -459,12 +491,18 @@ public class TagManager implements TagDispatcher {
private static TagValue[] toNoValue(String[] tags) {
if (tags == null) {
- return null;
+ return new TagValue[0];
}
return Arrays.stream(tags).map(TagValue::noValue).toArray(TagValue[]::new);
}
+ private static NameIdentifier[] toNameIdentifiers(String metalake,
TagValue[] tagValues) {
+ return Arrays.stream(tagValues)
+ .map(tagValue -> NameIdentifierUtil.ofTag(metalake, tagValue.name()))
+ .toArray(NameIdentifier[]::new);
+ }
+
private static RelationEdgeTarget[] toRelationEdgeTargets(String metalake,
TagValue[] tagValues) {
return Arrays.stream(tagValues)
.map(
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/TestRelationalEntityStore.java
b/core/src/test/java/org/apache/gravitino/storage/relational/TestRelationalEntityStore.java
index edc1cb9ebe..2aca05b3c1 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/TestRelationalEntityStore.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/TestRelationalEntityStore.java
@@ -161,33 +161,49 @@ public class TestRelationalEntityStore {
NameIdentifier[] destEntitiesToRemove = new NameIdentifier[]
{destToRemove};
NoOpsCache cache = (NoOpsCache) FieldUtils.readField(store, "cache", true);
- Mockito.doAnswer(
- invocation -> {
- Mockito.verify(cache, Mockito.never()).invalidate(src,
Entity.EntityType.TABLE);
- Mockito.verify(cache, Mockito.never()).invalidate(destToAdd,
destinationType);
- Mockito.verify(cache, Mockito.never()).invalidate(destToRemove,
destinationType);
- return List.of();
- })
- .when(backend)
- .updateEntityRelations(
- eq(relationType),
- eq(src),
- eq(Entity.EntityType.TABLE),
- any(NameIdentifier[].class),
- any(NameIdentifier[].class));
+ if (relationType ==
SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL) {
+ Mockito.doAnswer(
+ invocation -> {
+ Mockito.verify(cache, Mockito.never()).invalidate(src,
Entity.EntityType.TABLE);
+ Mockito.verify(cache, Mockito.never()).invalidate(destToAdd,
destinationType);
+ Mockito.verify(cache,
Mockito.never()).invalidate(destToRemove, destinationType);
+ return List.of();
+ })
+ .when(backend)
+ .updateEntityRelations(
+ eq(relationType),
+ eq(src),
+ eq(Entity.EntityType.TABLE),
+ any(NameIdentifier[].class),
+ any(NameIdentifier[].class));
+ } else {
+ Mockito.doAnswer(
+ invocation -> {
+ Mockito.verify(cache, Mockito.never()).invalidate(src,
Entity.EntityType.TABLE);
+ Mockito.verify(cache, Mockito.never()).invalidate(destToAdd,
destinationType);
+ Mockito.verify(cache,
Mockito.never()).invalidate(destToRemove, destinationType);
+ return List.of();
+ })
+ .when(backend)
+ .updateEntityRelations(any(RelationUpdate.class));
+ }
store.updateEntityRelations(
relationType, src, Entity.EntityType.TABLE, destEntitiesToAdd,
destEntitiesToRemove);
InOrder inOrder = Mockito.inOrder(backend, cache);
- inOrder
- .verify(backend)
- .updateEntityRelations(
- eq(relationType),
- eq(src),
- eq(Entity.EntityType.TABLE),
- any(NameIdentifier[].class),
- any(NameIdentifier[].class));
+ if (relationType ==
SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL) {
+ inOrder
+ .verify(backend)
+ .updateEntityRelations(
+ eq(relationType),
+ eq(src),
+ eq(Entity.EntityType.TABLE),
+ any(NameIdentifier[].class),
+ any(NameIdentifier[].class));
+ } else {
+ inOrder.verify(backend).updateEntityRelations(any(RelationUpdate.class));
+ }
inOrder.verify(cache).invalidate(src, Entity.EntityType.TABLE);
inOrder.verify(cache).invalidate(destToAdd, destinationType);
inOrder.verify(cache).invalidate(destToRemove, destinationType);
diff --git a/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
b/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
index ff2b33046c..a683561823 100644
--- a/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
+++ b/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
@@ -700,6 +700,16 @@ public class TestTagManager {
Assertions.assertEquals(
0, tagManager.listMetadataObjectsForTag(METALAKE, tag.name(),
"pii").length);
+ tagManager.associateTagValuesForMetadataObject(
+ METALAKE, tableObject, null, new TagValue[]
{TagValue.noValue(tag.name())});
+ Tag dataDomainInfo = tagManager.getTagForMetadataObject(METALAKE,
tableObject, tag.name());
+ Assertions.assertArrayEquals(
+ new String[] {"finance", "risk"},
dataDomainInfo.assignment().get().values());
+
+ Assertions.assertArrayEquals(
+ new MetadataObject[] {tableObject},
+ tagManager.listMetadataObjectsForTag(METALAKE, tag.name(), "finance"));
+
Tag ownerTag = tagManager.createTag(METALAKE, "owner", null, null);
tagManager.associateTagValuesForMetadataObject(
METALAKE, tableObject, new TagValue[]
{TagValue.noValue(ownerTag.name())}, null);
@@ -714,12 +724,50 @@ public class TestTagManager {
new MetadataObject[] {tableObject},
tagManager.listMetadataObjectsForTag(METALAKE, ownerTag.name(),
"team-a"));
+ Tag noValueIdempotentTag = tagManager.createTag(METALAKE,
"no_value_idempotent", null, null);
+ tagManager.associateTagValuesForMetadataObject(
+ METALAKE,
+ tableObject,
+ new TagValue[] {TagValue.noValue(noValueIdempotentTag.name())},
+ null);
+ Assertions.assertDoesNotThrow(
+ () ->
+ tagManager.associateTagValuesForMetadataObject(
+ METALAKE,
+ tableObject,
+ new TagValue[] {TagValue.noValue(noValueIdempotentTag.name())},
+ null));
+
Assertions.assertTrue(
tagInfos[0].valueConstraint().type() ==
TagValueConstraint.Type.ALLOWED_VALUES);
Assertions.assertArrayEquals(
new String[] {"finance", "risk"},
tagInfos[0].valueConstraint().allowedValues());
}
+ @Test
+ public void testV1RemoveValuedTagByName() {
+ Tag tag =
+ tagManager.createTag(
+ METALAKE, "v1_remove_valued", null, null,
TagValueConstraint.ofAllowedValues("dev"));
+ MetadataObject tableObject =
+ NameIdentifierUtil.toMetadataObject(
+ NameIdentifierUtil.ofTable(METALAKE, CATALOG, SCHEMA, TABLE),
Entity.EntityType.TABLE);
+
+ tagManager.associateTagValuesForMetadataObject(
+ METALAKE, tableObject, new TagValue[] {TagValue.of(tag.name(),
"dev")}, null);
+ Assertions.assertArrayEquals(
+ new MetadataObject[] {tableObject},
+ tagManager.listMetadataObjectsForTag(METALAKE, tag.name(), "dev"));
+
+ tagManager.associateTagsForMetadataObject(
+ METALAKE, tableObject, null, new String[] {tag.name()});
+
+ Assertions.assertEquals(
+ 0, tagManager.listTagsInfoForMetadataObject(METALAKE,
tableObject).length);
+ Assertions.assertEquals(
+ 0, tagManager.listMetadataObjectsForTag(METALAKE, tag.name(),
"dev").length);
+ }
+
@Test
public void testRejectNullTagValueToRemove() {
MetadataObject tableObject =
diff --git a/design-docs/tag-assignment-values.md
b/design-docs/tag-assignment-values.md
index 1bf0375b72..705cd82325 100644
--- a/design-docs/tag-assignment-values.md
+++ b/design-docs/tag-assignment-values.md
@@ -730,7 +730,8 @@ pair-level delta instead of replacing the complete value
list:
Adding a non-null pair to a tag with an active null row is a changed logical
assignment: the null row
is soft-deleted and the non-null row is inserted in the same transaction.
Adding a null pair to a tag
with active non-null rows is accepted only when the request's removals remove
all remaining
-non-null rows for that tag; otherwise it fails with `409 Conflict`.
+non-null rows for that tag; otherwise it fails with `400 Bad Request` as an
invalid assignment
+transition.
Name-only reads must use distinct tag names because a valued assignment can
now have multiple
physical rows. Detailed reads must group relation rows by logical assignment
before building
@@ -811,9 +812,9 @@ TagValuePair[] tagsToRemove();
| Assignment value is not in the tag's configured allowed values | `400 Bad
Request` |
| `tagsToAdd` and `tagsToRemove` contain the same exact pair | `400 Bad
Request` |
| `tagsToAdd` contains both a valueless pair and non-null value pairs for the
same tag | `400 Bad Request` |
-| Adding a valueless pair while non-null values remain active for the same tag
| `409 Conflict` |
+| Adding a valueless pair while non-null values remain active for the same tag
| `400 Bad Request` |
| `value` query parameter is blank or longer than 256 characters | `400 Bad
Request` |
-| `tagsToAdd` or `tagsToRemove` references a tag that does not exist | `404
Not Found` |
+| `tagsToAdd` or `tagsToRemove` references a tag that does not exist | `200
OK`; no relation row is inserted or deleted |
| `tagsToAdd` targets an already active pair | `200 OK`; no relation row is
inserted |
| `tagsToRemove` targets a missing pair | `200 OK`; no relation row is deleted
|
diff --git a/docs/open-api/tags.yaml b/docs/open-api/tags.yaml
index c2e39cd489..c9ea86cbf8 100644
--- a/docs/open-api/tags.yaml
+++ b/docs/open-api/tags.yaml
@@ -232,6 +232,12 @@ paths:
examples:
TagAssociate:
$ref: "#/components/examples/TagAssociate"
+ application/vnd.gravitino.v2+json:
+ schema:
+ $ref: "#/components/requests/TagValuesAssociateRequest"
+ examples:
+ TagValuesAssociate:
+ $ref: "#/components/examples/TagValuesAssociate"
responses:
"200":
description: Returns the list of tag names associated with the
specified metadata object
@@ -242,6 +248,21 @@ paths:
examples:
NameListResponse:
$ref: "#/components/examples/NameListResponse"
+ application/vnd.gravitino.v2+json:
+ schema:
+ $ref: "./openapi.yaml#/components/schemas/NameListResponse"
+ examples:
+ NameListResponse:
+ $ref: "#/components/examples/NameListResponse"
+ "400":
+ description: Bad Request - The request body is malformed or violates
tag association validation rules
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+ application/vnd.gravitino.v2+json:
+ schema:
+ $ref: "./openapi.yaml#/components/schemas/ErrorModel"
"404":
description: Not Found - The specified metalake does not exist
content:
@@ -251,6 +272,12 @@ paths:
examples:
NoSuchMetalakeException:
$ref:
"./metalakes.yaml#/components/examples/NoSuchMetalakeException"
+ application/vnd.gravitino.v2+json:
+ schema:
+ $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+ examples:
+ NoSuchMetalakeException:
+ $ref:
"./metalakes.yaml#/components/examples/NoSuchMetalakeException"
"409":
description: Conflict - The target tag already associated with the
specified metadata object
content:
@@ -260,8 +287,21 @@ paths:
examples:
TagAlreadyAssociatedException:
$ref: "#/components/examples/TagAlreadyAssociatedException"
+ application/vnd.gravitino.v2+json:
+ schema:
+ $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+ examples:
+ TagAlreadyAssociatedException:
+ $ref: "#/components/examples/TagAlreadyAssociatedException"
"5xx":
- $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+ description: A server-side problem that might not be addressable
from the client side
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+ application/vnd.gravitino.v2+json:
+ schema:
+ $ref: "./openapi.yaml#/components/schemas/ErrorModel"
/metalakes/{metalake}/objects/{metadataObjectType}/{metadataObjectFullName}/tags/{tag}:
@@ -313,6 +353,8 @@ paths:
- tag
summary: list metadata objects for tag
operationId: listTagObjects
+ parameters:
+ - $ref: "#/components/parameters/tagValue"
responses:
"200":
description: Returns the list of metadata objects associated with
specified tag
@@ -349,6 +391,15 @@ components:
type: boolean
default: false
+ tagValue:
+ name: value
+ in: query
+ description: Exact assignment value used to filter objects associated
with the tag
+ required: false
+ schema:
+ type: string
+ maxLength: 256
+
schemas:
Tag:
@@ -377,6 +428,20 @@ components:
type: boolean
description: Whether the tag is inherited from the parent metadata
object
nullable: true
+ allowedValues:
+ type: array
+ description: Allowed assignment values. Null means any value is
allowed, an empty array means the tag can only be assigned without a value.
+ nullable: true
+ items:
+ type: string
+ maxLength: 256
+ assignmentValues:
+ type: array
+ description: Assignment values in the current metadata object
context. Null means no assignment context, an empty array means assigned
without a value.
+ nullable: true
+ items:
+ type: string
+ maxLength: 256
MetadataObject:
type: object
@@ -421,6 +486,13 @@ components:
default: { }
additionalProperties:
type: string
+ allowedValues:
+ type: array
+ description: Allowed assignment values. Null means any value is
allowed, an empty array means the tag can only be assigned without a value.
+ nullable: true
+ items:
+ type: string
+ maxLength: 256
TagUpdatesRequest:
type: object
@@ -539,6 +611,36 @@ components:
description: The tags to remove
nullable: true
+ TagValuesAssociateRequest:
+ type: object
+ properties:
+ tagsToAdd:
+ type: array
+ items:
+ $ref: "#/components/requests/TagValue"
+ description: The tag values to add
+ nullable: true
+ tagsToRemove:
+ type: array
+ items:
+ $ref: "#/components/requests/TagValue"
+ description: The tag values to remove
+ nullable: true
+
+ TagValue:
+ type: object
+ required:
+ - name
+ properties:
+ name:
+ type: string
+ description: The tag name
+ value:
+ type: string
+ description: The optional assignment value
+ nullable: true
+ maxLength: 256
+
responses:
TagListResponse:
type: object
@@ -628,7 +730,8 @@ components:
"properties": {
"key1": "value1",
"key2": "value2"
- }
+ },
+ "allowedValues": ["finance", "engineering"]
}
TagAssociate:
@@ -637,6 +740,17 @@ components:
"tagsToRemove": ["my_tag3"]
}
+ TagValuesAssociate:
+ value: {
+ "tagsToAdd": [
+ {"name": "my_tag1", "value": "finance"},
+ {"name": "my_tag2"}
+ ],
+ "tagsToRemove": [
+ {"name": "my_tag3", "value": "engineering"}
+ ]
+ }
+
TagResponse:
value: {
"code": 0,
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/ApiVersion.java
b/server/src/main/java/org/apache/gravitino/server/web/ApiVersion.java
index 5740e9d791..87009dd978 100644
--- a/server/src/main/java/org/apache/gravitino/server/web/ApiVersion.java
+++ b/server/src/main/java/org/apache/gravitino/server/web/ApiVersion.java
@@ -18,17 +18,19 @@
*/
package org.apache.gravitino.server.web;
+import java.util.Arrays;
import java.util.Comparator;
import java.util.TreeSet;
public enum ApiVersion {
- V_1(1);
+ V_1(1),
+ V_2(2);
private static final TreeSet<ApiVersion> VERSIONS =
new TreeSet<>(Comparator.comparingInt(o -> o.version));
static {
- VERSIONS.add(V_1);
+ VERSIONS.addAll(Arrays.asList(values()));
}
private final int version;
@@ -45,6 +47,11 @@ public enum ApiVersion {
return VERSIONS.last();
}
+ /** Returns the API version used when a request does not specify one. */
+ public static ApiVersion defaultVersion() {
+ return V_1;
+ }
+
public static boolean isSupportedVersion(int version) {
for (ApiVersion v : ApiVersion.values()) {
if (v.version == version) {
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/VersioningFilter.java
b/server/src/main/java/org/apache/gravitino/server/web/VersioningFilter.java
index 7c2c71b32d..98e132f89c 100644
--- a/server/src/main/java/org/apache/gravitino/server/web/VersioningFilter.java
+++ b/server/src/main/java/org/apache/gravitino/server/web/VersioningFilter.java
@@ -65,6 +65,15 @@ public class VersioningFilter implements Filter {
return ((HttpServletRequest) getRequest()).getHeader(name);
}
+ @Override
+ public Enumeration<String> getHeaders(String name) {
+ String headerValue = customHeaders.get(name);
+ if (headerValue != null) {
+ return Collections.enumeration(Collections.singletonList(headerValue));
+ }
+ return ((HttpServletRequest) getRequest()).getHeaders(name);
+ }
+
@Override
public Enumeration<String> getHeaderNames() {
List<String> combinedHeaderNames = new
ArrayList<>(customHeaders.keySet());
@@ -78,9 +87,10 @@ public class VersioningFilter implements Filter {
}
}
- private static final Pattern ACCEPT_VERSION_REGEX =
+ private static final Pattern VERSIONED_JSON_MEDIA_TYPE_REGEX =
Pattern.compile("application/vnd\\.gravitino\\.v(\\d+)\\+json");
private static final String ACCEPT_VERSION_HEADER = "Accept";
+ private static final String CONTENT_TYPE_HEADER = "Content-Type";
private static String getAcceptVersion(int version) {
return String.format("application/vnd.gravitino.v%d+json", version);
@@ -93,34 +103,62 @@ public class VersioningFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
- Enumeration<String> acceptHeader = req.getHeaders(ACCEPT_VERSION_HEADER);
- while (acceptHeader.hasMoreElements()) {
- String value = acceptHeader.nextElement();
+ Integer acceptVersion =
versionFromHeaders(req.getHeaders(ACCEPT_VERSION_HEADER));
+ if (acceptVersion != null) {
+ if (isUnsupportedVersion(acceptVersion, response)) {
+ return;
+ }
+
+ chain.doFilter(request, response);
+ return;
+ }
- // If version accept header is set, then we need to check if it is
supported.
- Matcher m = ACCEPT_VERSION_REGEX.matcher(value);
- if (m.find()) {
- int version = Integer.parseInt(m.group(1));
+ MutableHttpServletRequest mutableRequest = new
MutableHttpServletRequest(req);
+ Integer contentTypeVersion =
versionFromHeader(req.getHeader(CONTENT_TYPE_HEADER));
+ if (contentTypeVersion != null) {
+ if (isUnsupportedVersion(contentTypeVersion, response)) {
+ return;
+ }
- if (!ApiVersion.isSupportedVersion(version)) {
- LOG.error("Unsupported version v{} in Request Header {}.", version,
value);
+ mutableRequest.putHeader(ACCEPT_VERSION_HEADER,
getAcceptVersion(contentTypeVersion));
+ } else {
+ ApiVersion defaultVersion = ApiVersion.defaultVersion();
+ mutableRequest.putHeader(ACCEPT_VERSION_HEADER,
getAcceptVersion(defaultVersion.version()));
+ }
- HttpServletResponse resp = (HttpServletResponse) response;
- resp.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE, "Unsupported
version");
- } else {
- chain.doFilter(request, response);
- }
+ chain.doFilter(mutableRequest, response);
+ }
- return;
+ private static Integer versionFromHeaders(Enumeration<String> headers) {
+ while (headers.hasMoreElements()) {
+ Integer version = versionFromHeader(headers.nextElement());
+ if (version != null) {
+ return version;
}
}
- // If no version accept header not is set, then we need to set the latest
version.
- MutableHttpServletRequest mutableRequest = new
MutableHttpServletRequest(req);
- ApiVersion latest = ApiVersion.latestVersion();
- mutableRequest.putHeader(ACCEPT_VERSION_HEADER,
getAcceptVersion(latest.version()));
+ return null;
+ }
- chain.doFilter(mutableRequest, response);
+ private static Integer versionFromHeader(String value) {
+ if (value == null) {
+ return null;
+ }
+
+ Matcher matcher = VERSIONED_JSON_MEDIA_TYPE_REGEX.matcher(value);
+ return matcher.find() ? Integer.parseInt(matcher.group(1)) : null;
+ }
+
+ private static boolean isUnsupportedVersion(int version, ServletResponse
response)
+ throws IOException {
+ if (ApiVersion.isSupportedVersion(version)) {
+ return false;
+ }
+
+ LOG.error("Unsupported version v{} in request header.", version);
+ HttpServletResponse resp = (HttpServletResponse) response;
+ resp.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE, "Unsupported
version");
+ return true;
}
@Override
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociateTagAuthorizationExecutor.java
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociateTagAuthorizationExecutor.java
index 2fef4e9ee3..f18ab38334 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociateTagAuthorizationExecutor.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociateTagAuthorizationExecutor.java
@@ -22,14 +22,18 @@ import static
org.apache.gravitino.server.web.filter.ParameterUtil.extractFromPa
import com.google.common.base.Preconditions;
import java.lang.reflect.Parameter;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
+import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Entity;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.authorization.AuthorizationRequestContext;
+import org.apache.gravitino.dto.requests.TagValuesAssociateRequest;
import org.apache.gravitino.dto.requests.TagsAssociateRequest;
import org.apache.gravitino.server.web.rest.MetadataObjectTagOperations;
+import org.apache.gravitino.tag.TagValue;
/**
* Metadata object authorization for {@link
@@ -63,37 +67,49 @@ public class AssociateTagAuthorizationExecutor extends
CommonAuthorizerExecutor
context.setOriginalAuthorizationExpression(expression);
Entity.EntityType targetType =
Entity.EntityType.TAG; // Tags are the only supported batch target here
+
Preconditions.checkArgument(
- request instanceof TagsAssociateRequest,
+ request instanceof TagsAssociateRequest || request instanceof
TagValuesAssociateRequest,
"Only tag can use AssociateTagAuthorizationExecutor, please contact
the administrator.");
- TagsAssociateRequest tagsAssociateRequest = (TagsAssociateRequest) request;
- tagsAssociateRequest.validate();
+
+ TagValue[] tagsToAdd;
+ TagValue[] tagsToRemove;
+ if (request instanceof TagsAssociateRequest) {
+ TagsAssociateRequest tagsAssociateRequest = (TagsAssociateRequest)
request;
+ tagsToAdd = toNoValue(tagsAssociateRequest.getTagsToAdd());
+ tagsToRemove = toNoValue(tagsAssociateRequest.getTagsToRemove());
+ } else {
+ TagValuesAssociateRequest tagValuesAssociateRequest =
(TagValuesAssociateRequest) request;
+ tagsToAdd = toNoValue(tagValuesAssociateRequest.tagNamesToAdd());
+ tagsToRemove = toNoValue(tagValuesAssociateRequest.tagNamesToRemove());
+ }
+
// Authorize both 'tagsToAdd' and 'tagsToRemove' fields.
- return authorizeTag(tagsAssociateRequest.getTagsToAdd(), context,
targetType)
- && authorizeTag(tagsAssociateRequest.getTagsToRemove(), context,
targetType);
+ return authorizeTag(tagsToAdd, context, targetType)
+ && authorizeTag(tagsToRemove, context, targetType);
}
/**
* Performs batch authorization for a given field (e.g., "tagsToAdd" or
"tagsToRemove") containing
- * an array of tag names.
+ * an array of tag values.
*
- * @param tagNames tagNames
+ * @param tagValues tag values
* @param context The shared authorization request context.
* @param targetType The entity type being authorized (expected to be TAG).
* @return {@code true} if all tags in the field pass authorization; {@code
false} otherwise.
*/
private boolean authorizeTag(
- String[] tagNames, AuthorizationRequestContext context,
Entity.EntityType targetType) {
+ TagValue[] tagValues, AuthorizationRequestContext context,
Entity.EntityType targetType) {
// Treat null or empty arrays as no-op (implicitly authorized)
- if (tagNames == null) {
+ if (tagValues == null) {
return true;
}
- for (String tagName : tagNames) {
+ for (TagValue tagValue : tagValues) {
// Use a fresh context copy for each tag to avoid cross-contamination
Map<Entity.EntityType, NameIdentifier> currentContext = new
HashMap<>(this.metadataContext);
- buildNameIdentifierForBatchAuthorization(currentContext, tagName,
targetType);
+ buildNameIdentifierForBatchAuthorization(currentContext,
tagValue.name(), targetType);
boolean authorized =
authorizationExpressionEvaluator.evaluate(
@@ -105,4 +121,14 @@ public class AssociateTagAuthorizationExecutor extends
CommonAuthorizerExecutor
}
return true;
}
+
+ private static TagValue[] toNoValue(String[] tags) {
+ if (tags == null) {
+ return new TagValue[0];
+ }
+ return Arrays.stream(tags)
+ .filter(StringUtils::isNotBlank)
+ .map(TagValue::noValue)
+ .toArray(TagValue[]::new);
+ }
}
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectTagOperations.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectTagOperations.java
index d96e655d30..a77584b3cb 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectTagOperations.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectTagOperations.java
@@ -23,14 +23,14 @@ import static
org.apache.gravitino.server.authorization.expression.Authorization
import com.codahale.metrics.annotation.ResponseMetered;
import com.codahale.metrics.annotation.Timed;
-import com.google.common.collect.Sets;
import java.util.Arrays;
-import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.Locale;
+import java.util.Map;
import java.util.Optional;
-import java.util.Set;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
@@ -44,6 +44,7 @@ import org.apache.commons.lang3.ArrayUtils;
import org.apache.gravitino.Entity;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.dto.requests.TagValuesAssociateRequest;
import org.apache.gravitino.dto.requests.TagsAssociateRequest;
import org.apache.gravitino.dto.responses.NameListResponse;
import org.apache.gravitino.dto.responses.TagListResponse;
@@ -71,6 +72,8 @@ import org.slf4j.LoggerFactory;
public class MetadataObjectTagOperations {
private static final Logger LOG =
LoggerFactory.getLogger(MetadataObjectTagOperations.class);
+ private static final String TAG_VALUES_MEDIA_TYPE =
"application/vnd.gravitino.v2+json";
+
private final TagDispatcher tagDispatcher;
@Context private HttpServletRequest httpRequest;
@@ -183,14 +186,12 @@ public class MetadataObjectTagOperations {
MetadataObjects.parse(
fullName,
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
- Set<TagDTO> tags = Sets.newHashSet();
+ Map<String, TagDTO> tags = new LinkedHashMap<>();
Tag[] nonInheritedTags =
tagDispatcher.listTagsInfoForMetadataObject(metalake, object);
if (ArrayUtils.isNotEmpty(nonInheritedTags)) {
- Collections.addAll(
- tags,
- Arrays.stream(nonInheritedTags)
- .map(t -> DTOConverters.toDTO(t, Optional.of(false)))
- .toArray(TagDTO[]::new));
+ Arrays.stream(nonInheritedTags)
+ .map(t -> DTOConverters.toDTO(t, Optional.of(false)))
+ .forEach(tag -> tags.putIfAbsent(tag.name(), tag));
}
for (MetadataObject parentObject :
@@ -198,11 +199,9 @@ public class MetadataObjectTagOperations {
Tag[] inheritedTags =
tagDispatcher.listTagsInfoForMetadataObject(metalake,
parentObject);
if (ArrayUtils.isNotEmpty(inheritedTags)) {
- Collections.addAll(
- tags,
- Arrays.stream(inheritedTags)
- .map(t -> DTOConverters.toDTO(t, Optional.of(true)))
- .toArray(TagDTO[]::new));
+ Arrays.stream(inheritedTags)
+ .map(t -> DTOConverters.toDTO(t, Optional.of(true)))
+ .forEach(tag -> tags.putIfAbsent(tag.name(), tag));
}
}
@@ -213,7 +212,7 @@ public class MetadataObjectTagOperations {
type,
fullName,
metalake);
- TagDTO[] tagDTOS = tags.toArray(new TagDTO[0]);
+ TagDTO[] tagDTOS = tags.values().toArray(new TagDTO[0]);
tagDTOS =
MetadataAuthzHelper.filterByExpression(
metalake,
@@ -224,8 +223,7 @@ public class MetadataObjectTagOperations {
return Utils.ok(new TagListResponse(tagDTOS));
} else {
- // We have used Set to avoid duplicate tag names
- String[] tagNames =
tags.stream().map(TagDTO::name).toArray(String[]::new);
+ String[] tagNames = tags.keySet().toArray(new String[0]);
tagNames =
MetadataAuthzHelper.filterByExpression(
metalake,
@@ -249,6 +247,7 @@ public class MetadataObjectTagOperations {
}
@POST
+ @Consumes("application/json")
@Produces("application/vnd.gravitino.v1+json")
@Timed(name = "associate-object-tags." + MetricNames.HTTP_PROCESS_DURATION,
absolute = true)
@ResponseMetered(name = "associate-object-tags", absolute = true)
@@ -260,6 +259,36 @@ public class MetadataObjectTagOperations {
@PathParam("fullName") @AuthorizationFullName String fullName,
@AuthorizationRequest(type =
AuthorizationRequest.RequestType.ASSOCIATE_TAG)
TagsAssociateRequest request) {
+ return associateTagsForObjectInternal(metalake, type, fullName, request);
+ }
+
+ /**
+ * Associates tag values with a metadata object using the v2 request
representation.
+ *
+ * @param metalake The metalake name.
+ * @param type The metadata object type.
+ * @param fullName The metadata object full name.
+ * @param request The tag values association request.
+ * @return The response containing associated tag names.
+ */
+ @POST
+ @Consumes(TAG_VALUES_MEDIA_TYPE)
+ @Produces(TAG_VALUES_MEDIA_TYPE)
+ @Timed(name = "associate-object-tags." + MetricNames.HTTP_PROCESS_DURATION,
absolute = true)
+ @ResponseMetered(name = "associate-object-tags", absolute = true)
+ @AuthorizationExpression(expression = CAN_ACCESS_METADATA_AND_TAG)
+ public Response associateTagValuesForObject(
+ @PathParam("metalake") @AuthorizationMetadata(type =
Entity.EntityType.METALAKE)
+ String metalake,
+ @PathParam("type") @AuthorizationObjectType String type,
+ @PathParam("fullName") @AuthorizationFullName String fullName,
+ @AuthorizationRequest(type =
AuthorizationRequest.RequestType.ASSOCIATE_TAG)
+ TagValuesAssociateRequest request) {
+ return associateTagValuesForObjectInternal(metalake, type, fullName,
request);
+ }
+
+ private Response associateTagsForObjectInternal(
+ String metalake, String type, String fullName, TagsAssociateRequest
request) {
LOG.info(
"Received associate tags request for object type: {}, full name: {}
under metalake: {}",
type,
@@ -270,19 +299,12 @@ public class MetadataObjectTagOperations {
httpRequest,
() -> {
request.validate();
- MetadataObject object =
- MetadataObjects.parse(
- fullName,
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
+ MetadataObject object = parseMetadataObject(type, fullName);
String[] tagNames =
tagDispatcher.associateTagsForMetadataObject(
metalake, object, request.getTagsToAdd(),
request.getTagsToRemove());
tagNames = tagNames == null ? new String[0] : tagNames;
- LOG.info(
- "Associated tags: {} for object type: {}, full name: {} under
metalake: {}",
- Arrays.toString(tagNames),
- type,
- fullName,
- metalake);
+ logAssociatedTags(type, fullName, metalake, tagNames);
return Utils.ok(new NameListResponse(tagNames));
});
} catch (Exception e) {
@@ -290,6 +312,52 @@ public class MetadataObjectTagOperations {
}
}
+ private Response associateTagValuesForObjectInternal(
+ String metalake, String type, String fullName, TagValuesAssociateRequest
request) {
+ LOG.info(
+ "Received associate tag values request for object type: {}, full name:
{} under metalake: {}",
+ type,
+ fullName,
+ metalake);
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ request.validate();
+ MetadataObject object = parseMetadataObject(type, fullName);
+ String[] tagNames =
+ tagDispatcher.associateTagValuesForMetadataObject(
+ metalake, object, request.tagValuesToAdd(),
request.tagValuesToRemove());
+ tagNames = tagNames == null ? new String[0] : tagNames;
+ logAssociatedTags(type, fullName, metalake, tagNames);
+ return Response.ok(new NameListResponse(tagNames),
TAG_VALUES_MEDIA_TYPE).build();
+ });
+ } catch (Exception e) {
+ return withMediaType(
+ ExceptionHandlers.handleTagException(OperationType.ASSOCIATE, "",
fullName, e),
+ TAG_VALUES_MEDIA_TYPE);
+ }
+ }
+
+ private static MetadataObject parseMetadataObject(String type, String
fullName) {
+ return MetadataObjects.parse(
+ fullName, MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
+ }
+
+ private static void logAssociatedTags(
+ String type, String fullName, String metalake, String[] tagNames) {
+ LOG.info(
+ "Associated tags: {} for object type: {}, full name: {} under
metalake: {}",
+ Arrays.toString(tagNames),
+ type,
+ fullName,
+ metalake);
+ }
+
+ private static Response withMediaType(Response response, String mediaType) {
+ return Response.fromResponse(response).type(mediaType).build();
+ }
+
private Optional<Tag> getTagForObject(String metalake, MetadataObject
object, String tagName) {
try {
return
Optional.ofNullable(tagDispatcher.getTagForMetadataObject(metalake, object,
tagName));
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/TagOperations.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/TagOperations.java
index c69b3a3432..de6460fa2a 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/TagOperations.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/TagOperations.java
@@ -39,6 +39,7 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Entity;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.dto.requests.TagCreateRequest;
@@ -74,6 +75,8 @@ public class TagOperations {
private static final Logger LOG =
LoggerFactory.getLogger(TagOperations.class);
+ private static final int MAX_TAG_VALUE_LENGTH = 256;
+
private final TagDispatcher tagDispatcher;
@Context private HttpServletRequest httpRequest;
@@ -157,7 +160,11 @@ public class TagOperations {
request.validate();
Tag tag =
tagDispatcher.createTag(
- metalake, request.getName(), request.getComment(),
request.getProperties());
+ metalake,
+ request.getName(),
+ request.getComment(),
+ request.getProperties(),
+ request.valueConstraint());
LOG.info("Created tag: {} under metalake: {}", tag.name(),
metalake);
return Utils.ok(new TagResponse(DTOConverters.toDTO(tag,
Optional.empty())));
@@ -268,14 +275,21 @@ public class TagOperations {
public Response listMetadataObjectsForTag(
@PathParam("metalake") @AuthorizationMetadata(type =
Entity.EntityType.METALAKE)
String metalake,
- @PathParam("tag") @AuthorizationMetadata(type = Entity.EntityType.TAG)
String tagName) {
- LOG.info("Received list objects for tag: {} under metalake: {}", tagName,
metalake);
+ @PathParam("tag") @AuthorizationMetadata(type = Entity.EntityType.TAG)
String tagName,
+ @QueryParam("value") String value) {
+ LOG.info(
+ "Received list objects for tag: {} and value: {} under metalake: {}",
+ tagName,
+ value,
+ metalake);
try {
+ validateTagAssignmentValueFilter(value);
return Utils.doAs(
httpRequest,
() -> {
- MetadataObject[] objects =
tagDispatcher.listMetadataObjectsForTag(metalake, tagName);
+ MetadataObject[] objects =
+ tagDispatcher.listMetadataObjectsForTag(metalake, tagName,
value);
objects = objects == null ? new MetadataObject[0] : objects;
LOG.info(
@@ -295,6 +309,21 @@ public class TagOperations {
}
}
+ private static void validateTagAssignmentValueFilter(String value) {
+ if (value == null) {
+ return;
+ }
+
+ if (StringUtils.isBlank(value)) {
+ throw new IllegalArgumentException("Tag assignment value must not be
null or empty");
+ }
+
+ if (value.length() > MAX_TAG_VALUE_LENGTH) {
+ throw new IllegalArgumentException(
+ "Tag assignment value must not exceed " + MAX_TAG_VALUE_LENGTH + "
characters");
+ }
+ }
+
/**
* @deprecated This API has moved to {@code
* /api/metalakes/{metalake}/objects/{type}/{fullName}/tags}.
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/TestApiVersion.java
b/server/src/test/java/org/apache/gravitino/server/web/TestApiVersion.java
index 55f21ba60b..2aa59752e0 100644
--- a/server/src/test/java/org/apache/gravitino/server/web/TestApiVersion.java
+++ b/server/src/test/java/org/apache/gravitino/server/web/TestApiVersion.java
@@ -29,12 +29,19 @@ public class TestApiVersion {
@Test
public void testLatestVersion() {
ApiVersion latest = ApiVersion.latestVersion();
- assertEquals(ApiVersion.V_1, latest);
+ assertEquals(ApiVersion.V_2, latest);
+ }
+
+ @Test
+ public void testDefaultVersion() {
+ ApiVersion defaultVersion = ApiVersion.defaultVersion();
+ assertEquals(ApiVersion.V_1, defaultVersion);
}
@Test
public void testIsSupportedVersion() {
assertTrue(ApiVersion.isSupportedVersion(1));
- assertFalse(ApiVersion.isSupportedVersion(2));
+ assertTrue(ApiVersion.isSupportedVersion(2));
+ assertFalse(ApiVersion.isSupportedVersion(3));
}
}
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/TestVersioningFilter.java
b/server/src/test/java/org/apache/gravitino/server/web/TestVersioningFilter.java
index 6da270ea4f..2d5f3dc1f0 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/TestVersioningFilter.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/TestVersioningFilter.java
@@ -73,7 +73,7 @@ public class TestVersioningFilter {
when(mockRequest.getHeaders("Accept"))
.thenReturn(
- new
Vector<>(Collections.singletonList("application/vnd.gravitino.v2+json"))
+ new
Vector<>(Collections.singletonList("application/vnd.gravitino.v3+json"))
.elements());
filter.doFilter(mockRequest, mockResponse, mockChain);
@@ -102,7 +102,7 @@ public class TestVersioningFilter {
ArgumentCaptor.forClass(MutableHttpServletRequest.class);
verify(mockChain).doFilter(captor.capture(), any());
assertEquals(
- String.format("application/vnd.gravitino.v%d+json",
ApiVersion.latestVersion().version()),
+ String.format("application/vnd.gravitino.v%d+json",
ApiVersion.defaultVersion().version()),
captor.getValue().getHeader("Accept"));
}
@@ -124,10 +124,53 @@ public class TestVersioningFilter {
ArgumentCaptor.forClass(MutableHttpServletRequest.class);
verify(mockChain).doFilter(captor.capture(), any());
assertEquals(
- String.format("application/vnd.gravitino.v%d+json",
ApiVersion.latestVersion().version()),
+ String.format("application/vnd.gravitino.v%d+json",
ApiVersion.defaultVersion().version()),
captor.getValue().getHeader("Accept"));
}
+ @Test
+ public void testContentTypeVersionWithoutAccept() throws ServletException,
IOException {
+ VersioningFilter filter = new VersioningFilter();
+ FilterChain mockChain = mock(FilterChain.class);
+ HttpServletRequest mockRequest = mock(HttpServletRequest.class);
+ HttpServletResponse mockResponse = mock(HttpServletResponse.class);
+
+
when(mockRequest.getHeaders("Accept")).thenReturn(Collections.emptyEnumeration());
+
when(mockRequest.getHeader("Content-Type")).thenReturn("application/vnd.gravitino.v2+json");
+
+ filter.doFilter(mockRequest, mockResponse, mockChain);
+
+ verify(mockChain).doFilter(any(), any());
+ verify(mockResponse, never()).sendError(anyInt(), anyString());
+
+ ArgumentCaptor<MutableHttpServletRequest> captor =
+ ArgumentCaptor.forClass(MutableHttpServletRequest.class);
+ verify(mockChain).doFilter(captor.capture(), any());
+ assertEquals("application/vnd.gravitino.v2+json",
captor.getValue().getHeader("Accept"));
+ }
+
+ @Test
+ public void testContentTypeVersionWithWildcardAccept() throws
ServletException, IOException {
+ VersioningFilter filter = new VersioningFilter();
+ FilterChain mockChain = mock(FilterChain.class);
+ HttpServletRequest mockRequest = mock(HttpServletRequest.class);
+ HttpServletResponse mockResponse = mock(HttpServletResponse.class);
+
+ when(mockRequest.getHeaders("Accept"))
+ .thenReturn(new Vector<>(Collections.singletonList("*/*")).elements());
+
when(mockRequest.getHeader("Content-Type")).thenReturn("application/vnd.gravitino.v2+json");
+
+ filter.doFilter(mockRequest, mockResponse, mockChain);
+
+ verify(mockChain).doFilter(any(), any());
+ verify(mockResponse, never()).sendError(anyInt(), anyString());
+
+ ArgumentCaptor<MutableHttpServletRequest> captor =
+ ArgumentCaptor.forClass(MutableHttpServletRequest.class);
+ verify(mockChain).doFilter(captor.capture(), any());
+ assertEquals("application/vnd.gravitino.v2+json",
captor.getValue().getHeader("Accept"));
+ }
+
@Test
public void testDoFilterWithMultipleAcceptHeaders() throws ServletException,
IOException {
VersioningFilter filter = new VersioningFilter();
@@ -147,8 +190,8 @@ public class TestVersioningFilter {
reset(mockChain, mockResponse);
filter.doFilter(mockRequest, mockResponse, mockChain);
- verify(mockChain, never()).doFilter(any(), any());
- verify(mockResponse).sendError(HttpServletResponse.SC_NOT_ACCEPTABLE,
"Unsupported version");
+ verify(mockChain).doFilter(any(), any());
+ verify(mockResponse, never()).sendError(anyInt(), anyString());
}
@Test
@@ -168,7 +211,7 @@ public class TestVersioningFilter {
ArgumentCaptor.forClass(MutableHttpServletRequest.class);
verify(mockChain).doFilter(captor.capture(), any());
assertEquals(
- String.format("application/vnd.gravitino.v%d+json",
ApiVersion.latestVersion().version()),
+ String.format("application/vnd.gravitino.v%d+json",
ApiVersion.defaultVersion().version()),
captor.getValue().getHeader("Accept"));
}
@@ -231,8 +274,8 @@ public class TestVersioningFilter {
reset(mockChain, mockResponse);
filter.doFilter(mockRequest, mockResponse, mockChain);
- verify(mockChain, never()).doFilter(any(), any());
- verify(mockResponse).sendError(HttpServletResponse.SC_NOT_ACCEPTABLE,
"Unsupported version");
+ verify(mockChain).doFilter(any(), any());
+ verify(mockResponse, never()).sendError(anyInt(), anyString());
reset(mockChain, mockResponse);
@@ -260,6 +303,25 @@ public class TestVersioningFilter {
assertTrue(actualHeaderNames.contains("CustomHeader"));
}
+ @Test
+ public void testGetHeaders() {
+ HttpServletRequest mockRequest = mock(HttpServletRequest.class);
+ when(mockRequest.getHeaders("Header1"))
+ .thenReturn(new
Vector<>(Collections.singletonList("Value1")).elements());
+
+ VersioningFilter.MutableHttpServletRequest mutableRequest =
+ new VersioningFilter.MutableHttpServletRequest(mockRequest);
+ mutableRequest.putHeader("Accept", "application/vnd.gravitino.v1+json");
+
+ List<String> customHeaderValues =
Collections.list(mutableRequest.getHeaders("Accept"));
+ assertEquals(1, customHeaderValues.size());
+ assertEquals("application/vnd.gravitino.v1+json",
customHeaderValues.get(0));
+
+ List<String> delegatedHeaderValues =
Collections.list(mutableRequest.getHeaders("Header1"));
+ assertEquals(1, delegatedHeaderValues.size());
+ assertEquals("Value1", delegatedHeaderValues.get(0));
+ }
+
@Test
public void testDoFilterWithHeaderContainingValidVersionAsSubstring() throws
Exception {
VersioningFilter filter = new VersioningFilter();
@@ -286,8 +348,8 @@ public class TestVersioningFilter {
.elements());
filter.doFilter(mockRequest, mockResponse, mockChain);
- verify(mockChain, never()).doFilter(any(), any());
- verify(mockResponse).sendError(HttpServletResponse.SC_NOT_ACCEPTABLE,
"Unsupported version");
+ verify(mockChain).doFilter(any(), any());
+ verify(mockResponse, never()).sendError(anyInt(), anyString());
reset(mockChain, mockResponse);
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
b/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
index 3a74305ce5..5e9f60ac36 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
@@ -17,7 +17,9 @@
package org.apache.gravitino.server.web.filter;
+import static
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants.CAN_ACCESS_METADATA_AND_TAG;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
@@ -30,9 +32,11 @@ import java.lang.reflect.Method;
import java.security.Principal;
import java.util.Collections;
import java.util.List;
+import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Response;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
+import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.gravitino.Entity;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.GravitinoEnv;
@@ -44,16 +48,23 @@ import
org.apache.gravitino.authorization.AuthorizationRequestContext;
import org.apache.gravitino.authorization.AuthorizationUtils;
import org.apache.gravitino.authorization.GravitinoAuthorizer;
import org.apache.gravitino.authorization.Privilege;
+import org.apache.gravitino.dto.requests.TagValuesAssociateRequest;
import org.apache.gravitino.dto.responses.ErrorResponse;
import org.apache.gravitino.exceptions.ForbiddenException;
import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.json.JsonUtils;
import org.apache.gravitino.listener.EventBus;
import
org.apache.gravitino.listener.api.event.server.AuthorizationDenialFailureEvent;
import org.apache.gravitino.metalake.MetalakeManager;
import org.apache.gravitino.server.authorization.GravitinoAuthorizerProvider;
import
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
+import
org.apache.gravitino.server.authorization.annotations.AuthorizationFullName;
import
org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata;
+import
org.apache.gravitino.server.authorization.annotations.AuthorizationObjectType;
+import
org.apache.gravitino.server.authorization.annotations.AuthorizationRequest;
import org.apache.gravitino.server.web.Utils;
+import org.apache.gravitino.server.web.rest.MetadataObjectTagOperations;
+import org.apache.gravitino.tag.TagDispatcher;
import org.apache.gravitino.utils.PrincipalUtils;
import org.apache.gravitino.utils.RequestContext;
import org.junit.jupiter.api.AfterEach;
@@ -317,6 +328,75 @@ public class TestGravitinoInterceptionService {
}
}
+ @Test
+ public void
testInvalidV2TagAssociationBodyReturnsBadRequestAfterAuthorization()
+ throws Throwable {
+ try (MockedStatic<PrincipalUtils> principalUtilsMocked =
mockStatic(PrincipalUtils.class);
+ MockedStatic<GravitinoAuthorizerProvider> authorizerMocked =
+ mockStatic(GravitinoAuthorizerProvider.class);
+ MockedStatic<AuthorizationUtils> authUtilsMocked =
mockStatic(AuthorizationUtils.class)) {
+
+ principalUtilsMocked
+ .when(PrincipalUtils::getCurrentPrincipal)
+ .thenReturn(new UserPrincipal("tester"));
+ principalUtilsMocked
+ .when(() -> PrincipalUtils.doAs(ArgumentMatchers.any(),
ArgumentMatchers.any()))
+ .thenCallRealMethod();
+
principalUtilsMocked.when(PrincipalUtils::getCurrentUserName).thenReturn("tester");
+
+ GravitinoAuthorizerProvider mockedProvider =
mock(GravitinoAuthorizerProvider.class);
+
authorizerMocked.when(GravitinoAuthorizerProvider::getInstance).thenReturn(mockedProvider);
+ GravitinoAuthorizer authorizer = mock(GravitinoAuthorizer.class);
+ when(mockedProvider.getGravitinoAuthorizer()).thenReturn(authorizer);
+ when(authorizer.authorize(any(), any(), any(), any(),
any())).thenReturn(true);
+ when(authorizer.deny(any(), any(), any(), any(),
any())).thenReturn(false);
+ when(authorizer.isOwner(any(), any(), any(), any())).thenReturn(true);
+ authUtilsMocked
+ .when(
+ () ->
+ AuthorizationUtils.checkCurrentUser(
+ ArgumentMatchers.any(), ArgumentMatchers.any(),
ArgumentMatchers.any()))
+ .thenAnswer(invocation -> null);
+
+ TagValuesAssociateRequest request =
+ JsonUtils.objectMapper()
+ .readValue(
+ "{\"tagsToAdd\":[{\"name\":\"data_domain\",\"value\":\"
\"}]}",
+ TagValuesAssociateRequest.class);
+ Method method =
+ TestMetadataObjectTagAssociationOperations.class.getMethod(
+ "associateTagValuesForObject",
+ String.class,
+ String.class,
+ String.class,
+ TagValuesAssociateRequest.class);
+ Object[] args = new Object[] {"testMetalake", "catalog", "object1",
request};
+ TagDispatcher tagDispatcher = mock(TagDispatcher.class);
+ MetadataObjectTagOperations operations = new
MetadataObjectTagOperations(tagDispatcher);
+ HttpServletRequest httpRequest = mock(HttpServletRequest.class);
+ FieldUtils.writeField(operations, "httpRequest", httpRequest, true);
+ Response invalidBodyResponse =
+ operations.associateTagValuesForObject(
+ (String) args[0],
+ (String) args[1],
+ (String) args[2],
+ (TagValuesAssociateRequest) args[3]);
+
+ MethodInvocation methodInvocation = mock(MethodInvocation.class);
+ when(methodInvocation.getMethod()).thenReturn(method);
+ when(methodInvocation.getArguments()).thenReturn(args);
+ when(methodInvocation.proceed()).thenReturn(invalidBodyResponse);
+
+ MethodInterceptor methodInterceptor =
+ new
GravitinoInterceptionService().getMethodInterceptors(method).get(0);
+ Response response = (Response)
methodInterceptor.invoke(methodInvocation);
+
+ assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
response.getStatus());
+ verify(tagDispatcher, never())
+ .associateTagValuesForMetadataObject(any(), any(), any(), any());
+ }
+ }
+
/**
* When the authorization executor returns {@code false}, {@code
buildNoAuthResponse} is called
* with {@code emitEvent=true}. Verify that:
@@ -489,6 +569,19 @@ public class TestGravitinoInterceptionService {
}
}
+ public static class TestMetadataObjectTagAssociationOperations {
+
+ @AuthorizationExpression(expression = CAN_ACCESS_METADATA_AND_TAG)
+ public Response associateTagValuesForObject(
+ @AuthorizationMetadata(type = Entity.EntityType.METALAKE) String
metalake,
+ @AuthorizationObjectType String type,
+ @AuthorizationFullName String fullName,
+ @AuthorizationRequest(type =
AuthorizationRequest.RequestType.ASSOCIATE_TAG)
+ TagValuesAssociateRequest request) {
+ return Utils.ok("unused");
+ }
+ }
+
public static class TestOperations {
@AuthorizationExpression(
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/filter/authorization/TestAssociateTagAuthorizationExecutor.java
b/server/src/test/java/org/apache/gravitino/server/web/filter/authorization/TestAssociateTagAuthorizationExecutor.java
new file mode 100644
index 0000000000..184b29ad1c
--- /dev/null
+++
b/server/src/test/java/org/apache/gravitino/server/web/filter/authorization/TestAssociateTagAuthorizationExecutor.java
@@ -0,0 +1,128 @@
+/*
+ * 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.gravitino.server.web.filter.authorization;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.authorization.AuthorizationRequestContext;
+import org.apache.gravitino.dto.requests.TagValuesAssociateRequest;
+import org.apache.gravitino.dto.requests.TagsAssociateRequest;
+import org.apache.gravitino.json.JsonUtils;
+import
org.apache.gravitino.server.authorization.annotations.AuthorizationRequest;
+import
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionEvaluator;
+import org.apache.gravitino.tag.TagValue;
+import org.junit.jupiter.api.Test;
+
+public class TestAssociateTagAuthorizationExecutor {
+
+ @Test
+ public void testAuthorizesV1TagNames() throws Exception {
+ TagsAssociateRequest request =
+ new TagsAssociateRequest(new String[] {"pii"}, new String[]
{"data_domain"});
+ assertAuthorizesAllTags("associateV1", TagsAssociateRequest.class,
request);
+ }
+
+ @Test
+ public void testAuthorizesV2TagValues() throws Exception {
+ TagValuesAssociateRequest request =
+ new TagValuesAssociateRequest(
+ new TagValue[] {TagValue.noValue("pii")},
+ new TagValue[] {TagValue.of("data_domain", "finance")});
+ assertAuthorizesAllTags("associateV2", TagValuesAssociateRequest.class,
request);
+ }
+
+ @Test
+ public void testAuthorizesV2TagNamesWithoutValidatingValues() throws
Exception {
+ TagValuesAssociateRequest request =
+ JsonUtils.objectMapper()
+ .readValue(
+ "{\"tagsToAdd\":[{\"name\":\"data_domain\",\"value\":\" \"}]}",
+ TagValuesAssociateRequest.class);
+ Method method = TestOperations.class.getDeclaredMethod("associateV2",
request.getClass());
+ Map<Entity.EntityType, NameIdentifier> metadataContext = new HashMap<>();
+ metadataContext.put(Entity.EntityType.METALAKE,
NameIdentifier.of("metalake"));
+ AssociateTagAuthorizationExecutor executor =
+ new AssociateTagAuthorizationExecutor(
+ "TAG::OWNER",
+ method.getParameters(),
+ new Object[] {request},
+ metadataContext,
+ Collections.emptyMap(),
+ Optional.empty());
+
+ AuthorizationExpressionEvaluator evaluator =
mock(AuthorizationExpressionEvaluator.class);
+ AuthorizationRequestContext context = new AuthorizationRequestContext();
+ when(evaluator.evaluate(anyMap(), anyMap(), any(),
any())).thenReturn(true);
+ executor.authorizationExpressionEvaluator = evaluator;
+
+ assertTrue(executor.execute(context));
+ verify(evaluator, times(1)).evaluate(anyMap(), anyMap(), any(), any());
+ }
+
+ private static void assertAuthorizesAllTags(
+ String methodName, Class<?> requestType, Object request) throws
Exception {
+ Method method = TestOperations.class.getDeclaredMethod(methodName,
requestType);
+ Map<Entity.EntityType, NameIdentifier> metadataContext = new HashMap<>();
+ metadataContext.put(Entity.EntityType.METALAKE,
NameIdentifier.of("metalake"));
+ AssociateTagAuthorizationExecutor executor =
+ new AssociateTagAuthorizationExecutor(
+ "TAG::OWNER",
+ method.getParameters(),
+ new Object[] {request},
+ metadataContext,
+ Collections.emptyMap(),
+ Optional.empty());
+
+ AuthorizationExpressionEvaluator evaluator =
mock(AuthorizationExpressionEvaluator.class);
+ AuthorizationRequestContext context = new AuthorizationRequestContext();
+ when(evaluator.evaluate(anyMap(), anyMap(), any(),
any())).thenReturn(true);
+ executor.authorizationExpressionEvaluator = evaluator;
+
+ assertTrue(executor.execute(context));
+ verify(evaluator, times(2)).evaluate(anyMap(), anyMap(), any(), any());
+ }
+
+ private static class TestOperations {
+ private void associateV1(
+ @AuthorizationRequest(type =
AuthorizationRequest.RequestType.ASSOCIATE_TAG)
+ TagsAssociateRequest request) {
+ request.validate();
+ }
+
+ private void associateV2(
+ @AuthorizationRequest(type =
AuthorizationRequest.RequestType.ASSOCIATE_TAG)
+ TagValuesAssociateRequest request) {
+ request.validate();
+ }
+ }
+}
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectTagOperations.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectTagOperations.java
index e9e7796521..1a291c305e 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectTagOperations.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectTagOperations.java
@@ -38,6 +38,7 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.dto.requests.TagValuesAssociateRequest;
import org.apache.gravitino.dto.requests.TagsAssociateRequest;
import org.apache.gravitino.dto.responses.ErrorConstants;
import org.apache.gravitino.dto.responses.ErrorResponse;
@@ -50,8 +51,10 @@ import org.apache.gravitino.meta.AuditInfo;
import org.apache.gravitino.meta.TagEntity;
import org.apache.gravitino.rest.RESTUtils;
import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagAssignment;
import org.apache.gravitino.tag.TagDispatcher;
import org.apache.gravitino.tag.TagManager;
+import org.apache.gravitino.tag.TagValue;
import org.glassfish.jersey.internal.inject.AbstractBinder;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.TestProperties;
@@ -502,6 +505,58 @@ public class TestMetadataObjectTagOperations extends
BaseOperationsTest {
Assertions.assertFalse(resultTags8.get("tag0").inherited().get());
}
+ @Test
+ public void testListTagsDeduplicatesDifferentAssignmentValues() {
+ MetadataObject catalog = MetadataObjects.parse("object1",
MetadataObject.Type.CATALOG);
+ MetadataObject schema = MetadataObjects.parse("object1.object2",
MetadataObject.Type.SCHEMA);
+ MetadataObject table =
+ MetadataObjects.parse("object1.object2.object3",
MetadataObject.Type.TABLE);
+ Tag directTag =
+ TagEntity.builder()
+ .withName("tag1")
+ .withId(1L)
+ .withAuditInfo(testAuditInfo1)
+ .withAssignment(TagAssignment.ofValues("finance"))
+ .build();
+ Tag inheritedTag =
+ TagEntity.builder()
+ .withName("tag1")
+ .withId(1L)
+ .withAuditInfo(testAuditInfo1)
+ .withAssignment(TagAssignment.ofValues("engineering"))
+ .build();
+ when(tagManager.listTagsInfoForMetadataObject(metalake, table))
+ .thenReturn(new Tag[] {directTag});
+ when(tagManager.listTagsInfoForMetadataObject(metalake, schema))
+ .thenReturn(new Tag[] {inheritedTag});
+ when(tagManager.listTagsInfoForMetadataObject(metalake,
catalog)).thenReturn(new Tag[0]);
+
+ Response detailedResponse =
+ target(basePath(metalake))
+ .path(table.type().toString())
+ .path(table.fullName())
+ .path("tags")
+ .queryParam("details", true)
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+ TagListResponse detailedResult =
detailedResponse.readEntity(TagListResponse.class);
+ Assertions.assertEquals(1, detailedResult.getTags().length);
+ Assertions.assertFalse(detailedResult.getTags()[0].inherited().get());
+ Assertions.assertArrayEquals(
+ new String[] {"finance"},
detailedResult.getTags()[0].assignment().get().values());
+
+ Response namesResponse =
+ target(basePath(metalake))
+ .path(table.type().toString())
+ .path(table.fullName())
+ .path("tags")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+ Assertions.assertEquals(1,
namesResponse.readEntity(NameListResponse.class).getNames().length);
+ }
+
@Test
public void testListTagsForObjectUnderHierarchicalSchema() {
// Hierarchical (multi-level) schema "a:b:c" using the default separator
":". Its ancestor
@@ -915,7 +970,6 @@ public class TestMetadataObjectTagOperations extends
BaseOperationsTest {
public void testAssociateTagsForObject() {
String[] tagsToAdd = new String[] {"tag1", "tag2"};
String[] tagsToRemove = new String[] {"tag3", "tag4"};
-
MetadataObject catalog = MetadataObjects.parse("object1",
MetadataObject.Type.CATALOG);
when(tagManager.associateTagsForMetadataObject(metalake, catalog,
tagsToAdd, tagsToRemove))
.thenReturn(tagsToAdd);
@@ -1017,7 +1071,8 @@ public class TestMetadataObjectTagOperations extends
BaseOperationsTest {
// Test throw RuntimeException
doThrow(new RuntimeException("mock error"))
.when(tagManager)
- .associateTagsForMetadataObject(any(), any(), any(), any());
+ .associateTagsForMetadataObject(
+ any(String.class), any(MetadataObject.class), any(String[].class),
any(String[].class));
Response response5 =
target(basePath(metalake))
@@ -1036,6 +1091,78 @@ public class TestMetadataObjectTagOperations extends
BaseOperationsTest {
Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResponse1.getType());
}
+ @Test
+ public void testAssociateTagValuesForObjectV2() {
+ MetadataObject catalog = MetadataObjects.parse("object1",
MetadataObject.Type.CATALOG);
+ TagValue[] tagsToAdd = {TagValue.noValue("pii"),
TagValue.of("data_domain", "finance")};
+ TagValue[] tagsToRemove = {TagValue.of("data_domain", "old")};
+ when(tagManager.associateTagValuesForMetadataObject(metalake, catalog,
tagsToAdd, tagsToRemove))
+ .thenReturn(new String[] {"pii", "data_domain"});
+
+ TagValuesAssociateRequest request = new
TagValuesAssociateRequest(tagsToAdd, tagsToRemove);
+ Response response =
+ target(basePath(metalake))
+ .path(catalog.type().toString())
+ .path(catalog.fullName())
+ .path("tags")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v2+json")
+ .post(Entity.entity(request, "application/vnd.gravitino.v2+json"));
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response.getStatus());
+ Assertions.assertEquals(
+ MediaType.valueOf("application/vnd.gravitino.v2+json"),
response.getMediaType());
+ Assertions.assertArrayEquals(
+ new String[] {"pii", "data_domain"},
+ response.readEntity(NameListResponse.class).getNames());
+ }
+
+ @Test
+ public void testV2ErrorMediaType() {
+ MetadataObject catalog = MetadataObjects.parse("object1",
MetadataObject.Type.CATALOG);
+ TagValuesAssociateRequest request = new TagValuesAssociateRequest(null,
null);
+
+ Response response =
+ target(basePath(metalake))
+ .path(catalog.type().toString())
+ .path(catalog.fullName())
+ .path("tags")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v2+json")
+ .post(Entity.entity(request, "application/vnd.gravitino.v2+json"));
+
+ Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
response.getStatus());
+ Assertions.assertEquals(
+ MediaType.valueOf("application/vnd.gravitino.v2+json"),
response.getMediaType());
+ }
+
+ @Test
+ public void testAssociateTagsRejectsRequestShapeFromOtherVersion() {
+ MetadataObject catalog = MetadataObjects.parse("object1",
MetadataObject.Type.CATALOG);
+ String v1Json = "{\"tagsToAdd\":[\"data_domain\"]}";
+ String v2Json =
"{\"tagsToAdd\":[{\"name\":\"data_domain\",\"value\":\"finance\"}]}";
+
+ Response v1WithV2Shape =
+ target(basePath(metalake))
+ .path(catalog.type().toString())
+ .path(catalog.fullName())
+ .path("tags")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(v2Json, MediaType.APPLICATION_JSON_TYPE));
+ Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
v1WithV2Shape.getStatus());
+
+ Response v2WithV1Shape =
+ target(basePath(metalake))
+ .path(catalog.type().toString())
+ .path(catalog.fullName())
+ .path("tags")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v2+json")
+ .post(Entity.entity(v1Json, "application/vnd.gravitino.v2+json"));
+ Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
v2WithV1Shape.getStatus());
+ }
+
private String basePath(String metalake) {
return "/metalakes/" + metalake + "/objects";
}
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestTagOperations.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestTagOperations.java
index 14c19791ba..6127bb77e5 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestTagOperations.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestTagOperations.java
@@ -67,6 +67,7 @@ import org.apache.gravitino.tag.Tag;
import org.apache.gravitino.tag.TagChange;
import org.apache.gravitino.tag.TagDispatcher;
import org.apache.gravitino.tag.TagManager;
+import org.apache.gravitino.tag.TagValueConstraint;
import org.glassfish.jersey.internal.inject.AbstractBinder;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.TestProperties;
@@ -280,7 +281,9 @@ public class TestTagOperations extends BaseOperationsTest {
.withComment("tag1 comment")
.withAuditInfo(testAuditInfo1)
.build();
- when(tagManager.createTag(metalake, "tag1", "tag1 comment",
null)).thenReturn(tag1);
+ when(tagManager.createTag(
+ metalake, "tag1", "tag1 comment", null,
TagValueConstraint.anyValue()))
+ .thenReturn(tag1);
TagCreateRequest request = new TagCreateRequest("tag1", "tag1 comment",
null);
Response resp =
@@ -303,7 +306,7 @@ public class TestTagOperations extends BaseOperationsTest {
// Test throw TagAlreadyExistsException
doThrow(new TagAlreadyExistsException("mock error"))
.when(tagManager)
- .createTag(any(), any(), any(), any());
+ .createTag(any(), any(), any(), any(), any());
Response resp1 =
target(tagPath(metalake))
.request(MediaType.APPLICATION_JSON_TYPE)
@@ -319,7 +322,7 @@ public class TestTagOperations extends BaseOperationsTest {
// Test throw RuntimeException
doThrow(new RuntimeException("mock error"))
.when(tagManager)
- .createTag(any(), any(), any(), any());
+ .createTag(any(), any(), any(), any(), any());
Response resp2 =
target(tagPath(metalake))
@@ -953,7 +956,6 @@ public class TestTagOperations extends BaseOperationsTest {
public void testAssociateTagsForObject() {
String[] tagsToAdd = new String[] {"tag1", "tag2"};
String[] tagsToRemove = new String[] {"tag3", "tag4"};
-
MetadataObject catalog = MetadataObjects.parse("object1",
MetadataObject.Type.CATALOG);
when(tagManager.associateTagsForMetadataObject(metalake, catalog,
tagsToAdd, tagsToRemove))
.thenReturn(tagsToAdd);
@@ -1015,7 +1017,8 @@ public class TestTagOperations extends BaseOperationsTest
{
// Test throw RuntimeException
doThrow(new RuntimeException("mock error"))
.when(tagManager)
- .associateTagsForMetadataObject(any(), any(), any(), any());
+ .associateTagsForMetadataObject(
+ any(String.class), any(MetadataObject.class), any(String[].class),
any(String[].class));
Response response3 =
target(tagPath(metalake))
@@ -1043,7 +1046,7 @@ public class TestTagOperations extends BaseOperationsTest
{
MetadataObjects.parse("object1.object2.object3.object4",
MetadataObject.Type.COLUMN)
};
- when(tagManager.listMetadataObjectsForTag(metalake,
"tag1")).thenReturn(objects);
+ when(tagManager.listMetadataObjectsForTag(metalake, "tag1",
null)).thenReturn(objects);
Response response =
target(tagPath(metalake))
@@ -1068,10 +1071,43 @@ public class TestTagOperations extends
BaseOperationsTest {
Assertions.assertEquals(objects[i].fullName(),
respObjects[i].fullName());
}
+ when(tagManager.listMetadataObjectsForTag(metalake, "tag1",
"finance")).thenReturn(objects);
+ Response filteredResponse =
+ target(tagPath(metalake))
+ .path("tag1")
+ .path("objects")
+ .queryParam("value", "finance")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
filteredResponse.getStatus());
+
+ Response emptyValueResponse =
+ target(tagPath(metalake))
+ .path("tag1")
+ .path("objects")
+ .queryParam("value", "")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+ Assertions.assertEquals(
+ Response.Status.BAD_REQUEST.getStatusCode(),
emptyValueResponse.getStatus());
+
+ Response tooLongValueResponse =
+ target(tagPath(metalake))
+ .path("tag1")
+ .path("objects")
+ .queryParam("value", "v".repeat(257))
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+ Assertions.assertEquals(
+ Response.Status.BAD_REQUEST.getStatusCode(),
tooLongValueResponse.getStatus());
+
// Test throw NoSuchTagException
doThrow(new NoSuchTagException("mock error"))
.when(tagManager)
- .listMetadataObjectsForTag(metalake, "tag1");
+ .listMetadataObjectsForTag(metalake, "tag1", null);
Response response1 =
target(tagPath(metalake))
@@ -1090,7 +1126,7 @@ public class TestTagOperations extends BaseOperationsTest
{
// Test throw RuntimeException
doThrow(new RuntimeException("mock error"))
.when(tagManager)
- .listMetadataObjectsForTag(any(), any());
+ .listMetadataObjectsForTag(any(), any(), any());
Response response2 =
target(tagPath(metalake))