/* * Copyright 2015-2016 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed 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.hawkular.inventory.rest; import static javax.ws.rs.core.Response.Status.CREATED; import java.io.IOException; import java.io.InputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Spliterator; import java.util.stream.StreamSupport; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import org.hawkular.inventory.api.model.AbstractElement; import org.hawkular.inventory.api.paging.Page; import org.hawkular.inventory.api.paging.PageContext; import org.hawkular.inventory.bus.Log; import org.hawkular.inventory.rest.json.Link; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SequenceWriter; /** * @author Lukas Krejci * @author Heiko W. Rupp * @since 0.0.1 */ public final class ResponseUtil { /** * This method exists solely to concentrate usage of {@link javax.ws.rs.core.Response#created(java.net.URI)} into * one place until <a href="https://issues.jboss.org/browse/RESTEASY-1162">this JIRA</a> is resolved somehow. * * @param element the newly created element. This will be returned in the response body as a JSON payload * @param info the UriInfo instance of the current request * @param id the ID of a newly created entity under the base * @return the response builder with status 201 and location set to the entity with the provided id. * @deprecated This is only used by the deprecated REST API */ @Deprecated public static Response.ResponseBuilder created(AbstractElement element, UriInfo info, String id) { return Response.status(CREATED) .location(info.getRequestUriBuilder().segment(id).build()) .entity(element); } public static Response.ResponseBuilder created(AbstractElement<?, ?> element, UriInfo info) throws URISyntaxException { URI baseURI = info.getBaseUri(); String newPath = baseURI.getPath() + "entity/" + Utils.getCanonicalLinkPath(element).toString(); URI uri = info.getRequestUriBuilder().replacePath(newPath).build(); return Response.status(CREATED).location(uri).entity(element); } /** * Similar to {@link #created(AbstractElement, UriInfo, String)} but used when more than 1 entity is created * during the request. * <p> * The provided list of ids is converted to URIs (by merely appending the ids using the * {@link UriBuilder#segment(String...)} method) and put in the response as its entity. * * @param info uri info to help with converting ids to URIs * @param ids the list of ids of the entities * @return the response builder with status 201 and entity set * @deprecated This returns just ids of the newly created entities in the response. That is not satisfactory. */ @Deprecated public static Response.ResponseBuilder created(UriInfo info, Spliterator<String> ids) { return Response.status(CREATED) .entity(StreamSupport.stream(ids, false).map( (id) -> info.getRequestUriBuilder().segment(id).build())); } public static Response.ResponseBuilder created(Collection<? extends AbstractElement<?, ?>> data, UriInfo info) { Response.ResponseBuilder ret = Response.status(CREATED); URI baseURI = info.getBaseUri(); data.forEach(e -> { String newPath = baseURI.getPath() + "entity/" + Utils.getCanonicalLinkPath(e).toString(); URI uri = info.getRequestUriBuilder().replacePath(newPath).build(); ret.location(uri); }); return ret.entity(data); } public static <T> Response.ResponseBuilder pagedResponse(Response.ResponseBuilder response, UriInfo uriInfo, ObjectMapper mapper, Page<T> page) { InputStream data = null; try { //extract the data out of the page data = pageToStream(page, mapper, () -> { // the page iterator should be depleted by this time so the total size should be correctly set response.type(MediaType.APPLICATION_OCTET_STREAM_TYPE); createPagingHeader(response, uriInfo, page); }); } catch (IOException e) { Log.LOG.error(e); } response.entity(data); return response; } public static <T> Response.ResponseBuilder pagedResponse(Response.ResponseBuilder response, UriInfo uriInfo, Page<T> page, Object data) { response.entity(data); createPagingHeader(response, uriInfo, page); return response; } private static <T> InputStream pageToStream(Page<T> page, ObjectMapper mapper, Runnable callback) throws IOException { final PipedOutputStream outs = new PipedOutputStream(); final PipedInputStream ins = new PipedInputStream() { @Override public void close() throws IOException { outs.close(); super.close(); } }; outs.connect(ins); mapper.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); PageToStreamThreadPool.getInstance().submit(() -> { try (Page<T> closeablePage = page; PipedOutputStream out = outs; SequenceWriter sequenceWriter = mapper.writer().writeValuesAsArray(out)) { for (T element : closeablePage) { sequenceWriter.write(element); sequenceWriter.flush(); } } catch (IOException e) { throw new IllegalStateException("Unable to convert page to input stream.", e); } finally { callback.run(); } }); return ins; } /** * Create the paging headers for collections and attach them to the passed builder. Those are represented as * <i>Link:</i> http headers that carry the URL for the pages and the respective relation. * <br/>In addition a <i>X-Total-Count</i> header is created that contains the whole collection size. * * @param builder The ResponseBuilder that receives the headers * @param uriInfo The uriInfo of the incoming request to build the urls * @param resultList The collection with its paging information */ public static void createPagingHeader(final Response.ResponseBuilder builder, final UriInfo uriInfo, final Page<?> resultList) { UriBuilder uriBuilder; PageContext pc = resultList.getPageContext(); int page = pc.getPageNumber(); List<Link> links = new ArrayList<>(); if (pc.isLimited() && resultList.getTotalSize() > (pc.getPageNumber() + 1) * pc.getPageSize()) { int nextPage = page + 1; uriBuilder = uriInfo.getRequestUriBuilder(); // adds ?q, ?per_page, ?page, etc. if needed uriBuilder.replaceQueryParam("page", nextPage); links.add(new Link("next", uriBuilder.build().toString())); } if (page > 0) { int prevPage = page - 1; uriBuilder = uriInfo.getRequestUriBuilder(); // adds ?q, ?per_page, ?page, etc. if needed uriBuilder.replaceQueryParam("page", prevPage); links.add(new Link("prev", uriBuilder.build().toString())); } // A link to the last page if (pc.isLimited()) { long lastPage = resultList.getTotalSize() / pc.getPageSize(); if (resultList.getTotalSize() % pc.getPageSize() == 0) { lastPage -= 1; } uriBuilder = uriInfo.getRequestUriBuilder(); // adds ?q, ?per_page, ?page, etc. if needed uriBuilder.replaceQueryParam("page", lastPage); links.add(new Link("last", uriBuilder.build().toString())); } // A link to the current page uriBuilder = uriInfo.getRequestUriBuilder(); // adds ?q, ?per_page, ?page, etc. if needed StringBuilder linkHeader = new StringBuilder(new Link("current", uriBuilder.build().toString()) .rfc5988String()); //followed by the rest of the link defined above links.forEach((l) -> linkHeader.append(", ").append(l.rfc5988String())); //add that all as a single Link header to the response builder.header("Link", linkHeader.toString()); // Create a total size header builder.header("X-Total-Count", resultList.getTotalSize()); } }