/*
* 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());
}
}