/*
* Copyright (c) 2014 Red Hat, Inc.
*
* 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.ovirt.engine.api.common.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.UriInfo;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HeaderElement;
import org.apache.http.NameValuePair;
import org.apache.http.message.BasicHeaderValueParser;
/**
* This class is responsible for determining what details should be included in a response. The details can be specified
* using the {@code detail} parameter of the HTTP {@code Accept} header, or with the {@code detail} matrix or query
* parameter. The value of this parameter should a list of names preceded by the plus or minus signs. If the name is
* preceded by the an plus sign (or not preceded by any sign, only for the first name)then it will be included,
* otherwise it will be included. For example, to request the information of NICs and disks using the header:
*
* <pre>
* GET /vms/{vm:id} HTTP/1.1
* Accept: application/xml; detail=nics+disks
* </pre>
*
* Same using a matrix parameter (this is the preferred way, as proxy servers may then cache the modified content):
*
* <pre>
* GET /vms/{vm:id};detail=nics+disks HTTP/1.1
* </pre>
*
* Same using a query parameter:
*
* <pre>
* GET /vms/{vm:id}?detail=nics+disks HTTP/1.1
* </pre>
*
* The minus sing is used to exclude some detail that is included by default. For example, it could be used to
* specify that only the size of a collection is requested, but not its actual data:
*
* <pre>
* GET /vms;detail=-main+size HTTP/1.1
* </pre>
*
* When not specified otherwise the main data will by default be included.
*/
public class DetailHelper {
/**
* The name of the HTTP {@code Accept} header.
*/
private static final String ACCEPT = "Accept";
/**
* The name of the header, matrix, or query parameter that contains the list of details to include or exclude.
*/
private static final String DETAIL = "detail";
/**
* The name of the detail name that indicates that the main data should be included.
*/
public static final String MAIN = "main";
/**
* Determines what details to include or exclude from the {@code detail} parameter of the {@code Accept} header and
* from the {@code detail} matrix or query parameters.
*
* @param headers the object that gives access to the HTTP headers of the request, may be {@code null} in which case
* it will be completely ignored
* @param uri the object that gives access to the URI information, may be {@code null} in which case it will be
* completely ignored
* @return the set containing the extracted information, may be empty, but never {@code null}
*/
public static Set<String> getDetails(HttpHeaders headers, UriInfo uri) {
// We will collect the detail specifications obtained from different places into this list, for later
// processing:
List<String> allSpecs = new ArrayList<>(0);
// Try to extract the specification of what to include/exclude from the accept header:
if (headers != null) {
List<String> headerValues = headers.getRequestHeader(ACCEPT);
if (CollectionUtils.isNotEmpty(headerValues)) {
for (String headerValue : headerValues) {
HeaderElement[] headerElements = BasicHeaderValueParser.parseElements(headerValue, null);
if (ArrayUtils.isNotEmpty(headerElements)) {
for (HeaderElement headerElement : headerElements) {
for (NameValuePair parameter : headerElement.getParameters()) {
if (StringUtils.equalsIgnoreCase(parameter.getName(), DETAIL)) {
String spec = parameter.getValue();
if (StringUtils.isNotEmpty(spec)) {
allSpecs.add(parameter.getValue());
}
}
}
}
}
}
}
}
// Try also from the matrix parameters:
if (uri != null) {
List<PathSegment> segments = uri.getPathSegments();
if (CollectionUtils.isNotEmpty(segments)) {
PathSegment last = segments.get(segments.size() - 1);
if (last != null) {
MultivaluedMap<String, String> parameters = last.getMatrixParameters();
if (MapUtils.isNotEmpty(parameters)) {
List<String> specs = parameters.get(DETAIL);
if (CollectionUtils.isNotEmpty(specs)) {
allSpecs.addAll(specs);
}
}
}
}
}
// Try also from the query parameters:
if (uri != null) {
MultivaluedMap<String, String> parameters = uri.getQueryParameters();
if (MapUtils.isNotEmpty(parameters)) {
List<String> specs = parameters.get(DETAIL);
if (CollectionUtils.isNotEmpty(specs)) {
allSpecs.addAll(specs);
}
}
}
// Process all the obtained detail specifications:
return parseDetails(allSpecs);
}
/**
* Parses a string into the object that represents what to include.
*
* @param specs the specification of what to include or exclude
* @return the set that represents what to include, which may be completely empty, but never
* {@code null}
*/
private static Set<String> parseDetails(List<String> specs) {
// In most cases the user won't give any detail specification, so it is worth to avoid creating an expensive
// set in that case:
if (CollectionUtils.isEmpty(specs)) {
return Collections.singleton(MAIN);
}
// If the user gave a detail specification then we need first to add the default value and then parse it:
Set<String> details = new HashSet<>(2);
details.add(MAIN);
if (CollectionUtils.isNotEmpty(specs)) {
for (String spec : specs) {
if (spec != null) {
String[] chunks = spec.split("(?=[+-])");
if (ArrayUtils.isNotEmpty(chunks)) {
for (String chunk : chunks) {
chunk = chunk.trim();
if (chunk.startsWith("+")) {
chunk = chunk.substring(1).trim();
if (StringUtils.isNotEmpty(chunk)) {
details.add(chunk);
}
}
else if (chunk.startsWith("-")) {
chunk = chunk.substring(1).trim();
if (StringUtils.isNotEmpty(chunk)) {
details.remove(chunk);
}
}
else {
details.add(chunk);
}
}
}
}
}
}
return details;
}
}