Java tutorial
/* * Copyright 2017 LINE Corporation * * LINE Corporation licenses this file to you 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: * * https://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 com.linecorp.armeria.server.logging; import static com.google.common.base.Preconditions.checkArgument; import static com.linecorp.armeria.server.logging.AccessLogComponent.ofPredefinedCommon; import static com.linecorp.armeria.server.logging.AccessLogComponent.ofQuotedRequestHeader; import static com.linecorp.armeria.server.logging.AccessLogComponent.ofText; import static com.linecorp.armeria.server.logging.AccessLogType.AUTHENTICATED_USER; import static com.linecorp.armeria.server.logging.AccessLogType.REMOTE_HOST; import static com.linecorp.armeria.server.logging.AccessLogType.REQUEST_LINE; import static com.linecorp.armeria.server.logging.AccessLogType.REQUEST_TIMESTAMP; import static com.linecorp.armeria.server.logging.AccessLogType.RESPONSE_LENGTH; import static com.linecorp.armeria.server.logging.AccessLogType.RESPONSE_STATUS_CODE; import static com.linecorp.armeria.server.logging.AccessLogType.RFC931; import static java.util.Objects.requireNonNull; import java.util.List; import java.util.Set; import java.util.function.Function; import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.linecorp.armeria.common.HttpHeaderNames; import com.linecorp.armeria.common.HttpHeaders; import com.linecorp.armeria.common.HttpStatus; import com.linecorp.armeria.server.logging.AccessLogComponent.AttributeComponent; import com.linecorp.armeria.server.logging.AccessLogComponent.CommonComponent; import com.linecorp.armeria.server.logging.AccessLogComponent.RequestHeaderComponent; import com.linecorp.armeria.server.logging.AccessLogComponent.TextComponent; import io.netty.util.AsciiString; /** * Pre-defined access log formats and the utility methods for {@link AccessLogComponent}. */ final class AccessLogFormats { private static final AccessLogComponent BLANK = ofText(" "); /** * A common log format. */ static final List<AccessLogComponent> COMMON = ImmutableList.of(ofPredefinedCommon(REMOTE_HOST), BLANK, ofPredefinedCommon(RFC931), BLANK, ofPredefinedCommon(AUTHENTICATED_USER), BLANK, ofPredefinedCommon(REQUEST_TIMESTAMP), BLANK, ofPredefinedCommon(REQUEST_LINE), BLANK, ofPredefinedCommon(RESPONSE_STATUS_CODE), BLANK, ofPredefinedCommon(RESPONSE_LENGTH)); /** * A combined log format. */ static final List<AccessLogComponent> COMBINED = ImmutableList.of(ofPredefinedCommon(REMOTE_HOST), BLANK, ofPredefinedCommon(RFC931), BLANK, ofPredefinedCommon(AUTHENTICATED_USER), BLANK, ofPredefinedCommon(REQUEST_TIMESTAMP), BLANK, ofPredefinedCommon(REQUEST_LINE), BLANK, ofPredefinedCommon(RESPONSE_STATUS_CODE), BLANK, ofPredefinedCommon(RESPONSE_LENGTH), BLANK, ofQuotedRequestHeader(HttpHeaderNames.REFERER), BLANK, ofQuotedRequestHeader(HttpHeaderNames.USER_AGENT), BLANK, ofQuotedRequestHeader(HttpHeaderNames.COOKIE)); @VisibleForTesting static List<AccessLogComponent> parseCustom(String formatStr) { requireNonNull(formatStr, "formatStr"); final ImmutableList.Builder<AccessLogComponent> builder = ImmutableList.builder(); final StringBuilder textBuilder = new StringBuilder(); Condition.Builder condBuilder = null; String variable = null; State state = State.TEXT; for (int i = 0; i < formatStr.length();/* Increase 'i' at the end of the loop. */) { final char ch = formatStr.charAt(i); switch (state) { case TEXT: if (ch == '%') { if (textBuilder.length() > 0) { builder.add(ofText(newStringAndReset(textBuilder))); } condBuilder = null; variable = null; state = State.PERCENT; } else { textBuilder.append(ch); } break; case PERCENT: // Loop again. if (Character.isAlphabetic(ch)) { state = State.TOKEN; continue; } // Loop again. if (Character.isDigit(ch)) { condBuilder = Condition.builder(); state = State.CONDITION; continue; } if (ch == '!') { condBuilder = Condition.builder().setSign(false); state = State.CONDITION; } else if (ch == '{') { state = State.VARIABLE; } break; case CONDITION: assert condBuilder != null; if (Character.isDigit(ch)) { textBuilder.append(ch); } else { if (textBuilder.length() > 0) { condBuilder.addHttpStatus(newStringAndReset(textBuilder)); } // Loop again. if (Character.isAlphabetic(ch)) { state = State.TOKEN; continue; } if (ch == '{') { state = State.VARIABLE; } else if (ch != ',') { throw new IllegalArgumentException("Unexpected character in condition:" + ch); } } break; case VARIABLE: if (ch != '}') { textBuilder.append(ch); } else { if (textBuilder.length() > 0) { variable = newStringAndReset(textBuilder); } state = State.TOKEN; } break; case TOKEN: builder.add(newAccessLogComponent(ch, variable, condBuilder)); state = State.TEXT; break; } // Go to the next index only if the 'switch' statement exits by 'break' statement. ++i; } if (state != State.TEXT) { throw new IllegalArgumentException("Unexpected access log format: " + formatStr); } if (textBuilder.length() > 0) { builder.add(ofText(newStringAndReset(textBuilder))); } return builder.build(); } private static AccessLogComponent newAccessLogComponent(char token, @Nullable String variable, @Nullable Condition.Builder condBuilder) { final AccessLogType type = AccessLogType.find(token) .orElseThrow(() -> new IllegalArgumentException("Unexpected token character: '" + token + '\'')); if (type.isVariableRequired()) { checkArgument(variable != null, "Token " + type.token() + " requires a variable."); } if (type.isConditionAvailable()) { if (condBuilder != null) { checkArgument(!condBuilder.isEmpty(), "Token " + type.token() + " has an invalid condition."); } } else { checkArgument(condBuilder == null, "Token " + type.token() + " does not support a condition."); } if (TextComponent.isSupported(type)) { assert variable != null; return ofText(variable); } // Do not add quotes when parsing a user-provided custom format. final boolean addQuote = false; final Function<HttpHeaders, Boolean> condition = condBuilder != null ? condBuilder.build() : null; if (CommonComponent.isSupported(type)) { return new CommonComponent(type, addQuote, condition); } if (RequestHeaderComponent.isSupported(type)) { assert variable != null; return new RequestHeaderComponent(AsciiString.of(variable), addQuote, condition); } if (AttributeComponent.isSupported(type)) { assert variable != null; final Function<Object, String> stringifier; final String[] components = variable.split(":"); if (components.length == 2) { stringifier = newStringifier(components[0], components[1]); } else { stringifier = Object::toString; } return new AttributeComponent(components[0], stringifier, addQuote, condition); } // Should not reach here. throw new Error("Unexpected access log type: " + type.name()); } private static String newStringAndReset(StringBuilder textBuilder) { final String str = textBuilder.toString(); textBuilder.setLength(0); return str; } @SuppressWarnings("unchecked") private static Function<Object, String> newStringifier(String attrName, String className) { final Function<Object, String> stringifier; try { stringifier = (Function<Object, String>) Class .forName(className, true, AccessLogFormats.class.getClassLoader()).newInstance(); } catch (Exception e) { throw new IllegalArgumentException("failed to instantiate a stringifier function: " + attrName, e); } return stringifier; } /** * A condition based on {@link HttpStatus} of the HTTP response. */ private static final class Condition implements Function<HttpHeaders, Boolean> { private final Set<HttpStatus> statusSet; private final boolean sign; Condition(Set<HttpStatus> statusSet, boolean sign) { this.statusSet = statusSet; this.sign = sign; } public Set<HttpStatus> statusSet() { return statusSet; } public boolean isSign() { return sign; } @Override public Boolean apply(HttpHeaders headers) { return statusSet.contains(headers.status()) == sign; } @Override public String toString() { return MoreObjects.toStringHelper(this).add("sign", isSign()).add("statusSet", statusSet()).toString(); } static Builder builder() { return new Builder(); } static final class Builder { private final ImmutableSet.Builder<HttpStatus> statusSet = ImmutableSet.builder(); private boolean sign = true; private boolean isEmpty = true; Builder setSign(boolean sign) { this.sign = sign; return this; } Builder addHttpStatus(String text) { final HttpStatus s = HttpStatus.valueOf(Integer.valueOf(text)); statusSet.add(s); isEmpty = false; return this; } boolean isEmpty() { return isEmpty; } Function<HttpHeaders, Boolean> build() { return new Condition(statusSet.build(), sign); } } } /** * Parsing states. */ private enum State { TEXT, PERCENT, CONDITION, VARIABLE, TOKEN } private AccessLogFormats() { } }