| /*=============================================================================# |
| # 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; |
| } |
| |
| } |