package org.karmaexchange.resources.msg; import static java.util.Arrays.asList; import java.util.List; import java.util.Map; import javax.annotation.Nullable; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.UriInfo; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlSeeAlso; import org.karmaexchange.dao.BaseDao; import org.karmaexchange.dao.Organization; import org.karmaexchange.dao.User; import org.karmaexchange.dao.UserManagedEvent; import org.karmaexchange.resources.msg.ErrorResponseMsg.ErrorInfo; import org.karmaexchange.util.PaginatedQuery; import org.karmaexchange.util.URLUtil; import com.google.appengine.api.datastore.Cursor; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import lombok.Data; import lombok.NoArgsConstructor; @XmlRootElement @Data @NoArgsConstructor @XmlSeeAlso({EventParticipantView.class, EventSearchView.class, ReviewCommentView.class, Organization.class, OrganizationMemberView.class, OrganizationMembershipView.class, WaiverSummaryView.class, UserManagedEvent.class, User.class}) public class ListResponseMsg<T> { private List<T> data; @Nullable private PagingInfo paging; public static <T> ListResponseMsg<T> create(List<T> data) { return create(data, null); } public static <T extends BaseDao<T>> ListResponseMsg<T> create( PaginatedQuery.Result<T> queryResult) { return create(queryResult.getSearchResults(), queryResult.getPagingInfo()); } public static <T> ListResponseMsg<T> create(List<T> data, @Nullable PagingInfo paging) { return new ListResponseMsg<T>(data, paging); } private ListResponseMsg(List<T> data, @Nullable PagingInfo paging) { this.data = data; this.paging = paging; } @Data @NoArgsConstructor public static class PagingInfo { public static final String AFTER_CURSOR_PARAM = "after"; public static final String LIMIT_PARAM = "limit"; public static final String OFFSET_PARAM = "offset"; /** * The URL for the next set of results. If null, there are no more results. */ @Nullable private String next; private String afterCursor; @Nullable public static PagingInfo create(@Nullable Cursor afterCursor, int limit, boolean moreResults, UriInfo uriInfo) { String afterCursorStr = (afterCursor == null) ? "" : afterCursor.toWebSafeString(); if (afterCursorStr.isEmpty()) { return null; } else { String nextUrl; if (moreResults) { Multimap<String, String> queryParams = toMultimap(uriInfo.getQueryParameters()); queryParams.replaceValues(AFTER_CURSOR_PARAM, asList(afterCursorStr)); queryParams.replaceValues(LIMIT_PARAM, asList(String.valueOf(limit))); nextUrl = URLUtil.buildURL(uriInfo.getAbsolutePath(), queryParams); } else { nextUrl = null; } return new PagingInfo(nextUrl, afterCursorStr); } } @Nullable public static PagingInfo create(int listSize, UriInfo uriInfo, int defaultLimit) { MultivaluedMap<String, String> inputQueryParams = uriInfo.getQueryParameters(); int currentOffset = inputQueryParams.containsKey(OFFSET_PARAM) ? Integer.valueOf(inputQueryParams.getFirst(OFFSET_PARAM)) : 0; int limit = inputQueryParams.containsKey(LIMIT_PARAM) ? Integer.valueOf(inputQueryParams.getFirst(LIMIT_PARAM)) : defaultLimit; int nextOffset = currentOffset + limit; if (nextOffset >= listSize) { // No more results. return null; } Multimap<String, String> outputQueryParams = toMultimap(uriInfo.getQueryParameters()); outputQueryParams.replaceValues(OFFSET_PARAM, asList(String.valueOf(nextOffset))); outputQueryParams.replaceValues(LIMIT_PARAM, asList(String.valueOf(limit))); String nextUrl = URLUtil.buildURL(uriInfo.getAbsolutePath(), outputQueryParams); return new PagingInfo(nextUrl, null); } public static <T> List<T> offsetResult(List<T> result, UriInfo uriInfo, int defaultLimit) { MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters(); int offset = queryParams.containsKey(OFFSET_PARAM) ? Integer.valueOf(queryParams.getFirst(OFFSET_PARAM)) : 0; int limit = queryParams.containsKey(LIMIT_PARAM) ? Integer.valueOf(queryParams.getFirst(LIMIT_PARAM)) : defaultLimit; validateOffsettedResultParams(offset, limit); if (offset >= result.size()) { return result.subList(0, 0); } else { limit = Math.min(limit, result.size() - offset); return result.subList(offset, offset + limit); } } private static void validateOffsettedResultParams(int offset, int limit) { if (limit <= 0) { throw ErrorResponseMsg.createException("limit must be greater than zero", ErrorInfo.Type.BAD_REQUEST); } if (offset < 0) { throw ErrorResponseMsg.createException("offset must be greater than or equal to zero", ErrorInfo.Type.BAD_REQUEST); } } private static Multimap<String, String> toMultimap( MultivaluedMap<String, String> multivaluedMap) { Multimap<String, String> multimap = ArrayListMultimap.create(); for (Map.Entry<String, List<String>> entry : multivaluedMap.entrySet()) { multimap.putAll(entry.getKey(), entry.getValue()); } return multimap; } private PagingInfo(@Nullable String nextUrl, @Nullable String afterCursor) { this.next = nextUrl; this.afterCursor = afterCursor; } } }