blob: 96203c9e18b8a0dfa6fd266e6c39b4bacaaacc3c [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2019, 2021 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.internal.rhelp.core.http;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.function.BiPredicate;
import java.util.function.IntPredicate;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.statet.jcommons.collections.BasicImMapEntry;
import org.eclipse.statet.jcommons.collections.ImCollections;
import org.eclipse.statet.jcommons.collections.ImList;
import org.eclipse.statet.jcommons.collections.ImMapEntry;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
@NonNullByDefault
public class HttpHeaderUtils {
public static final String LOCATION_NAME= "Location"; //$NON-NLS-1$
public static final String ACCEPT_NAME= "Accept"; //$NON-NLS-1$
public static final String QUALITY_FACTOR_PARAM_NAME= "q"; //$NON-NLS-1$
public static class MediaTypeEntry extends BasicMediaType implements Comparable<MediaTypeEntry> {
private final float qualityFactor;
public MediaTypeEntry(final String type, final String subtype,
final float qualityFactor, final ImList<? extends ImMapEntry<String, String>> extParams) {
super(type, subtype, extParams);
this.qualityFactor= qualityFactor;
}
public float getQualityFactor() {
return this.qualityFactor;
}
@Override
public int compareTo(final MediaTypeEntry o) {
{ final float diff= this.qualityFactor - o.qualityFactor;
if (diff != 0) {
return (diff > 0) ? -1 : 1;
}
}
{ final int diff= compare(getType(), o.getType());
if (diff != 0) {
return diff;
}
}
{ final int diff= compare(getSubtype(), o.getSubtype());
if (diff != 0) {
return diff;
}
}
return -(getParameters().size() - o.getParameters().size());
}
}
public static class ParseException extends Exception {
private static final long serialVersionUID= 1L;
private final String errorDescription;
private final String input;
private final int errorOffset;
public ParseException(final String errorDescription, final String input, final int errorOffset) {
super(String.format("Parse error: %1$s at %3$s in:\n%2$s", errorDescription, input, errorOffset));
this.errorDescription= errorDescription;
this.input= input;
this.errorOffset= errorOffset;
}
public String getErrorDescription() {
return this.errorDescription;
}
public String getInput() {
return this.input;
}
public int getErrorOffset() {
return this.errorOffset;
}
}
public static final class HeaderBuilder {
private final StringBuilder sb= new StringBuilder();
public HeaderBuilder() {
}
public void newEntry(final String value) {
if (this.sb.length() > 0) {
this.sb.append(',');
}
this.sb.append(value);
}
public void newEntry(final String value, final float qValue) {
newEntry(value);
this.sb.append(';');
this.sb.append(QUALITY_FACTOR_PARAM_NAME);
this.sb.append('=');
this.sb.append(qValue);
}
public void addParameter(final String name, final String value) {
this.sb.append(';');
this.sb.append(name);
this.sb.append('=');
this.sb.append(value);
}
public String build() {
return this.sb.toString();
}
}
private static class Tokenizer {
private static boolean isLinearWhitespace(final char c) {
switch (c) {
case ' ':
case '\t':
case '\r':
case '\n':
return true;
default:
return false;
}
}
private static boolean isSeparator(final char c) {
switch (c) {
case '(':
case ')':
case '<':
case '>':
case '@':
case ',':
case ';':
case ':':
case '\\':
case '"':
case '/':
case '[':
case ']':
case '?':
case '=':
case '{':
case '}':
case ' ':
case '\t':
return true;
default:
return false;
}
}
private final String input;
private int index;
public Tokenizer(final String input) {
this.input= input;
}
private int consumeWhitespace(int idx) {
while (idx < this.input.length() && isLinearWhitespace(this.input.charAt(idx))) {
idx++;
}
return idx;
}
private int consumeToken(int idx) {
while (idx < this.input.length()) {
final char c= this.input.charAt(idx);
if (c > 0x20 && c < 0x7F && !isSeparator(c)) {
idx++;
}
else {
break;
}
}
return idx;
}
public void readOperatorAssert(final char c) throws ParseException {
int idx= this.index;
idx= consumeWhitespace(idx);
if (idx < this.input.length() && this.input.charAt(idx) == c) {
this.index= idx + 1;
return;
}
throw new ParseException("operator '" + c + "' expected", this.input, idx);
}
public String readTokenAssert() throws ParseException {
int idx= this.index;
idx= consumeWhitespace(idx);
final int start= idx;
idx= consumeToken(idx);
if (start < idx) {
this.index= idx;
return this.input.substring(start, idx);
}
throw new ParseException("token expected", this.input, idx);
}
private String readParamValue(int idx) throws ParseException {
if (idx < this.input.length()) {
if (this.input.charAt(idx) == '"') {
idx++;
final int start= idx;
int end= -1;
while (idx < this.input.length()) {
final char c= this.input.charAt(idx);
switch (c) {
case '"':
end= idx;
idx++;
break;
case '\\':
idx++;
if (idx < this.input.length()) {
idx++;
}
continue;
default:
idx++;
continue;
}
}
this.index= idx;
if (end >= 0) {
return this.input.substring(start, end);
}
throw new ParseException("closing quote ('\"') expected", this.input, idx);
}
else {
final int start= idx;
idx= consumeToken(idx);
this.index= idx;
return this.input.substring(start, idx);
}
}
this.index= idx;
return "";
}
public String readParamValue() throws ParseException {
int idx= this.index;
idx= consumeWhitespace(idx);
return readParamValue(idx);
}
public void skipParamValue() throws ParseException {
int idx= this.index;
idx= consumeWhitespace(idx);
if (idx < this.input.length()) {
if (this.input.charAt(idx) == '"') {
idx++;
int end= -1;
while (idx < this.input.length()) {
final char c= this.input.charAt(idx);
switch (c) {
case '"':
end= idx;
idx++;
break;
case '\\':
idx++;
if (idx < this.input.length()) {
idx++;
}
continue;
default:
idx++;
continue;
}
}
this.index= idx;
if (end >= 0) {
return;
}
throw new ParseException("closing quote ('\"') expected", this.input, idx);
}
else {
idx= consumeToken(idx);
this.index= idx;
return;
}
}
this.index= idx;
}
public float readQValue() throws ParseException {
int idx= this.index;
idx= consumeWhitespace(idx);
final int start= idx;
try {
idx= consumeToken(idx);
this.index= idx;
return Float.parseFloat(this.input.substring(start, idx));
}
catch (final NumberFormatException e) {
throw new ParseException("q value (number) expected", this.input, start);
}
}
public boolean readNextEntry() {
int idx= this.index;
idx= consumeWhitespace(idx);
if (idx < this.input.length()) {
if (this.index == 0) {
return true;
}
if (this.input.charAt(idx) == ',') {
this.index= idx + 1;
return true;
}
}
this.index= idx;
return false;
}
public boolean readNextParam() {
int idx= this.index;
idx= consumeWhitespace(idx);
if (idx < this.input.length() && this.input.charAt(idx) == ';') {
this.index= idx + 1;
return true;
}
this.index= idx;
return false;
}
public void assertEnd() throws ParseException {
int idx= this.index;
idx= consumeWhitespace(idx);
this.index= idx;
if (idx == this.input.length()) {
return;
}
throw new ParseException("EOF expected", this.input, idx);
}
}
public static List<MediaTypeEntry> readAcceptHeaderEntries(final HttpServletRequest req,
final @Nullable BiPredicate<String, String> filter)
throws ServletException {
try {
final List<MediaTypeEntry> entries= new ArrayList<>();
final Enumeration<String> headers= req.getHeaders(ACCEPT_NAME);
if (headers != null) {
parseMediaTypes(headers, filter, entries);
}
entries.sort(null);
return entries;
}
catch (final ParseException e) {
throw new ServletException("Failed to read Accept header of request.", e);
}
}
public static List<MediaTypeEntry> readMediaTypeEntries(final Collection<String> headers,
final @Nullable BiPredicate<String, String> filter)
throws ParseException {
final List<MediaTypeEntry> entries= new ArrayList<>();
if (!headers.isEmpty()) {
parseMediaTypes(Collections.enumeration(headers), filter, entries);
}
entries.sort(null);
return entries;
}
protected final static void parseMediaTypes(final Enumeration<String> headers,
final @Nullable BiPredicate<String, String> filter, final List<MediaTypeEntry> entries) throws ParseException {
final List<ImMapEntry<String, String>> extParams= new ArrayList<>();
while (headers.hasMoreElements()) {
final Tokenizer tokenizer= new Tokenizer(headers.nextElement());
while (tokenizer.readNextEntry()) {
extParams.clear();
final String type= tokenizer.readTokenAssert();
tokenizer.readOperatorAssert('/');
final String subtype= tokenizer.readTokenAssert();
final boolean include= (filter == null || filter.test(type, subtype));
float qValue= 1;
for (int nParam= 0; tokenizer.readNextParam(); nParam++) {
final String paramName= tokenizer.readTokenAssert();
tokenizer.readOperatorAssert('=');
if (!include) {
tokenizer.skipParamValue();
continue;
}
if (nParam == 0 && paramName.equals(QUALITY_FACTOR_PARAM_NAME)) {
qValue= tokenizer.readQValue();
}
else {
final String paramValue= tokenizer.readParamValue();
extParams.add(new BasicImMapEntry<>(paramName, paramValue));
}
}
entries.add(new MediaTypeEntry(type, subtype, qValue,
ImCollections.toList(extParams) ));
}
tokenizer.assertEnd();
}
}
public static int findFirstValid(final List<MediaTypeEntry> entries,
final String parameterName, final IntPredicate paramPredicate) {
for (final MediaTypeEntry entry : entries) {
final String vString= entry.getParameterValue(parameterName);
if (vString != null) {
try {
final int v= Integer.parseInt(vString);
if (paramPredicate.test(v)) {
return v;
}
}
catch (final NumberFormatException e) {}
}
}
return -1;
}
}