This is an automated email from the ASF dual-hosted git repository.

jackie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 777796a3615 Encode column and segment names when constructing metadata 
read URL (#16940)
777796a3615 is described below

commit 777796a36151d73bdc4f7b08e9ece55f213d1ee4
Author: Xiaotian (Jackie) Jiang <17555551+jackie-ji...@users.noreply.github.com>
AuthorDate: Fri Oct 3 11:22:37 2025 -0700

    Encode column and segment names when constructing metadata read URL (#16940)
---
 .../broker/api/resources/PinotBrokerRouting.java   |  7 +-
 .../controller/api/resources/DebugResource.java    |  8 +-
 .../api/resources/PinotSegmentRestletResource.java | 27 +++---
 .../api/resources/PinotTableRestletResource.java   |  4 +-
 .../util/ServerSegmentMetadataReader.java          | 69 +++++++--------
 .../tests/HybridClusterIntegrationTest.java        | 16 +---
 .../tests/OfflineClusterIntegrationTest.java       | 15 ----
 .../server/api/resources/ReingestionResource.java  |  5 +-
 .../api/resources/SegmentMetadataFetcher.java      |  8 +-
 .../pinot/server/api/resources/TablesResource.java | 99 ++++++++--------------
 .../utils/builder/ControllerRequestURLBuilder.java | 57 ++-----------
 .../pinot/spi/utils/builder/UrlBuilderUtils.java   | 75 ++++++++++++++++
 12 files changed, 180 insertions(+), 210 deletions(-)

diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/PinotBrokerRouting.java
 
b/pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/PinotBrokerRouting.java
index 546e155cf0c..dd55158e5ad 100644
--- 
a/pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/PinotBrokerRouting.java
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/PinotBrokerRouting.java
@@ -29,6 +29,7 @@ import io.swagger.annotations.SecurityDefinition;
 import io.swagger.annotations.SwaggerDefinition;
 import javax.inject.Inject;
 import javax.ws.rs.DELETE;
+import javax.ws.rs.Encoded;
 import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
@@ -40,6 +41,7 @@ import org.apache.helix.HelixManager;
 import org.apache.pinot.broker.routing.BrokerRoutingManager;
 import org.apache.pinot.common.metadata.ZKMetadataProvider;
 import org.apache.pinot.common.utils.DatabaseUtils;
+import org.apache.pinot.common.utils.URIUtils;
 import org.apache.pinot.core.auth.Actions;
 import org.apache.pinot.core.auth.Authorize;
 import org.apache.pinot.core.auth.TargetType;
@@ -97,9 +99,10 @@ public class PinotBrokerRouting {
   })
   public String refreshRouting(
       @ApiParam(value = "Table name (with type)") @PathParam("tableName") 
String tableNameWithType,
-      @ApiParam(value = "Segment name") @PathParam("segmentName") String 
segmentName,
+      @ApiParam(value = "Segment name") @PathParam("segmentName") @Encoded 
String segmentName,
       @Context HttpHeaders headers) {
-    
_routingManager.refreshSegment(DatabaseUtils.translateTableName(tableNameWithType,
 headers), segmentName);
+    
_routingManager.refreshSegment(DatabaseUtils.translateTableName(tableNameWithType,
 headers),
+        URIUtils.decode(segmentName));
     return "Success";
   }
 
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/DebugResource.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/DebugResource.java
index 6a2235389c1..106bb3a4aef 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/DebugResource.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/DebugResource.java
@@ -42,6 +42,7 @@ import java.util.function.Function;
 import java.util.stream.Collectors;
 import javax.inject.Inject;
 import javax.ws.rs.DefaultValue;
+import javax.ws.rs.Encoded;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
@@ -63,6 +64,7 @@ import 
org.apache.pinot.common.restlet.resources.SegmentConsumerInfo;
 import org.apache.pinot.common.restlet.resources.SegmentErrorInfo;
 import org.apache.pinot.common.restlet.resources.SegmentServerDebugInfo;
 import org.apache.pinot.common.utils.DatabaseUtils;
+import org.apache.pinot.common.utils.URIUtils;
 import org.apache.pinot.controller.ControllerConf;
 import org.apache.pinot.controller.api.debug.TableDebugInfo;
 import 
org.apache.pinot.controller.api.exception.ControllerApplicationException;
@@ -168,12 +170,12 @@ public class DebugResource {
   })
   public TableDebugInfo.SegmentDebugInfo getSegmentDebugInfo(
       @ApiParam(value = "Name of the table (with type)", required = true) 
@PathParam("tableName")
-          String tableNameWithType,
-      @ApiParam(value = "Name of the segment", required = true) 
@PathParam("segmentName") String segmentName,
+      String tableNameWithType,
+      @ApiParam(value = "Name of the segment", required = true) 
@PathParam("segmentName") @Encoded String segmentName,
       @Context HttpHeaders headers)
       throws Exception {
     tableNameWithType = DatabaseUtils.translateTableName(tableNameWithType, 
headers);
-    return debugSegment(tableNameWithType, segmentName);
+    return debugSegment(tableNameWithType, URIUtils.decode(segmentName));
   }
 
   /**
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSegmentRestletResource.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSegmentRestletResource.java
index 33441ed9067..def5f3fa843 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSegmentRestletResource.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSegmentRestletResource.java
@@ -350,7 +350,7 @@ public class PinotSegmentRestletResource {
 
     if (segmentMetadata != null) {
       Map<String, Object> result = new HashMap<>(segmentMetadata);
-      if (columns.size() > 0) {
+      if (!columns.isEmpty()) {
         JsonNode segmentsMetadataJson = getExtraMetaData(tableName, 
segmentName, columns);
         if (segmentsMetadataJson.has("indexes")) {
           result.put("indexes", segmentsMetadataJson.get("indexes"));
@@ -815,7 +815,7 @@ public class PinotSegmentRestletResource {
     }
     String tableNameWithType =
         
ResourceUtils.getExistingTableNamesWithType(_pinotHelixResourceManager, 
tableName, tableType, LOGGER).get(0);
-    if (segments == null || segments.isEmpty()) {
+    if (segments.isEmpty()) {
       deleteSegmentsInternal(tableNameWithType,
           
_pinotHelixResourceManager.getSegmentsFromPropertyStore(tableNameWithType), 
retentionPeriod);
       return new SuccessResponse("All segments of table " + tableNameWithType 
+ " deleted");
@@ -876,9 +876,9 @@ public class PinotSegmentRestletResource {
           + " specified in the segment lineage entries and cannot be queried 
from the table, false by default")
       @QueryParam("excludeReplacedSegments") @DefaultValue("false") boolean 
excludeReplacedSegments,
       @ApiParam(value = "Start timestamp (inclusive) in milliseconds", 
required = true) @QueryParam("startTimestamp")
-          String startTimestampStr,
+      String startTimestampStr,
       @ApiParam(value = "End timestamp (exclusive) in milliseconds", required 
= true) @QueryParam("endTimestamp")
-          String endTimestampStr,
+      String endTimestampStr,
       @ApiParam(value = "Whether to ignore segments that are partially 
overlapping with the [start, end)"
           + "for deletion, true by default")
       @QueryParam("excludeOverlapping") @DefaultValue("true") boolean 
excludeOverlapping,
@@ -926,12 +926,13 @@ public class PinotSegmentRestletResource {
   public String getServerMetadata(
       @ApiParam(value = "Name of the table", required = true) 
@PathParam("tableName") String tableName,
       @ApiParam(value = "OFFLINE|REALTIME") @QueryParam("type") String 
tableTypeStr,
-      @Encoded @ApiParam(value = "Segments to include (all if not specified)", 
allowMultiple = true)
-      @QueryParam("segments") @Nullable List<String> segments,
-      @Encoded @ApiParam(value = "Columns name", allowMultiple = true) 
@QueryParam("columns")
-      @Nullable List<String> columns, @Context HttpHeaders headers) {
+      @ApiParam(value = "Segments to include (all if not specified)", 
allowMultiple = true) @QueryParam("segments")
+      List<String> segments,
+      @ApiParam(value = "Columns to include (use '*' to include all)", 
allowMultiple = true) @QueryParam("columns")
+      List<String> columns,
+      @Context HttpHeaders headers) {
     tableName = DatabaseUtils.translateTableName(tableName, headers);
-    String segmentCount = (segments == null) ? "all" : 
String.valueOf(segments.size());
+    String segmentCount = segments.isEmpty() ? "all" : 
String.valueOf(segments.size());
     LOGGER.info("Received a request to fetch metadata for {} segments for 
table {}", segmentCount, tableName);
     TableType tableType = Constants.validateTableType(tableTypeStr);
 
@@ -1162,14 +1163,12 @@ public class PinotSegmentRestletResource {
    * @param segments name of the segments to include in metadata
    * @return Map<String, String>  metadata of the table segments -> map of 
segment name to its metadata
    */
-  private JsonNode getSegmentsMetadataFromServer(String tableNameWithType, 
@Nullable List<String> columns,
-      @Nullable List<String> segments)
+  private JsonNode getSegmentsMetadataFromServer(String tableNameWithType, 
List<String> columns, List<String> segments)
       throws InvalidConfigException, IOException {
     TableMetadataReader tableMetadataReader =
         new TableMetadataReader(_executor, _connectionManager, 
_pinotHelixResourceManager);
-    return tableMetadataReader
-        .getSegmentsMetadata(tableNameWithType, columns, segments,
-            _controllerConf.getServerAdminRequestTimeoutSeconds() * 1000);
+    return tableMetadataReader.getSegmentsMetadata(tableNameWithType, columns, 
segments,
+        _controllerConf.getServerAdminRequestTimeoutSeconds() * 1000);
   }
 
   @POST
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotTableRestletResource.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotTableRestletResource.java
index 7f52166d24c..505216602fb 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotTableRestletResource.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotTableRestletResource.java
@@ -1097,8 +1097,8 @@ public class PinotTableRestletResource {
   public String getTableAggregateMetadata(
       @ApiParam(value = "Name of the table", required = true) 
@PathParam("tableName") String tableName,
       @ApiParam(value = "OFFLINE|REALTIME") @QueryParam("type") String 
tableTypeStr,
-      @ApiParam(value = "Columns name", allowMultiple = true) 
@QueryParam("columns") @DefaultValue("")
-      List<String> columns, @Context HttpHeaders headers) {
+      @ApiParam(value = "Columns name", allowMultiple = true) 
@QueryParam("columns") List<String> columns,
+      @Context HttpHeaders headers) {
     tableName = DatabaseUtils.translateTableName(tableName, headers);
     LOGGER.info("Received a request to fetch aggregate metadata for a table 
{}", tableName);
     TableType tableType = Constants.validateTableType(tableTypeStr);
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/util/ServerSegmentMetadataReader.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/util/ServerSegmentMetadataReader.java
index c0ebfd5313c..53cf628c7fb 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/util/ServerSegmentMetadataReader.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/util/ServerSegmentMetadataReader.java
@@ -39,7 +39,6 @@ import javax.annotation.Nullable;
 import javax.ws.rs.client.ClientBuilder;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
-import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
 import org.apache.hc.client5.http.io.HttpClientConnectionManager;
@@ -51,6 +50,7 @@ import org.apache.pinot.common.utils.RoaringBitmapUtils;
 import org.apache.pinot.controller.api.resources.TableStaleSegmentResponse;
 import org.apache.pinot.segment.local.data.manager.StaleSegment;
 import org.apache.pinot.spi.utils.JsonUtils;
+import org.apache.pinot.spi.utils.builder.UrlBuilderUtils;
 import org.glassfish.jersey.client.ClientConfig;
 import org.glassfish.jersey.client.ClientProperties;
 import org.roaringbitmap.RoaringBitmap;
@@ -430,38 +430,41 @@ public class ServerSegmentMetadataReader {
     return serverResponses;
   }
 
-  private String generateAggregateSegmentMetadataServerURL(String 
tableNameWithType, List<String> columns,
+  private String generateAggregateSegmentMetadataServerURL(String 
tableNameWithType, @Nullable List<String> columns,
       String endpoint) {
-    tableNameWithType = URLEncoder.encode(tableNameWithType, 
StandardCharsets.UTF_8);
-    String paramsStr = generateParam(COLUMNS_KEY, columns);
-    return String.format("%s/tables/%s/metadata?%s", endpoint, 
tableNameWithType, paramsStr);
+    tableNameWithType = encode(tableNameWithType);
+    String columnsParam = UrlBuilderUtils.generateColumnsParam(columns);
+    String url = String.format("%s/tables/%s/metadata", endpoint, 
tableNameWithType);
+    return columnsParam != null ? url + "?" + columnsParam : url;
   }
 
   public String generateSegmentMetadataServerURL(String tableNameWithType, 
String segmentName,
       @Nullable List<String> columns, String endpoint) {
-    tableNameWithType = URLEncoder.encode(tableNameWithType, 
StandardCharsets.UTF_8);
-    segmentName = URLEncoder.encode(segmentName, StandardCharsets.UTF_8);
-    String paramsStr = generateParam(COLUMNS_KEY, columns);
-    return String.format("%s/tables/%s/segments/%s/metadata?%s", endpoint, 
tableNameWithType, segmentName, paramsStr);
+    tableNameWithType = encode(tableNameWithType);
+    segmentName = UrlBuilderUtils.encode(segmentName);
+    String columnsParam = UrlBuilderUtils.generateColumnsParam(columns);
+    String url = String.format("%s/tables/%s/segments/%s/metadata", endpoint, 
tableNameWithType, segmentName);
+    return columnsParam != null ? url + "?" + columnsParam : url;
   }
 
   public String generateTableMetadataServerURL(String tableNameWithType, 
@Nullable List<String> columns,
-      @Nullable List<String> segmentsToInclude, String endpoint) {
-    tableNameWithType = URLEncoder.encode(tableNameWithType, 
StandardCharsets.UTF_8);
-    String paramsStr = generateParam(COLUMNS_KEY, columns) + "&" + 
generateParam(SEGMENTS_KEY, segmentsToInclude);
-    return String.format("%s/tables/%s/segments/metadata?%s", endpoint, 
tableNameWithType, paramsStr);
+      @Nullable List<String> segments, String endpoint) {
+    tableNameWithType = encode(tableNameWithType);
+    String columnsAndSegmentsParam = 
UrlBuilderUtils.generateColumnsAndSegmentsParam(columns, segments);
+    String url = String.format("%s/tables/%s/segments/metadata", endpoint, 
tableNameWithType);
+    return columnsAndSegmentsParam != null ? url + "?" + 
columnsAndSegmentsParam : url;
   }
 
   private String generateCheckReloadSegmentsServerURL(String 
tableNameWithType, String endpoint) {
-    tableNameWithType = URLEncoder.encode(tableNameWithType, 
StandardCharsets.UTF_8);
+    tableNameWithType = encode(tableNameWithType);
     return String.format("%s/tables/%s/segments/needReload", endpoint, 
tableNameWithType);
   }
 
   @Deprecated
   private String generateValidDocIdsURL(String tableNameWithType, String 
segmentName, String validDocIdsType,
       String endpoint) {
-    tableNameWithType = URLEncoder.encode(tableNameWithType, 
StandardCharsets.UTF_8);
-    segmentName = URLEncoder.encode(segmentName, StandardCharsets.UTF_8);
+    tableNameWithType = encode(tableNameWithType);
+    segmentName = encode(segmentName);
     String url = String.format("%s/segments/%s/%s/validDocIds", endpoint, 
tableNameWithType, segmentName);
     if (validDocIdsType != null) {
       url = url + "?validDocIdsType=" + validDocIdsType;
@@ -471,18 +474,15 @@ public class ServerSegmentMetadataReader {
 
   private String generateValidDocIdsBitmapURL(String tableNameWithType, String 
segmentName, String validDocIdsType,
       String endpoint) {
-    tableNameWithType = URLEncoder.encode(tableNameWithType, 
StandardCharsets.UTF_8);
-    segmentName = URLEncoder.encode(segmentName, StandardCharsets.UTF_8);
+    tableNameWithType = encode(tableNameWithType);
+    segmentName = encode(segmentName);
     String url = String.format("%s/segments/%s/%s/validDocIdsBitmap", 
endpoint, tableNameWithType, segmentName);
-    if (validDocIdsType != null) {
-      url = url + "?validDocIdsType=" + validDocIdsType;
-    }
-    return url;
+    return validDocIdsType != null ? url + "?validDocIdsType=" + 
validDocIdsType : url;
   }
 
   private Pair<String, String> generateValidDocIdsMetadataURL(String 
tableNameWithType, List<String> segmentNames,
       String validDocIdsType, String endpoint) {
-    tableNameWithType = URLEncoder.encode(tableNameWithType, 
StandardCharsets.UTF_8);
+    tableNameWithType = encode(tableNameWithType);
     TableSegments tableSegments = new TableSegments(segmentNames);
     String jsonTableSegments;
     try {
@@ -499,28 +499,19 @@ public class ServerSegmentMetadataReader {
   }
 
   private String generateStaleSegmentsServerURL(String tableNameWithType, 
String endpoint) {
-    tableNameWithType = URLEncoder.encode(tableNameWithType, 
StandardCharsets.UTF_8);
+    tableNameWithType = encode(tableNameWithType);
     return String.format("%s/tables/%s/segments/isStale", endpoint, 
tableNameWithType);
   }
 
-  private String generateParam(String key, List<String> values) {
-    String paramsStr = "";
-    if (CollectionUtils.isEmpty(values)) {
-      return paramsStr;
-    }
-    List<String> params = new ArrayList<>(values.size());
-    for (String value : values) {
-      params.add(key + "=" + value);
-    }
-    paramsStr = String.join("&", params);
-    return paramsStr;
+  private static String encode(String value) {
+    return URLEncoder.encode(value, StandardCharsets.UTF_8);
   }
 
-  public class TableReloadResponse {
-    private int _numFailedResponses;
-    private List<String> _serverReloadResponses;
+  public static class TableReloadResponse {
+    private final int _numFailedResponses;
+    private final List<String> _serverReloadResponses;
 
-    TableReloadResponse(int numFailedResponses, List<String> 
serverReloadResponses) {
+    private TableReloadResponse(int numFailedResponses, List<String> 
serverReloadResponses) {
       _numFailedResponses = numFailedResponses;
       _serverReloadResponses = serverReloadResponses;
     }
diff --git 
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/HybridClusterIntegrationTest.java
 
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/HybridClusterIntegrationTest.java
index 57a7ad236d9..b8a6f5c2f9a 100644
--- 
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/HybridClusterIntegrationTest.java
+++ 
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/HybridClusterIntegrationTest.java
@@ -19,9 +19,6 @@
 package org.apache.pinot.integration.tests;
 
 import com.fasterxml.jackson.databind.JsonNode;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -125,12 +122,7 @@ public class HybridClusterIntegrationTest extends 
BaseHybridClusterIntegrationTe
           segmentMetadataFromDirectEndpoint.get("segment.total.docs"));
     }
     // get list of segment names to pass in query params for following tests
-    List<String> segmentNames = getSegmentNames(getTableName(), 
TableType.OFFLINE.toString());
-    List<String> segments = new ArrayList<>();
-    for (String segment : segmentNames) {
-      String encodedSegmentName = URLEncoder.encode(segment, 
StandardCharsets.UTF_8.toString());
-      segments.add(encodedSegmentName);
-    }
+    List<String> segments = getSegmentNames(getTableName(), 
TableType.OFFLINE.toString());
     // with null column params
     {
       String jsonOutputStr = 
sendGetRequest(_controllerRequestURLBuilder.forSegmentsMetadataFromServer(getTableName(),
@@ -143,7 +135,7 @@ public class HybridClusterIntegrationTest extends 
BaseHybridClusterIntegrationTe
       JsonNode segmentMetadataFromDirectEndpoint = 
JsonUtils.stringToJsonNode(jsonOutputStr);
       Assert.assertEquals(segmentMetadataFromAllEndpoint.get("totalDocs"),
           segmentMetadataFromDirectEndpoint.get("segment.total.docs"));
-      
Assert.assertEquals(tableSegmentsMetadata.get(segmentNames.get(0)).get("columns").size(),
 0);
+      
Assert.assertEquals(tableSegmentsMetadata.get(segments.get(0)).get("columns").size(),
 0);
     }
     // with * column param
     {
@@ -157,7 +149,7 @@ public class HybridClusterIntegrationTest extends 
BaseHybridClusterIntegrationTe
       JsonNode segmentMetadataFromDirectEndpoint = 
JsonUtils.stringToJsonNode(jsonOutputStr);
       Assert.assertEquals(segmentMetadataFromAllEndpoint.get("totalDocs"),
           segmentMetadataFromDirectEndpoint.get("segment.total.docs"));
-      
Assert.assertEquals(tableSegmentsMetadata.get(segmentNames.get(0)).get("columns").size(),
 79);
+      
Assert.assertEquals(tableSegmentsMetadata.get(segments.get(0)).get("columns").size(),
 79);
     }
     // with specified column params
     {
@@ -172,7 +164,7 @@ public class HybridClusterIntegrationTest extends 
BaseHybridClusterIntegrationTe
       JsonNode segmentMetadataFromDirectEndpoint = 
JsonUtils.stringToJsonNode(jsonOutputStr);
       Assert.assertEquals(segmentMetadataFromAllEndpoint.get("totalDocs"),
           segmentMetadataFromDirectEndpoint.get("segment.total.docs"));
-      
Assert.assertEquals(tableSegmentsMetadata.get(segmentNames.get(0)).get("columns").size(),
 columns.size());
+      
Assert.assertEquals(tableSegmentsMetadata.get(segments.get(0)).get("columns").size(),
 columns.size());
     }
   }
 
diff --git 
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/OfflineClusterIntegrationTest.java
 
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/OfflineClusterIntegrationTest.java
index 02c16814488..25c09423795 100644
--- 
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/OfflineClusterIntegrationTest.java
+++ 
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/OfflineClusterIntegrationTest.java
@@ -3763,20 +3763,10 @@ public class OfflineClusterIntegrationTest extends 
BaseClusterIntegrationTestSet
         + 
"/tables/mytable/metadata?columns=DivActualElapsedTime&columns=CRSElapsedTime&columns=OriginStateName"));
     validateMetadataResponse(threeSVColumnsResponse, 3, 0);
 
-    JsonNode threeSVColumnsWholeEncodedResponse = 
JsonUtils.stringToJsonNode(sendGetRequest(
-        getControllerBaseApiUrl() + "/tables/mytable/metadata?columns="
-            + 
"DivActualElapsedTime%26columns%3DCRSElapsedTime%26columns%3DOriginStateName"));
-    validateMetadataResponse(threeSVColumnsWholeEncodedResponse, 3, 0);
-
     JsonNode threeMVColumnsResponse = 
JsonUtils.stringToJsonNode(sendGetRequest(getControllerBaseApiUrl()
         + 
"/tables/mytable/metadata?columns=DivLongestGTimes&columns=DivWheelsOns&columns=DivAirports"));
     validateMetadataResponse(threeMVColumnsResponse, 3, 3);
 
-    JsonNode threeMVColumnsWholeEncodedResponse = 
JsonUtils.stringToJsonNode(sendGetRequest(
-        getControllerBaseApiUrl() + "/tables/mytable/metadata?columns="
-            + 
"DivLongestGTimes%26columns%3DDivWheelsOns%26columns%3DDivAirports"));
-    validateMetadataResponse(threeMVColumnsWholeEncodedResponse, 3, 3);
-
     JsonNode zeroColumnResponse =
         JsonUtils.stringToJsonNode(sendGetRequest(getControllerBaseApiUrl() + 
"/tables/mytable/metadata"));
     validateMetadataResponse(zeroColumnResponse, 0, 0);
@@ -3798,11 +3788,6 @@ public class OfflineClusterIntegrationTest extends 
BaseClusterIntegrationTestSet
         getControllerBaseApiUrl() + "/tables/mytable/metadata?columns="
             + "CRSElapsedTime&columns=%2A&columns=OriginStateName"));
     validateMetadataResponse(starWithExtraEncodedColumnResponse, 83, 10);
-
-    JsonNode starWithExtraColumnWholeEncodedResponse = 
JsonUtils.stringToJsonNode(sendGetRequest(
-        getControllerBaseApiUrl() + "/tables/mytable/metadata?columns="
-            + "CRSElapsedTime%26columns%3D%2A%26columns%3DOriginStateName"));
-    validateMetadataResponse(starWithExtraColumnWholeEncodedResponse, 83, 10);
   }
 
   private void validateMetadataResponse(JsonNode response, int numTotalColumn, 
int numMVColumn) {
diff --git 
a/pinot-server/src/main/java/org/apache/pinot/server/api/resources/ReingestionResource.java
 
b/pinot-server/src/main/java/org/apache/pinot/server/api/resources/ReingestionResource.java
index 9b1c93e4dea..6386a85b52c 100644
--- 
a/pinot-server/src/main/java/org/apache/pinot/server/api/resources/ReingestionResource.java
+++ 
b/pinot-server/src/main/java/org/apache/pinot/server/api/resources/ReingestionResource.java
@@ -42,6 +42,7 @@ import java.util.concurrent.Semaphore;
 import java.util.concurrent.atomic.AtomicBoolean;
 import javax.annotation.Nullable;
 import javax.inject.Inject;
+import javax.ws.rs.Encoded;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
@@ -54,6 +55,7 @@ import javax.ws.rs.core.Response;
 import org.apache.pinot.common.metadata.segment.SegmentZKMetadata;
 import org.apache.pinot.common.metrics.ServerMeter;
 import org.apache.pinot.common.utils.LLCSegmentName;
+import org.apache.pinot.common.utils.URIUtils;
 import org.apache.pinot.core.data.manager.realtime.RealtimeTableDataManager;
 import 
org.apache.pinot.segment.local.realtime.writer.StatelessRealtimeSegmentWriter;
 import org.apache.pinot.segment.local.segment.index.loader.IndexLoadingConfig;
@@ -148,7 +150,8 @@ public class ReingestionResource {
       @ApiResponse(code = 200, message = "Success", response = 
ReingestionJob.class),
       @ApiResponse(code = 500, message = "Internal server error", response = 
ErrorInfo.class)
   })
-  public Response reingestSegment(@PathParam("segmentName") String 
segmentName) {
+  public Response reingestSegment(@PathParam("segmentName") @Encoded String 
encodedSegmentName) {
+    String segmentName = URIUtils.decode(encodedSegmentName);
     LOGGER.info("Re-ingesting segment: {}", segmentName);
 
     // if segment is not in LLC format, return error
diff --git 
a/pinot-server/src/main/java/org/apache/pinot/server/api/resources/SegmentMetadataFetcher.java
 
b/pinot-server/src/main/java/org/apache/pinot/server/api/resources/SegmentMetadataFetcher.java
index c557d0681b8..b30abe3db2f 100644
--- 
a/pinot-server/src/main/java/org/apache/pinot/server/api/resources/SegmentMetadataFetcher.java
+++ 
b/pinot-server/src/main/java/org/apache/pinot/server/api/resources/SegmentMetadataFetcher.java
@@ -75,13 +75,7 @@ public class SegmentMetadataFetcher {
       throws JsonProcessingException {
     IndexSegment segment = segmentDataManager.getSegment();
     SegmentMetadata segmentMetadata = segment.getSegmentMetadata();
-    Set<String> columnSet;
-    if (columns.size() == 1 && columns.get(0).equals("*")) {
-      // Making code consistent and returning metadata and indexes only for 
non-virtual columns.
-      columnSet = segment.getPhysicalColumnNames();
-    } else {
-      columnSet = new HashSet<>(columns);
-    }
+    Set<String> columnSet = columns.contains("*") ? 
segment.getPhysicalColumnNames() : new HashSet<>(columns);
     ObjectNode segmentMetadataJson = (ObjectNode) 
segmentMetadata.toJson(columnSet);
     segmentMetadataJson.set(COLUMN_INDEX_KEY,
         
JsonUtils.objectToJsonNode(getIndexesForSegmentColumns(segmentDataManager, 
columnSet)));
diff --git 
a/pinot-server/src/main/java/org/apache/pinot/server/api/resources/TablesResource.java
 
b/pinot-server/src/main/java/org/apache/pinot/server/api/resources/TablesResource.java
index 0b4e3e97793..b482d662a5e 100644
--- 
a/pinot-server/src/main/java/org/apache/pinot/server/api/resources/TablesResource.java
+++ 
b/pinot-server/src/main/java/org/apache/pinot/server/api/resources/TablesResource.java
@@ -61,7 +61,6 @@ import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.StreamingOutput;
-import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.helix.model.IdealState;
@@ -188,7 +187,6 @@ public class TablesResource {
   }
 
   @GET
-  @Encoded
   @Produces(MediaType.APPLICATION_JSON)
   @Path("/tables/{tableName}/metadata")
   @ApiOperation(value = "List metadata for all segments of a given table", 
notes = "List segments metadata of table "
@@ -200,8 +198,8 @@ public class TablesResource {
   })
   public String getSegmentMetadata(
       @ApiParam(value = "Table Name with type", required = true) 
@PathParam("tableName") String tableName,
-      @ApiParam(value = "Column name", allowMultiple = true) 
@QueryParam("columns") @DefaultValue("")
-      List<String> columns, @Context HttpHeaders headers)
+      @ApiParam(value = "Column name", allowMultiple = true) 
@QueryParam("columns") List<String> columns,
+      @Context HttpHeaders headers)
       throws WebApplicationException {
     tableName = DatabaseUtils.translateTableName(tableName, headers);
     InstanceDataManager instanceDataManager = 
_serverInstance.getInstanceDataManager();
@@ -215,21 +213,7 @@ public class TablesResource {
       throw new WebApplicationException("Table: " + tableName + " is not 
found", Response.Status.NOT_FOUND);
     }
 
-    List<String> decodedColumns = new ArrayList<>(columns.size());
-    for (String column : columns) {
-      decodedColumns.add(URIUtils.decode(column));
-    }
-
-    boolean allColumns = false;
-    // For robustness, loop over all columns, if any of the columns is "*", 
return metadata for all columns.
-    for (String column : decodedColumns) {
-      if (column.equals("*")) {
-        allColumns = true;
-        break;
-      }
-    }
-    Set<String> columnSet = allColumns ? null : new HashSet<>(decodedColumns);
-
+    Set<String> columnSet = columns.contains("*") ? null : new 
HashSet<>(columns);
     List<SegmentDataManager> segmentDataManagers = 
tableDataManager.acquireAllSegments();
     long totalSegmentSizeBytes = 0;
     long totalNumRows = 0;
@@ -327,14 +311,13 @@ public class TablesResource {
   }
 
   @GET
-  @Encoded
   @Path("/tables/{tableName}/indexes")
   @Produces(MediaType.APPLICATION_JSON)
   @ApiOperation(value = "Provide index metadata", notes = "Provide index 
details for the table")
   @ApiResponses(value = {
-      @ApiResponse(code = 200, message = "Success"), @ApiResponse(code = 500, 
message = "Internal server error",
-      response = ErrorInfo.class), @ApiResponse(code = 404, message = "Table 
or segment not found", response =
-      ErrorInfo.class)
+      @ApiResponse(code = 200, message = "Success"),
+      @ApiResponse(code = 500, message = "Internal server error", response = 
ErrorInfo.class),
+      @ApiResponse(code = 404, message = "Table or segment not found", 
response = ErrorInfo.class)
   })
   public String getTableIndexes(
       @ApiParam(value = "Table name including type", required = true, example 
= "myTable_OFFLINE")
@@ -373,7 +356,6 @@ public class TablesResource {
   }
 
   @GET
-  @Encoded
   @Path("/tables/{tableName}/segments/{segmentName}/metadata")
   @Produces(MediaType.APPLICATION_JSON)
   @ApiOperation(value = "Provide segment metadata", notes = "Provide segments 
metadata for the segment on server")
@@ -385,16 +367,13 @@ public class TablesResource {
   public String getSegmentMetadata(
       @ApiParam(value = "Table name including type", required = true, example 
= "myTable_OFFLINE")
       @PathParam("tableName") String tableName,
-      @ApiParam(value = "Segment name", required = true) 
@PathParam("segmentName") String segmentName,
-      @ApiParam(value = "Column name", allowMultiple = true) 
@QueryParam("columns") @DefaultValue("")
-      List<String> columns, @Context HttpHeaders headers) {
+      @ApiParam(value = "Segment name", required = true) 
@PathParam("segmentName") @Encoded String segmentName,
+      @ApiParam(value = "Column name", allowMultiple = true) 
@QueryParam("columns") List<String> columns,
+      @Context HttpHeaders headers) {
     tableName = DatabaseUtils.translateTableName(tableName, headers);
-    for (int i = 0; i < columns.size(); i++) {
-      columns.set(i, URIUtils.decode(columns.get(i)));
-    }
+    segmentName = URIUtils.decode(segmentName);
 
     TableDataManager tableDataManager = 
ServerResourceUtils.checkGetTableDataManager(_serverInstance, tableName);
-    segmentName = URIUtils.decode(segmentName);
     SegmentDataManager segmentDataManager = 
tableDataManager.acquireSegment(segmentName);
     if (segmentDataManager == null) {
       throw new WebApplicationException(String.format("Table %s segments %s 
does not exist", tableName, segmentName),
@@ -413,7 +392,6 @@ public class TablesResource {
   }
 
   @GET
-  @Encoded
   @Path("/tables/{tableName}/segments/metadata")
   @Produces(MediaType.APPLICATION_JSON)
   @ApiOperation(value = "Provide segments metadata", notes = "Provide segments 
metadata for the segments on server")
@@ -425,36 +403,25 @@ public class TablesResource {
   public String getSegmentsMetadata(
       @ApiParam(value = "Table name including type", required = true, example 
= "myTable_OFFLINE")
       @PathParam("tableName") String tableName,
-      @Nullable @ApiParam(value = "Segments name", allowMultiple = true) 
@QueryParam("segments") List<String> segments,
-      @Nullable @ApiParam(value = "Column name", allowMultiple = true) 
@QueryParam("columns") List<String> columns,
+      @ApiParam(value = "Segments to include (all if not specified)", 
allowMultiple = true) @QueryParam("segments")
+      List<String> segments,
+      @ApiParam(value = "Columns to include (use '*' to include all)", 
allowMultiple = true) @QueryParam("columns")
+      List<String> columns,
       @Context HttpHeaders headers) {
     tableName = DatabaseUtils.translateTableName(tableName, headers);
     TableDataManager tableDataManager = 
ServerResourceUtils.checkGetTableDataManager(_serverInstance, tableName);
-    // decode columns and segments
-    List<String> decodedSegments = new ArrayList<>();
-    if (CollectionUtils.isNotEmpty(segments)) {
-      for (String segment : segments) {
-        decodedSegments.add(URIUtils.decode(segment));
-      }
-    }
     List<SegmentDataManager> segmentDataManagers;
-    if (!decodedSegments.isEmpty()) {
-      segmentDataManagers = tableDataManager.acquireSegments(decodedSegments, 
new ArrayList<>());
+    if (!segments.isEmpty()) {
+      segmentDataManagers = tableDataManager.acquireSegments(segments, new 
ArrayList<>());
     } else {
       segmentDataManagers = tableDataManager.acquireAllSegments();
     }
-    List<String> decodedColumns = new ArrayList<>();
-    if (CollectionUtils.isNotEmpty(columns)) {
-      for (String column: columns) {
-        decodedColumns.add(URIUtils.decode(column));
-      }
-    }
     // get metadata for every segment in the list
     Map<String, JsonNode> response = new HashMap<>();
     try {
       for (SegmentDataManager segmentDataManager: segmentDataManagers) {
         String segmentName = segmentDataManager.getSegmentName();
-        String segmentMetadata = 
SegmentMetadataFetcher.getSegmentMetadata(segmentDataManager, decodedColumns);
+        String segmentMetadata = 
SegmentMetadataFetcher.getSegmentMetadata(segmentDataManager, columns);
         JsonNode segmentMetadataJson = 
JsonUtils.stringToJsonNode(segmentMetadata);
         response.put(segmentName, segmentMetadataJson);
       }
@@ -514,6 +481,7 @@ public class TablesResource {
       @Context HttpHeaders httpHeaders)
       throws Exception {
     tableNameWithType = DatabaseUtils.translateTableName(tableNameWithType, 
httpHeaders);
+    segmentName = URIUtils.decode(segmentName);
     LOGGER.info("Received a request to download segment {} for table {}", 
segmentName, tableNameWithType);
     // Validate data access
     ServerResourceUtils.validateDataAccess(_accessControlFactory, 
tableNameWithType, httpHeaders);
@@ -628,8 +596,8 @@ public class TablesResource {
       @ApiParam(value = "Name of the table with type REALTIME", required = 
true, example = "myTable_REALTIME")
       @PathParam("tableNameWithType") String tableNameWithType,
       @ApiParam(value = "Name of the segment", required = true) 
@PathParam("segmentName") @Encoded String segmentName,
-      @ApiParam(value = "Valid doc ids type")
-      @QueryParam("validDocIdsType") String validDocIdsType, @Context 
HttpHeaders httpHeaders) {
+      @ApiParam(value = "Valid doc ids type") @QueryParam("validDocIdsType") 
@Nullable String validDocIdsType,
+      @Context HttpHeaders httpHeaders) {
     tableNameWithType = DatabaseUtils.translateTableName(tableNameWithType, 
httpHeaders);
     segmentName = URIUtils.decode(segmentName);
     LOGGER.info("Received a request to download validDocIds for segment {} 
table {}", segmentName, tableNameWithType);
@@ -680,18 +648,16 @@ public class TablesResource {
   @Produces(MediaType.APPLICATION_JSON)
   @ApiOperation(value = "Provides segment validDocId metadata", notes = 
"Provides segment validDocId metadata")
   @ApiResponses(value = {
-      @ApiResponse(code = 200, message = "Success"), @ApiResponse(code = 500, 
message = "Internal server error",
-      response = ErrorInfo.class), @ApiResponse(code = 404, message = "Table 
or segment not found", response =
-      ErrorInfo.class)
+      @ApiResponse(code = 200, message = "Success"),
+      @ApiResponse(code = 500, message = "Internal server error", response = 
ErrorInfo.class),
+      @ApiResponse(code = 404, message = "Table or segment not found", 
response = ErrorInfo.class)
   })
   public String getValidDocIdsMetadata(
       @ApiParam(value = "Table name including type", required = true, example 
= "myTable_REALTIME")
       @PathParam("tableNameWithType") String tableNameWithType,
-      @ApiParam(value = "Valid doc ids type")
-      @QueryParam("validDocIdsType") String validDocIdsType,
+      @ApiParam(value = "Valid doc ids type") @QueryParam("validDocIdsType") 
String validDocIdsType,
       @ApiParam(value = "Segment name", allowMultiple = true) 
@QueryParam("segmentNames") List<String> segmentNames,
-      @Context HttpHeaders headers)
-      throws Exception {
+      @Context HttpHeaders headers) {
     tableNameWithType = DatabaseUtils.translateTableName(tableNameWithType, 
headers);
     return ResourceUtils.convertToJsonString(
         processValidDocIdsMetadata(tableNameWithType, segmentNames, 
validDocIdsType));
@@ -702,9 +668,9 @@ public class TablesResource {
   @Produces(MediaType.APPLICATION_JSON)
   @ApiOperation(value = "Provides segment validDocIds metadata", notes = 
"Provides segment validDocIds metadata")
   @ApiResponses(value = {
-      @ApiResponse(code = 200, message = "Success"), @ApiResponse(code = 500, 
message = "Internal server error",
-      response = ErrorInfo.class), @ApiResponse(code = 404, message = "Table 
or segment not found", response =
-      ErrorInfo.class)
+      @ApiResponse(code = 200, message = "Success"),
+      @ApiResponse(code = 500, message = "Internal server error", response = 
ErrorInfo.class),
+      @ApiResponse(code = 404, message = "Table or segment not found", 
response = ErrorInfo.class)
   })
   public String getValidDocIdsMetadata(
       @ApiParam(value = "Table name including type", required = true, example 
= "myTable_REALTIME")
@@ -869,11 +835,12 @@ public class TablesResource {
   public String uploadLLCSegment(
       @ApiParam(value = "Name of the REALTIME table", required = true) 
@PathParam("realtimeTableName")
       String realtimeTableName,
-      @ApiParam(value = "Name of the segment", required = true) 
@PathParam("segmentName") String segmentName,
+      @ApiParam(value = "Name of the segment", required = true) 
@PathParam("segmentName") @Encoded String segmentName,
       @QueryParam("uploadTimeoutMs") @DefaultValue("-1") int timeoutMs,
       @Context HttpHeaders headers)
       throws Exception {
     realtimeTableName = DatabaseUtils.translateTableName(realtimeTableName, 
headers);
+    segmentName = URIUtils.decode(segmentName);
     LOGGER.info("Received a request to upload low level consumer segment {} 
for table {}", segmentName,
         realtimeTableName);
 
@@ -943,11 +910,12 @@ public class TablesResource {
   public TableLLCSegmentUploadResponse uploadLLCSegmentV2(
       @ApiParam(value = "Name of the REALTIME table", required = true) 
@PathParam("realtimeTableNameWithType")
       String realtimeTableNameWithType,
-      @ApiParam(value = "Name of the segment", required = true) 
@PathParam("segmentName") String segmentName,
+      @ApiParam(value = "Name of the segment", required = true) 
@PathParam("segmentName") @Encoded String segmentName,
       @QueryParam("uploadTimeoutMs") @DefaultValue("-1") int timeoutMs,
       @Context HttpHeaders headers)
       throws Exception {
     realtimeTableNameWithType = 
DatabaseUtils.translateTableName(realtimeTableNameWithType, headers);
+    segmentName = URIUtils.decode(segmentName);
     LOGGER.info("Received a request to upload low level consumer segment {} 
for table {}", segmentName,
         realtimeTableNameWithType);
 
@@ -1014,10 +982,11 @@ public class TablesResource {
   public String uploadCommittedSegment(
       @ApiParam(value = "Name of the real-time table", required = true) 
@PathParam("realtimeTableName")
       String realtimeTableName,
-      @ApiParam(value = "Name of the segment", required = true) 
@PathParam("segmentName") String segmentName,
+      @ApiParam(value = "Name of the segment", required = true) 
@PathParam("segmentName") @Encoded String segmentName,
       @QueryParam("uploadTimeoutMs") @DefaultValue("-1") int timeoutMs, 
@Context HttpHeaders headers)
       throws Exception {
     realtimeTableName = DatabaseUtils.translateTableName(realtimeTableName, 
headers);
+    segmentName = URIUtils.decode(segmentName);
     LOGGER.info("Received a request to upload committed segment: {} for table: 
{}", segmentName, realtimeTableName);
 
     // Check it's real-time table
diff --git 
a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/builder/ControllerRequestURLBuilder.java
 
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/builder/ControllerRequestURLBuilder.java
index c17bc7c0b61..2da3d879d0a 100644
--- 
a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/builder/ControllerRequestURLBuilder.java
+++ 
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/builder/ControllerRequestURLBuilder.java
@@ -20,12 +20,10 @@ package org.apache.pinot.spi.utils.builder;
 
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
-import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.pinot.spi.config.table.TableType;
 import org.apache.pinot.spi.config.table.assignment.InstancePartitionsType;
@@ -342,46 +340,11 @@ public class ControllerRequestURLBuilder {
   }
 
   public String forTableAggregateMetadata(String tableName, @Nullable 
List<String> columns) {
-    return StringUtil.join("/", _baseUrl, "tables", tableName, "metadata") + 
constructColumnsParameter(columns);
-  }
-
-  private String constructColumnsParameter(@Nullable List<String> columns) {
-    if (!CollectionUtils.isEmpty(columns)) {
-      StringBuilder parameter = new StringBuilder();
-      parameter.append("?columns=");
-      parameter.append(columns.get(0));
-      int numColumns = columns.size();
-      if (numColumns > 1) {
-        for (int i = 1; i < numColumns; i++) {
-          parameter.append("&columns=").append(columns.get(i));
-        }
-      }
-      return parameter.toString();
-    } else {
-      return "";
-    }
-  }
-
-  private String constructQueryParametersString(Map<String, List<String>> 
queryParams) {
-    if (queryParams.isEmpty()) {
-      return "";
-    }
-    StringBuilder query = new StringBuilder("?");
-    boolean firstParam = true;
-    for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) {
-      String key = entry.getKey();
-      for (String value : entry.getValue()) {
-        if (!firstParam) {
-          query.append("&");
-        }
-        query.append(key).append("=").append(value);
-        firstParam = false;
-      }
-    }
-    return query.toString();
+    String columnsParam = UrlBuilderUtils.generateColumnsParam(columns);
+    String url = StringUtil.join("/", _baseUrl, "tables", tableName, 
"metadata");
+    return columnsParam != null ? url + "?" + columnsParam : url;
   }
 
-
   public String forSchemaValidate() {
     return StringUtil.join("/", _baseUrl, "schemas", "validate");
   }
@@ -471,16 +434,10 @@ public class ControllerRequestURLBuilder {
 
   public String forSegmentsMetadataFromServer(String tableName, @Nullable 
List<String> columns,
       @Nullable List<String> segments) {
-    String basePath = StringUtil.join("/", _baseUrl, "segments", tableName, 
"metadata");
-    Map<String, List<String>> queryParams = new LinkedHashMap<>();
-    if (!CollectionUtils.isEmpty(columns)) {
-      queryParams.put("columns", columns);
-    }
-    if (!CollectionUtils.isEmpty(segments)) {
-      queryParams.put("segments", segments);
-    }
-    String queryString = constructQueryParametersString(queryParams);
-    return basePath + queryString;
+    tableName = encode(tableName);
+    String columnsAndSegmentsParam = 
UrlBuilderUtils.generateColumnsAndSegmentsParam(columns, segments);
+    String url = StringUtil.join("/", _baseUrl, "segments", tableName, 
"metadata");
+    return columnsAndSegmentsParam != null ? url + "?" + 
columnsAndSegmentsParam : url;
   }
 
   public String forSegmentMetadata(String tableName, String segmentName) {
diff --git 
a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/builder/UrlBuilderUtils.java
 
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/builder/UrlBuilderUtils.java
new file mode 100644
index 00000000000..d67e7887082
--- /dev/null
+++ 
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/builder/UrlBuilderUtils.java
@@ -0,0 +1,75 @@
+/**
+ * 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.pinot.spi.utils.builder;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.apache.commons.collections4.CollectionUtils;
+
+
+public class UrlBuilderUtils {
+  private UrlBuilderUtils() {
+  }
+
+  @Nullable
+  public static String generateColumnsParam(@Nullable List<String> columns) {
+    if (CollectionUtils.isEmpty(columns)) {
+      return null;
+    }
+    StringBuilder builder = new 
StringBuilder("columns=").append(encode(columns.get(0)));
+    int numColumns = columns.size();
+    for (int i = 1; i < numColumns; i++) {
+      builder.append("&columns=").append(encode(columns.get(i)));
+    }
+    return builder.toString();
+  }
+
+  @Nullable
+  public static String generateSegmentsParam(@Nullable List<String> segments) {
+    if (CollectionUtils.isEmpty(segments)) {
+      return null;
+    }
+    StringBuilder builder = new 
StringBuilder("segments=").append(encode(segments.get(0)));
+    int numSegments = segments.size();
+    for (int i = 1; i < numSegments; i++) {
+      builder.append("&segments=").append(encode(segments.get(i)));
+    }
+    return builder.toString();
+  }
+
+  public static String encode(String value) {
+    return URLEncoder.encode(value, StandardCharsets.UTF_8);
+  }
+
+  @Nullable
+  public static String generateColumnsAndSegmentsParam(@Nullable List<String> 
columns,
+      @Nullable List<String> segments) {
+    String columnsParam = generateColumnsParam(columns);
+    String segmentsParam = generateSegmentsParam(segments);
+    if (columnsParam == null) {
+      return segmentsParam;
+    }
+    if (segmentsParam == null) {
+      return columnsParam;
+    }
+    return columnsParam + "&" + segmentsParam;
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@pinot.apache.org
For additional commands, e-mail: commits-h...@pinot.apache.org

Reply via email to