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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,25 @@ public class GlobalExceptionMapper implements jakarta.ws.rs.ext.ExceptionMapper<
* {@inheritDoc}
*
* <p>Maps the throwable to an HTTP error response. If the throwable is a
* {@link WebApplicationException}, its own HTTP status is used; all other
* throwables result in HTTP 500 Internal Server Error.</p>
* {@link WebApplicationException}, its own HTTP status is used. Query-contract
* validation failures reported as {@link IllegalArgumentException} are mapped
* to HTTP 400 Bad Request. All other throwables result in HTTP 500 Internal
* Server Error.</p>
*/
@Override
public Response toResponse(Throwable throwable) {
Debug.logError(throwable.getMessage(), MODULE);
if (Debug.verboseOn()) {
throwable.printStackTrace();
}
Response.StatusType type = (throwable instanceof WebApplicationException
? ((WebApplicationException) throwable).getResponse().getStatusInfo()
: Response.Status.INTERNAL_SERVER_ERROR);
Response.StatusType type;
if (throwable instanceof WebApplicationException) {
type = ((WebApplicationException) throwable).getResponse().getStatusInfo();
} else if (throwable instanceof IllegalArgumentException) {
type = Response.Status.BAD_REQUEST;
} else {
type = Response.Status.INTERNAL_SERVER_ERROR;
}

Error error = new Error(type.getStatusCode(), type.getReasonPhrase(), throwable.getMessage());
return Response.status(type.getStatusCode()).entity(error).type(MediaType.APPLICATION_JSON).build();
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*******************************************************************************
* 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.ofbiz.ws.rs.util;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.ofbiz.base.util.UtilMisc;
import org.apache.ofbiz.base.util.UtilValidate;

/**
* Builds a generic list response payload containing pagination metadata, list
* data, and REST-style navigation links.
*/
public final class RestListResponseBuilder {

private final String collectionName;
private final Object collectionData;
private int pageIndex = RestQueryOptions.DEFAULT_PAGE_INDEX;
private int pageSize = RestQueryOptions.DEFAULT_PAGE_SIZE;
private long totalCount;
private String requestPath;

private RestListResponseBuilder(String collectionName, Object collectionData) {
if (UtilValidate.isEmpty(collectionName)) {
throw new IllegalArgumentException("collectionName must not be empty");
}
this.collectionName = collectionName;
this.collectionData = collectionData;
}

/**
* Creates a builder for a named collection payload.
*
* @param collectionName the JSON property name for the collection payload
* @param collectionData the collection payload value
* @return a new response builder
*/
public static RestListResponseBuilder forList(String collectionName, Object collectionData) {
return new RestListResponseBuilder(collectionName, collectionData);
}

/**
* Sets the zero-based page index.
*
* @param pageIndex the current page index
* @return this builder
*/
public RestListResponseBuilder pageIndex(int pageIndex) {
this.pageIndex = validatePageIndex(pageIndex);
return this;
}

/**
* Sets the page size.
*
* @param pageSize the current page size
* @return this builder
*/
public RestListResponseBuilder pageSize(int pageSize) {
this.pageSize = validatePageSize(pageSize);
return this;
}

/**
* Sets the total number of matching records.
*
* @param totalCount the total matching record count
* @return this builder
*/
public RestListResponseBuilder totalCount(long totalCount) {
this.totalCount = Math.max(0, totalCount);
return this;
}

/**
* Sets the originating request path used to construct pagination links.
*
* @param requestPath the request path, optionally including a query string
* @return this builder
*/
public RestListResponseBuilder requestPath(String requestPath) {
this.requestPath = requestPath;
return this;
}

/**
* Builds the collection response payload.
*
* @return a map containing pagination metadata, collection data, and links
*/
public Map<String, Object> build() {
Map<String, Object> response = new LinkedHashMap<>();
response.put("pageIndex", pageIndex);
response.put("pageSize", pageSize);
response.put("totalCount", totalCount);
response.put("totalPages", getTotalPages());
response.put("hasNext", hasNext());
response.put(collectionName, collectionData);

Map<String, Object> links = buildLinks();
if (UtilValidate.isNotEmpty(links)) {
response.put("links", links);
}
return response;
}

private boolean hasNext() {
return totalCount > ((long) pageIndex + 1L) * pageSize;
}

private long getTotalPages() {
if (totalCount <= 0) {
return 0L;
}
return (totalCount / pageSize) + ((totalCount % pageSize == 0) ? 0L : 1L);
}

private Map<String, Object> buildLinks() {
Map<String, Object> links = new LinkedHashMap<>();
if (UtilValidate.isEmpty(requestPath)) {
return links;
}

String basePath = getBasePath(requestPath);
if (UtilValidate.isEmpty(basePath)) {
return links;
}

long totalPages = getTotalPages();
links.put("self", RestApiUtil.makeLinkMap(buildPageHref(basePath, pageIndex), UtilMisc.toMap("rel", "self")));
if (totalPages > 0) {
links.put("first", RestApiUtil.makeLinkMap(buildPageHref(basePath, 0), UtilMisc.toMap("rel", "first")));
if (pageIndex > 0) {
links.put("prev", RestApiUtil.makeLinkMap(buildPageHref(basePath, pageIndex - 1), UtilMisc.toMap("rel", "prev")));
}
Integer nextPageIndex = getNextPageIndex();
if (hasNext() && nextPageIndex != null) {
links.put("next", RestApiUtil.makeLinkMap(buildPageHref(basePath, nextPageIndex), UtilMisc.toMap("rel", "next")));
}
Integer lastPageIndex = getLastPageIndex();
if (lastPageIndex != null) {
links.put("last", RestApiUtil.makeLinkMap(buildPageHref(basePath, lastPageIndex), UtilMisc.toMap("rel", "last")));
}
}
return links;
}

private Integer getLastPageIndex() {
long lastPageIndex = getTotalPages() - 1L;
if (lastPageIndex > Integer.MAX_VALUE) {
return null;
}
return (int) lastPageIndex;
}

private Integer getNextPageIndex() {
if (pageIndex == Integer.MAX_VALUE) {
return null;
}
return pageIndex + 1;
}

private String buildPageHref(String basePath, int targetPageIndex) {
List<RestApiUtil.QueryParameter> queryParameters = new ArrayList<>(RestApiUtil.extractQueryParameters(requestPath));
queryParameters.removeIf(parameter ->
RestApiUtil.isReservedParameter(parameter.getName(), Set.of("pageIndex", "VIEW_INDEX", "pageSize", "VIEW_SIZE")));
queryParameters.add(new RestApiUtil.QueryParameter("pageIndex", Integer.toString(targetPageIndex)));
queryParameters.add(new RestApiUtil.QueryParameter("pageSize", Integer.toString(pageSize)));

return new StringBuilder(basePath).append('?').append(RestApiUtil.encodeQueryParameters(queryParameters)).toString();
}

private static String getBasePath(String path) {
int querySeparatorIndex = path.indexOf('?');
String basePath = querySeparatorIndex >= 0 ? path.substring(0, querySeparatorIndex) : path;
return UtilValidate.isEmpty(basePath) ? null : basePath;
}

private static int validatePageIndex(int pageIndex) {
if (pageIndex < 0) {
throw new IllegalArgumentException("pageIndex must be greater than or equal to 0");
}
return pageIndex;
}

private static int validatePageSize(int pageSize) {
if (pageSize < 1 || pageSize > RestQueryOptions.MAX_PAGE_SIZE) {
throw new IllegalArgumentException("pageSize must be between 1 and 100");
}
return pageSize;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*******************************************************************************
* 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.ofbiz.ws.rs.util;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import org.apache.ofbiz.base.util.UtilMisc;
import org.apache.ofbiz.base.util.UtilValidate;

/**
* Normalizes REST list query parameters into reusable paging, sorting, and
* filter options for framework-level handlers.
*/
public final class RestQueryOptions {

public static final int DEFAULT_PAGE_INDEX = 0;
public static final int DEFAULT_PAGE_SIZE = 20;
public static final int MAX_PAGE_SIZE = 100;

private static final Set<String> RESERVED_PARAMETERS = Set.of(
"pageIndex", "VIEW_INDEX", "pageSize", "VIEW_SIZE", "sort", "orderBy");

private final int pageIndex;
private final int pageSize;
private final String sort;
private final Map<String, Object> filters;

private RestQueryOptions(int pageIndex, int pageSize, String sort, Map<String, Object> filters) {
this.pageIndex = pageIndex;
this.pageSize = pageSize;
this.sort = sort;
this.filters = Collections.unmodifiableMap(new LinkedHashMap<>(filters));
}

/**
* Creates a normalized collection query from request parameters.
*
* @param parameters raw request parameters
* @return a normalized query object with paging, sorting, and filters
* @throws IllegalArgumentException when paging values are invalid
*/
public static RestQueryOptions fromParameters(Map<String, ?> parameters) {
Map<String, Object> filters = new LinkedHashMap<>();
if (UtilValidate.isNotEmpty(parameters)) {
for (Map.Entry<String, ?> entry : parameters.entrySet()) {
if (RestApiUtil.isReservedParameter(entry.getKey(), RESERVED_PARAMETERS)
|| UtilValidate.isEmpty(entry.getValue())) {
continue;
}
filters.put(entry.getKey(), entry.getValue());
}
}

Object pageIndexParam = RestApiUtil.getParameterValueIgnoreCase(parameters, "pageIndex", "VIEW_INDEX");
Object pageSizeParam = RestApiUtil.getParameterValueIgnoreCase(parameters, "pageSize", "VIEW_SIZE");
Integer pageIndexValue = UtilMisc.toIntegerObject(pageIndexParam);
Integer pageSizeValue = UtilMisc.toIntegerObject(pageSizeParam);
if (UtilValidate.isNotEmpty(pageIndexParam) && pageIndexValue == null) {
throw new IllegalArgumentException("pageIndex must be a valid integer");
}
if (UtilValidate.isNotEmpty(pageSizeParam) && pageSizeValue == null) {
throw new IllegalArgumentException("pageSize must be a valid integer");
}
int pageIndex = pageIndexValue != null ? pageIndexValue : DEFAULT_PAGE_INDEX;
int pageSize = pageSizeValue != null ? pageSizeValue : DEFAULT_PAGE_SIZE;
if (pageIndex < 0) {
throw new IllegalArgumentException("pageIndex must be greater than or equal to 0");
}
if (pageSize < 1 || pageSize > MAX_PAGE_SIZE) {
throw new IllegalArgumentException("pageSize must be between 1 and 100");
}

String sort = null;
Object sortValue = RestApiUtil.getParameterValueIgnoreCase(parameters, "sort", "orderBy");
if (UtilValidate.isNotEmpty(sortValue)) {
sort = sortValue.toString().trim();
if (UtilValidate.isEmpty(sort)) {
sort = null;
}
}

return new RestQueryOptions(pageIndex, pageSize, sort, filters);
}

/**
* Returns the zero-based page index.
*
* @return the current page index
*/
public int getPageIndex() {
return pageIndex;
}

/**
* Returns the requested page size.
*
* @return the page size
*/
public int getPageSize() {
return pageSize;
}

/**
* Returns the normalized sort expression, when present.
*
* @return the sort expression, or {@code null} when not requested
*/
public String getSort() {
return sort;
}

/**
* Returns the remaining non-reserved request parameters as filters in
* insertion order.
*
* @return immutable filter parameters
*/
public Map<String, Object> getFilters() {
return filters;
}
}
Loading
Loading