This is an automated email from the ASF dual-hosted git repository.
jshao pushed a commit to branch branch-0.9
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-0.9 by this push:
new 07914561b3 [#6893] feat(server): support fileset multiple locations in
REST API (#6933)
07914561b3 is described below
commit 07914561b3428e17ebfa6f0bb9d85d1a73522f68
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Apr 15 15:21:35 2025 +0800
[#6893] feat(server): support fileset multiple locations in REST API (#6933)
### What changes were proposed in this pull request?
support fileset multiple locations in REST API
### Why are the changes needed?
support fileset multiple locations in REST API
Fix: #6893
### Does this PR introduce _any_ user-facing change?
yes
### How was this patch tested?
tests added
Co-authored-by: mchades <[email protected]>
---
.../gravitino/client/TestFilesetCatalog.java | 5 +-
.../gravitino/client/TestSupportCredentials.java | 4 +-
.../apache/gravitino/client/TestSupportRoles.java | 4 +-
.../apache/gravitino/client/TestSupportTags.java | 4 +-
.../filesystem/hadoop/GravitinoMockServerBase.java | 7 +-
.../gravitino/filesystem/hadoop/TestGvfsBase.java | 3 +-
.../org/apache/gravitino/dto/file/FilesetDTO.java | 156 ++++++++++++++++++---
.../dto/requests/FilesetCreateRequest.java | 4 +
.../apache/gravitino/dto/util/DTOConverters.java | 2 +-
.../apache/gravitino/dto/file/TestFilesetDTO.java | 98 +++++++++++++
docs/open-api/filesets.yaml | 15 ++
.../server/web/rest/FilesetOperations.java | 31 +++-
.../server/web/rest/TestFilesetOperations.java | 135 ++++++++++++++++--
13 files changed, 423 insertions(+), 45 deletions(-)
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestFilesetCatalog.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestFilesetCatalog.java
index f45adfab5f..6597ed5c59 100644
---
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestFilesetCatalog.java
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestFilesetCatalog.java
@@ -18,6 +18,7 @@
*/
package org.apache.gravitino.client;
+import static org.apache.gravitino.file.Fileset.LOCATION_NAME_UNKNOWN;
import static org.apache.hc.core5.http.HttpStatus.SC_CONFLICT;
import static org.apache.hc.core5.http.HttpStatus.SC_NOT_FOUND;
import static org.apache.hc.core5.http.HttpStatus.SC_OK;
@@ -544,11 +545,13 @@ public class TestFilesetCatalog extends TestBase {
String comment,
String location,
Map<String, String> properties) {
+ Map<String, String> locations =
+ location == null ? ImmutableMap.of() :
ImmutableMap.of(LOCATION_NAME_UNKNOWN, location);
return FilesetDTO.builder()
.name(name)
.type(type)
.comment(comment)
- .storageLocation(location)
+ .storageLocations(locations)
.properties(properties)
.audit(AuditDTO.builder().withCreator("creator").withCreateTime(Instant.now()).build())
.build();
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportCredentials.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportCredentials.java
index 842af4a640..b2f305fbf0 100644
---
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportCredentials.java
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportCredentials.java
@@ -18,10 +18,12 @@
*/
package org.apache.gravitino.client;
+import static org.apache.gravitino.file.Fileset.LOCATION_NAME_UNKNOWN;
import static org.apache.hc.core5.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import static org.apache.hc.core5.http.HttpStatus.SC_OK;
import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableMap;
import java.util.Collections;
import java.util.Locale;
import org.apache.gravitino.Catalog;
@@ -74,7 +76,7 @@ public class TestSupportCredentials extends TestBase {
.name("fileset1")
.comment("comment1")
.type(Fileset.Type.EXTERNAL)
- .storageLocation("s3://bucket/path")
+ .storageLocations(ImmutableMap.of(LOCATION_NAME_UNKNOWN,
"s3://bucket/path"))
.properties(Collections.emptyMap())
.audit(AuditDTO.builder().withCreator("test").build())
.build(),
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportRoles.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportRoles.java
index b22d5b21b4..ceb6bec575 100644
---
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportRoles.java
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportRoles.java
@@ -18,11 +18,13 @@
*/
package org.apache.gravitino.client;
+import static org.apache.gravitino.file.Fileset.LOCATION_NAME_UNKNOWN;
import static org.apache.hc.core5.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import static org.apache.hc.core5.http.HttpStatus.SC_NOT_FOUND;
import static org.apache.hc.core5.http.HttpStatus.SC_OK;
import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableMap;
import java.util.Collections;
import java.util.Locale;
import org.apache.gravitino.Catalog;
@@ -142,7 +144,7 @@ public class TestSupportRoles extends TestBase {
.name("fileset1")
.comment("comment1")
.type(Fileset.Type.EXTERNAL)
- .storageLocation("s3://bucket/path")
+ .storageLocations(ImmutableMap.of(LOCATION_NAME_UNKNOWN,
"s3://bucket/path"))
.properties(Collections.emptyMap())
.audit(AuditDTO.builder().withCreator("test").build())
.build(),
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 3d903a972c..157f69191e 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
@@ -18,11 +18,13 @@
*/
package org.apache.gravitino.client;
+import static org.apache.gravitino.file.Fileset.LOCATION_NAME_UNKNOWN;
import static org.apache.hc.core5.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import static org.apache.hc.core5.http.HttpStatus.SC_NOT_FOUND;
import static org.apache.hc.core5.http.HttpStatus.SC_OK;
import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableMap;
import java.util.Collections;
import java.util.Locale;
import org.apache.gravitino.Catalog;
@@ -152,7 +154,7 @@ public class TestSupportTags extends TestBase {
.name("fileset1")
.comment("comment1")
.type(Fileset.Type.EXTERNAL)
- .storageLocation("s3://bucket/path")
+ .storageLocations(ImmutableMap.of(LOCATION_NAME_UNKNOWN,
"s3://bucket/path"))
.properties(Collections.emptyMap())
.audit(AuditDTO.builder().withCreator("test").build())
.build(),
diff --git
a/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/GravitinoMockServerBase.java
b/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/GravitinoMockServerBase.java
index da2be0caef..531cb37eb1 100644
---
a/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/GravitinoMockServerBase.java
+++
b/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/GravitinoMockServerBase.java
@@ -18,6 +18,7 @@
*/
package org.apache.gravitino.filesystem.hadoop;
+import static org.apache.gravitino.file.Fileset.LOCATION_NAME_UNKNOWN;
import static org.apache.hc.core5.http.HttpStatus.SC_OK;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -184,11 +185,15 @@ public abstract class GravitinoMockServerBase {
String.format(
"/api/metalakes/%s/catalogs/%s/schemas/%s/filesets/%s",
metalakeName, catalogName, schemaName, filesetName);
+ Map<String, String> locations =
+ location == null
+ ? Collections.emptyMap()
+ : ImmutableMap.of(LOCATION_NAME_UNKNOWN, location);
FilesetDTO mockFileset =
FilesetDTO.builder()
.name(fileset.name())
.type(type)
- .storageLocation(location)
+ .storageLocations(locations)
.comment("comment")
.properties(ImmutableMap.of("k1", "v1"))
.audit(AuditDTO.builder().withCreator("creator").withCreateTime(Instant.now()).build())
diff --git
a/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/TestGvfsBase.java
b/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/TestGvfsBase.java
index be30e42a4b..9c8a863eaa 100644
---
a/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/TestGvfsBase.java
+++
b/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/TestGvfsBase.java
@@ -18,6 +18,7 @@
*/
package org.apache.gravitino.filesystem.hadoop;
+import static org.apache.gravitino.file.Fileset.LOCATION_NAME_UNKNOWN;
import static org.apache.hc.core5.http.HttpStatus.SC_OK;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -341,7 +342,7 @@ public class TestGvfsBase extends GravitinoMockServerBase {
.comment("comment")
.type(Fileset.Type.MANAGED)
.audit(AuditDTO.builder().build())
- .storageLocation(filesetLocation.toString())
+ .storageLocations(ImmutableMap.of(LOCATION_NAME_UNKNOWN,
filesetLocation))
.build());
CredentialResponse credentialResponse = new CredentialResponse(new
CredentialDTO[] {});
diff --git a/common/src/main/java/org/apache/gravitino/dto/file/FilesetDTO.java
b/common/src/main/java/org/apache/gravitino/dto/file/FilesetDTO.java
index 7630b4c8d0..eb6a4ffef2 100644
--- a/common/src/main/java/org/apache/gravitino/dto/file/FilesetDTO.java
+++ b/common/src/main/java/org/apache/gravitino/dto/file/FilesetDTO.java
@@ -20,20 +20,16 @@ package org.apache.gravitino.dto.file;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
-import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
import lombok.EqualsAndHashCode;
-import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.dto.AuditDTO;
import org.apache.gravitino.file.Fileset;
/** Represents a Fileset DTO (Data Transfer Object). */
-@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
-@AllArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode
public class FilesetDTO implements Fileset {
@@ -49,12 +45,34 @@ public class FilesetDTO implements Fileset {
@JsonProperty("storageLocation")
private String storageLocation;
+ @JsonProperty("storageLocations")
+ private Map<String, String> storageLocations;
+
@JsonProperty("properties")
private Map<String, String> properties;
@JsonProperty("audit")
private AuditDTO audit;
+ private FilesetDTO() {}
+
+ private FilesetDTO(
+ String name,
+ String comment,
+ Type type,
+ String storageLocation,
+ Map<String, String> storageLocations,
+ Map<String, String> properties,
+ AuditDTO audit) {
+ this.name = name;
+ this.comment = comment;
+ this.type = type;
+ this.storageLocation = storageLocation;
+ this.storageLocations = storageLocations;
+ this.properties = properties;
+ this.audit = audit;
+ }
+
@Override
public String name() {
return name;
@@ -72,8 +90,8 @@ public class FilesetDTO implements Fileset {
}
@Override
- public String storageLocation() {
- return storageLocation;
+ public Map<String, String> storageLocations() {
+ return storageLocations;
}
@Override
@@ -86,18 +104,116 @@ public class FilesetDTO implements Fileset {
return audit;
}
- @Builder(builderMethodName = "builder")
- private static FilesetDTO internalBuilder(
- String name,
- String comment,
- Type type,
- String storageLocation,
- Map<String, String> properties,
- AuditDTO audit) {
- Preconditions.checkArgument(StringUtils.isNotBlank(name), "name cannot be
null or empty");
- Preconditions.checkNotNull(type, "type cannot be null");
- Preconditions.checkNotNull(audit, "audit cannot be null");
+ /**
+ * Create a new FilesetDTO builder.
+ *
+ * @return A new FilesetDTO builder.
+ */
+ public static FilesetDTOBuilder builder() {
+ return new FilesetDTOBuilder();
+ }
- return new FilesetDTO(name, comment, type, storageLocation, properties,
audit);
+ /** Builder for FilesetDTO. */
+ public static class FilesetDTOBuilder {
+ private String name;
+ private String comment;
+ private Type type;
+ private Map<String, String> storageLocations = new HashMap<>();
+ private Map<String, String> properties;
+ private AuditDTO audit;
+
+ private FilesetDTOBuilder() {}
+
+ /**
+ * Set the name of the fileset.
+ *
+ * @param name The name of the fileset.
+ * @return The builder instance.
+ */
+ public FilesetDTOBuilder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /**
+ * Set the comment of the fileset.
+ *
+ * @param comment The comment of the fileset.
+ * @return The builder instance.
+ */
+ public FilesetDTOBuilder comment(String comment) {
+ this.comment = comment;
+ return this;
+ }
+
+ /**
+ * Set the type of the fileset.
+ *
+ * @param type The type of the fileset.
+ * @return The builder instance.
+ */
+ public FilesetDTOBuilder type(Type type) {
+ this.type = type;
+ return this;
+ }
+
+ /**
+ * Set the storage locations of the fileset.
+ *
+ * @param storageLocations The storage locations of the fileset.
+ * @return The builder instance.
+ */
+ public FilesetDTOBuilder storageLocations(Map<String, String>
storageLocations) {
+ this.storageLocations = ImmutableMap.copyOf(storageLocations);
+ return this;
+ }
+
+ /**
+ * Set the properties of the fileset.
+ *
+ * @param properties The properties of the fileset.
+ * @return The builder instance.
+ */
+ public FilesetDTOBuilder properties(Map<String, String> properties) {
+ this.properties = properties;
+ return this;
+ }
+
+ /**
+ * Set the audit information of the fileset.
+ *
+ * @param audit The audit information of the fileset.
+ * @return The builder instance.
+ */
+ public FilesetDTOBuilder audit(AuditDTO audit) {
+ this.audit = audit;
+ return this;
+ }
+
+ /**
+ * Build the FilesetDTO.
+ *
+ * @return The built FilesetDTO.
+ */
+ public FilesetDTO build() {
+ Preconditions.checkArgument(StringUtils.isNotBlank(name), "name cannot
be null or empty");
+ Preconditions.checkArgument(
+ !storageLocations.isEmpty(),
+ "storage locations cannot be empty. At least one location is
required.");
+ storageLocations.forEach(
+ (n, l) -> {
+ Preconditions.checkArgument(StringUtils.isNotBlank(n), "location
name cannot be empty");
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(l), "storage location cannot be empty");
+ });
+ return new FilesetDTO(
+ name,
+ comment,
+ type,
+ storageLocations.get(LOCATION_NAME_UNKNOWN),
+ storageLocations,
+ properties,
+ audit);
+ }
}
}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/requests/FilesetCreateRequest.java
b/common/src/main/java/org/apache/gravitino/dto/requests/FilesetCreateRequest.java
index bd28189787..f4c79bca34 100644
---
a/common/src/main/java/org/apache/gravitino/dto/requests/FilesetCreateRequest.java
+++
b/common/src/main/java/org/apache/gravitino/dto/requests/FilesetCreateRequest.java
@@ -56,6 +56,10 @@ public class FilesetCreateRequest implements RESTRequest {
@JsonProperty("storageLocation")
private String storageLocation;
+ @Nullable
+ @JsonProperty("storageLocations")
+ private Map<String, String> storageLocations;
+
@Nullable
@JsonProperty("properties")
private Map<String, String> properties;
diff --git
a/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
b/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
index ce63398e60..5dd503ce84 100644
--- a/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
+++ b/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
@@ -612,7 +612,7 @@ public class DTOConverters {
.name(fileset.name())
.comment(fileset.comment())
.type(fileset.type())
- .storageLocation(fileset.storageLocation())
+ .storageLocations(fileset.storageLocations())
.properties(fileset.properties())
.audit(toDTO(fileset.auditInfo()))
.build();
diff --git
a/common/src/test/java/org/apache/gravitino/dto/file/TestFilesetDTO.java
b/common/src/test/java/org/apache/gravitino/dto/file/TestFilesetDTO.java
new file mode 100644
index 0000000000..eb43552a73
--- /dev/null
+++ b/common/src/test/java/org/apache/gravitino/dto/file/TestFilesetDTO.java
@@ -0,0 +1,98 @@
+/*
+ * 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.dto.file;
+
+import static org.apache.gravitino.file.Fileset.LOCATION_NAME_UNKNOWN;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableMap;
+import java.time.Instant;
+import java.util.Map;
+import org.apache.gravitino.dto.AuditDTO;
+import org.apache.gravitino.json.JsonUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestFilesetDTO {
+
+ @Test
+ public void testFilesetSerDe() throws JsonProcessingException {
+ AuditDTO audit =
AuditDTO.builder().withCreator("user1").withCreateTime(Instant.now()).build();
+ Map<String, String> props = ImmutableMap.of("key", "value");
+
+ // test with default location
+ FilesetDTO filesetDTO =
+ FilesetDTO.builder()
+ .name("fileset_test")
+ .comment("model comment")
+ .storageLocations(ImmutableMap.of(LOCATION_NAME_UNKNOWN, "/a/b/c",
"v1", "/d/e/f"))
+ .properties(props)
+ .audit(audit)
+ .build();
+ Assertions.assertEquals("fileset_test", filesetDTO.name());
+ Assertions.assertEquals("model comment", filesetDTO.comment());
+ Assertions.assertEquals(
+ ImmutableMap.of(LOCATION_NAME_UNKNOWN, "/a/b/c", "v1", "/d/e/f"),
+ filesetDTO.storageLocations());
+ Assertions.assertEquals("/a/b/c", filesetDTO.storageLocation());
+ Assertions.assertEquals(props, filesetDTO.properties());
+ Assertions.assertEquals(audit, filesetDTO.auditInfo());
+
+ String serJson = JsonUtils.objectMapper().writeValueAsString(filesetDTO);
+ FilesetDTO deserFilesetDTO = JsonUtils.objectMapper().readValue(serJson,
FilesetDTO.class);
+ Assertions.assertEquals(filesetDTO, deserFilesetDTO);
+
+ // test without default location exception
+ Exception exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
FilesetDTO.builder().name("test").build());
+ Assertions.assertEquals(
+ "storage locations cannot be empty. At least one location is
required.",
+ exception.getMessage());
+
+ // test empty location name exception
+ exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> FilesetDTO.builder().storageLocations(ImmutableMap.of("",
"/a/b/c")).build());
+ Assertions.assertEquals("name cannot be null or empty",
exception.getMessage());
+
+ // test empty default location
+ exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ FilesetDTO.builder()
+ .name("test")
+ .storageLocations(ImmutableMap.of(LOCATION_NAME_UNKNOWN,
""))
+ .build());
+ Assertions.assertEquals("storage location cannot be empty",
exception.getMessage());
+
+ // test empty location
+ exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ FilesetDTO.builder()
+ .name("test")
+ .storageLocations(ImmutableMap.of(LOCATION_NAME_UNKNOWN,
"/a/b/c", "v1", ""))
+ .build());
+ Assertions.assertEquals("storage location cannot be empty",
exception.getMessage());
+ }
+}
diff --git a/docs/open-api/filesets.yaml b/docs/open-api/filesets.yaml
index f2b3fe8359..db5c5b190d 100644
--- a/docs/open-api/filesets.yaml
+++ b/docs/open-api/filesets.yaml
@@ -177,6 +177,13 @@ paths:
schema:
type: string
description: The sub path to the file or directory
+ - name: location_name
+ in: query
+ required: false
+ schema:
+ type: string
+ default: "default"
+ description: The location name in the fileset
responses:
"200":
$ref: "#/components/responses/FileLocationResponse"
@@ -249,6 +256,14 @@ components:
description: The location of the fileset. If the storage location of
managed fileset is empty, it will \
use the location of namespace. The storage location of external
fileset must be set.
nullable: true
+ storageLocations:
+ type: object
+ description: The storage locations of the fileset. If the storage
location of managed fileset is empty, \
+ it will use the location of namespace. The storage locations of
external fileset must be set.
+ nullable: true
+ default: { }
+ additionalProperties:
+ type: string
properties:
type: object
description: The properties of the fileset. Can be empty.
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/FilesetOperations.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/FilesetOperations.java
index 713ba0c8eb..06342d4131 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/FilesetOperations.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/FilesetOperations.java
@@ -18,8 +18,13 @@
*/
package org.apache.gravitino.server.web.rest;
+import static org.apache.gravitino.file.Fileset.LOCATION_NAME_UNKNOWN;
+
import com.codahale.metrics.annotation.ResponseMetered;
import com.codahale.metrics.annotation.Timed;
+import com.google.common.collect.ImmutableMap;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
@@ -124,12 +129,21 @@ public class FilesetOperations {
NameIdentifier ident =
NameIdentifierUtil.ofFileset(metalake, catalog, schema,
request.getName());
+ // set storageLocation value as unnamed location if provided
+ Map<String, String> tmpLocations =
+ new HashMap<>(
+ Optional.ofNullable(request.getStorageLocations())
+ .orElse(Collections.emptyMap()));
+ Optional.ofNullable(request.getStorageLocation())
+ .ifPresent(loc -> tmpLocations.put(LOCATION_NAME_UNKNOWN,
loc));
+ Map<String, String> storageLocations =
ImmutableMap.copyOf(tmpLocations);
+
Fileset fileset =
- dispatcher.createFileset(
+ dispatcher.createMultipleLocationFileset(
ident,
request.getComment(),
Optional.ofNullable(request.getType()).orElse(Fileset.Type.MANAGED),
- request.getStorageLocation(),
+ storageLocations,
request.getProperties());
Response response = Utils.ok(new
FilesetResponse(DTOConverters.toDTO(fileset)));
LOG.info("Fileset created: {}.{}.{}.{}", metalake, catalog,
schema, request.getName());
@@ -242,14 +256,16 @@ public class FilesetOperations {
@PathParam("catalog") String catalog,
@PathParam("schema") String schema,
@PathParam("fileset") String fileset,
- @QueryParam("sub_path") @NotNull String subPath) {
+ @QueryParam("sub_path") @NotNull String subPath,
+ @QueryParam("location_name") String locationName) {
LOG.info(
- "Received get file location request: {}.{}.{}.{}, sub path:{}",
+ "Received get file location request: {}.{}.{}.{}, sub path:{},
location name:{}",
metalake,
catalog,
schema,
fileset,
- RESTUtils.decodeString(subPath));
+ RESTUtils.decodeString(subPath),
+
Optional.ofNullable(locationName).map(RESTUtils::decodeString).orElse(null));
try {
return Utils.doAs(
httpRequest,
@@ -263,7 +279,10 @@ public class FilesetOperations {
CallerContext.CallerContextHolder.set(context);
}
String actualFileLocation =
- dispatcher.getFileLocation(ident,
RESTUtils.decodeString(subPath));
+ dispatcher.getFileLocation(
+ ident,
+ RESTUtils.decodeString(subPath),
+
Optional.ofNullable(locationName).map(RESTUtils::decodeString).orElse(null));
return Utils.ok(new FileLocationResponse(actualFileLocation));
});
} catch (Exception e) {
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestFilesetOperations.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestFilesetOperations.java
index 4258346e49..f8248bc1d8 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestFilesetOperations.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestFilesetOperations.java
@@ -21,6 +21,7 @@ package org.apache.gravitino.server.web.rest;
import static org.apache.gravitino.Configs.TREE_LOCK_CLEAN_INTERVAL;
import static org.apache.gravitino.Configs.TREE_LOCK_MAX_NODE_IN_MEMORY;
import static org.apache.gravitino.Configs.TREE_LOCK_MIN_NODE_IN_MEMORY;
+import static org.apache.gravitino.file.Fileset.LOCATION_NAME_UNKNOWN;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
@@ -60,6 +61,7 @@ import
org.apache.gravitino.dto.responses.FileLocationResponse;
import org.apache.gravitino.dto.responses.FilesetResponse;
import org.apache.gravitino.exceptions.FilesetAlreadyExistsException;
import org.apache.gravitino.exceptions.NoSuchFilesetException;
+import org.apache.gravitino.exceptions.NoSuchLocationNameException;
import org.apache.gravitino.exceptions.NoSuchSchemaException;
import org.apache.gravitino.file.Fileset;
import org.apache.gravitino.file.FilesetChange;
@@ -204,6 +206,8 @@ public class TestFilesetOperations extends JerseyTest {
Assertions.assertEquals(fileset.type(), filesetDTO.type());
Assertions.assertEquals(fileset.comment(), filesetDTO.comment());
Assertions.assertEquals(fileset.properties(), filesetDTO.properties());
+ Assertions.assertEquals(fileset.storageLocation(),
filesetDTO.storageLocation());
+ Assertions.assertEquals(fileset.storageLocations(),
filesetDTO.storageLocations());
// Test throw NoSuchFilesetException
doThrow(new NoSuchFilesetException("no
found")).when(dispatcher).loadFileset(any());
@@ -242,7 +246,8 @@ public class TestFilesetOperations extends JerseyTest {
"mock comment",
"mock location",
ImmutableMap.of("k1", "v1"));
- when(dispatcher.createFileset(any(), any(), any(), any(),
any())).thenReturn(fileset);
+ when(dispatcher.createMultipleLocationFileset(any(), any(), any(), any(),
any()))
+ .thenReturn(fileset);
FilesetCreateRequest req =
FilesetCreateRequest.builder()
@@ -268,12 +273,59 @@ public class TestFilesetOperations extends JerseyTest {
Assertions.assertEquals("mock comment", filesetDTO.comment());
Assertions.assertEquals(Fileset.Type.MANAGED, filesetDTO.type());
Assertions.assertEquals("mock location", filesetDTO.storageLocation());
+ Assertions.assertEquals(
+ ImmutableMap.of(LOCATION_NAME_UNKNOWN, "mock location"),
filesetDTO.storageLocations());
Assertions.assertEquals(ImmutableMap.of("k1", "v1"),
filesetDTO.properties());
+ // test multiple locations
+ Map<String, String> locations =
+ ImmutableMap.of(
+ LOCATION_NAME_UNKNOWN,
+ "mock default",
+ "location1",
+ "mock location1",
+ "location2",
+ "mock location2");
+ fileset =
+ mockMultipleLocationsFileset(
+ "fileset1",
+ Fileset.Type.MANAGED,
+ "mock comment",
+ locations,
+ ImmutableMap.of("k1", "v1"));
+ when(dispatcher.createMultipleLocationFileset(any(), any(), any(), any(),
any()))
+ .thenReturn(fileset);
+
+ req =
+ FilesetCreateRequest.builder()
+ .name("fileset1")
+ .comment("mock comment")
+ .storageLocations(locations)
+ .properties(ImmutableMap.of("k1", "v1"))
+ .build();
+
+ resp =
+ target(filesetPath(metalake, catalog, schema))
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp.getStatus());
+
+ filesetResp = resp.readEntity(FilesetResponse.class);
+ Assertions.assertEquals(0, filesetResp.getCode());
+
+ filesetDTO = filesetResp.getFileset();
+ Assertions.assertEquals("fileset1", filesetDTO.name());
+ Assertions.assertEquals("mock comment", filesetDTO.comment());
+ Assertions.assertEquals(Fileset.Type.MANAGED, filesetDTO.type());
+ Assertions.assertEquals("mock default", filesetDTO.storageLocation());
+ Assertions.assertEquals(locations, filesetDTO.storageLocations());
+
// Test throw NoSuchSchemaException
doThrow(new NoSuchSchemaException("mock error"))
.when(dispatcher)
- .createFileset(any(), any(), any(), any(), any());
+ .createMultipleLocationFileset(any(), any(), any(), any(), any());
Response resp1 =
target(filesetPath(metalake, catalog, schema))
@@ -290,7 +342,7 @@ public class TestFilesetOperations extends JerseyTest {
// Test throw FilesetAlreadyExistsException
doThrow(new FilesetAlreadyExistsException("mock error"))
.when(dispatcher)
- .createFileset(any(), any(), any(), any(), any());
+ .createMultipleLocationFileset(any(), any(), any(), any(), any());
Response resp2 =
target(filesetPath(metalake, catalog, schema))
@@ -308,7 +360,7 @@ public class TestFilesetOperations extends JerseyTest {
// Test throw RuntimeException
doThrow(new RuntimeException("mock error"))
.when(dispatcher)
- .createFileset(any(), any(), any(), any(), any());
+ .createMultipleLocationFileset(any(), any(), any(), any(), any());
Response resp3 =
target(filesetPath(metalake, catalog, schema))
@@ -444,7 +496,7 @@ public class TestFilesetOperations extends JerseyTest {
// Test encoded subPath
NameIdentifier fullIdentifier = NameIdentifier.of(metalake, catalog,
schema, "fileset1");
String subPath = "/test/1";
- when(dispatcher.getFileLocation(fullIdentifier,
subPath)).thenReturn(subPath);
+ when(dispatcher.getFileLocation(fullIdentifier, subPath,
null)).thenReturn(subPath);
Response resp =
target(filesetPath(metalake, catalog, schema) + "fileset1/location")
.queryParam("sub_path", RESTUtils.encodeString(subPath))
@@ -458,10 +510,27 @@ public class TestFilesetOperations extends JerseyTest {
Assertions.assertEquals(subPath, contextResponse.getFileLocation());
+ // test specify location name
+ String locationName = "location1";
+ String location1 = "/test/location1";
+ when(dispatcher.getFileLocation(fullIdentifier, subPath,
locationName)).thenReturn(location1);
+ resp =
+ target(filesetPath(metalake, catalog, schema) + "fileset1/location")
+ .queryParam("sub_path", RESTUtils.encodeString(subPath))
+ .queryParam("location_name", locationName)
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp.getStatus());
+
+ contextResponse = resp.readEntity(FileLocationResponse.class);
+ Assertions.assertEquals(0, contextResponse.getCode());
+ Assertions.assertEquals(location1, contextResponse.getFileLocation());
+
// Test throw NoSuchFilesetException
doThrow(new NoSuchFilesetException("no found"))
.when(dispatcher)
- .getFileLocation(fullIdentifier, subPath);
+ .getFileLocation(fullIdentifier, subPath, null);
Response resp1 =
target(filesetPath(metalake, catalog, schema) + "fileset1/location")
.queryParam("sub_path", RESTUtils.encodeString(subPath))
@@ -477,7 +546,7 @@ public class TestFilesetOperations extends JerseyTest {
// Test throw RuntimeException
doThrow(new RuntimeException("internal error"))
.when(dispatcher)
- .getFileLocation(fullIdentifier, subPath);
+ .getFileLocation(fullIdentifier, subPath, null);
Response resp2 =
target(filesetPath(metalake, catalog, schema) + "fileset1/location")
.queryParam("sub_path", RESTUtils.encodeString(subPath))
@@ -494,7 +563,7 @@ public class TestFilesetOperations extends JerseyTest {
// Test not encoded subPath
NameIdentifier fullIdentifier1 = NameIdentifier.of(metalake, catalog,
schema, "fileset2");
String subPath1 = "/test/2";
- when(dispatcher.getFileLocation(fullIdentifier1,
subPath1)).thenReturn(subPath1);
+ when(dispatcher.getFileLocation(fullIdentifier1, subPath1,
null)).thenReturn(subPath1);
Response resp3 =
target(filesetPath(metalake, catalog, schema) + "fileset2/location")
.queryParam("sub_path", subPath1)
@@ -508,12 +577,30 @@ public class TestFilesetOperations extends JerseyTest {
Assertions.assertEquals(subPath1, contextResponse1.getFileLocation());
+ // test throw NoSuchLocationNameException
+ doThrow(new NoSuchLocationNameException("no found"))
+ .when(dispatcher)
+ .getFileLocation(fullIdentifier, subPath, "not_exist");
+ Response noNameResp =
+ target(filesetPath(metalake, catalog, schema) + "fileset1/location")
+ .queryParam("sub_path", RESTUtils.encodeString(subPath))
+ .queryParam("location_name", "not_exist")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+ Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(),
noNameResp.getStatus());
+
+ ErrorResponse errorResp4 = noNameResp.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE,
errorResp4.getCode());
+ Assertions.assertEquals(
+ NoSuchLocationNameException.class.getSimpleName(),
errorResp4.getType());
+
// Test header to caller context
try {
Map<String, String> callerContextMap = Maps.newHashMap();
NameIdentifier fullIdentifier2 = NameIdentifier.of(metalake, catalog,
schema, "fileset3");
String subPath2 = "/test/3";
- when(dispatcher.getFileLocation(fullIdentifier2, subPath2))
+ when(dispatcher.getFileLocation(fullIdentifier2, subPath2, null))
.thenAnswer(
(Answer<String>)
invocation -> {
@@ -561,10 +648,10 @@ public class TestFilesetOperations extends JerseyTest {
CallerContext context =
CallerContext.builder().withContext(testContextMap).build();
CallerContext.CallerContextHolder.set(context);
FilesetOperations mockOperations = Mockito.mock(FilesetOperations.class);
- Mockito.when(mockOperations.getFileLocation(any(), any(), any(), any(),
any()))
+ Mockito.when(mockOperations.getFileLocation(any(), any(), any(), any(),
any(), any()))
.thenCallRealMethod();
mockOperations.getFileLocation(
- "test_metalake", "test_catalog", "test_schema", "fileset4", "/test");
+ "test_metalake", "test_catalog", "test_schema", "fileset4", "/test",
"default");
Assertions.assertNull(CallerContext.CallerContextHolder.get());
}
@@ -610,7 +697,31 @@ public class TestFilesetOperations extends JerseyTest {
when(mockFileset.name()).thenReturn(filesetName);
when(mockFileset.type()).thenReturn(type);
when(mockFileset.comment()).thenReturn(comment);
- when(mockFileset.storageLocation()).thenReturn(storageLocation);
+ when(mockFileset.storageLocation()).thenCallRealMethod();
+ when(mockFileset.storageLocations())
+ .thenReturn(ImmutableMap.of(LOCATION_NAME_UNKNOWN, storageLocation));
+ when(mockFileset.properties()).thenReturn(properties);
+
+ Audit mockAudit = mock(Audit.class);
+ when(mockAudit.creator()).thenReturn("gravitino");
+ when(mockAudit.createTime()).thenReturn(Instant.now());
+ when(mockFileset.auditInfo()).thenReturn(mockAudit);
+
+ return mockFileset;
+ }
+
+ private static Fileset mockMultipleLocationsFileset(
+ String filesetName,
+ Fileset.Type type,
+ String comment,
+ Map<String, String> storageLocations,
+ Map<String, String> properties) {
+ Fileset mockFileset = mock(Fileset.class);
+ when(mockFileset.name()).thenReturn(filesetName);
+ when(mockFileset.type()).thenReturn(type);
+ when(mockFileset.comment()).thenReturn(comment);
+ when(mockFileset.storageLocation()).thenCallRealMethod();
+ when(mockFileset.storageLocations()).thenReturn(storageLocations);
when(mockFileset.properties()).thenReturn(properties);
Audit mockAudit = mock(Audit.class);