ExchangeAttributeParser.java

/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual 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 io.undertow.attribute;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.ServiceLoader;

import io.undertow.UndertowLogger;
import io.undertow.UndertowMessages;

/**
 * Attribute parser for exchange attributes. This builds an attribute from a string definition.
 * <p>
 * This uses a service loader mechanism to allow additional token types to be loaded. Token definitions are loaded
 * from the provided class loader.
 *
 * @author Stuart Douglas
 * @see ExchangeAttributes#parser(ClassLoader)
 */
public class ExchangeAttributeParser {

    private final List<ExchangeAttributeBuilder> builders;
    private final List<ExchangeAttributeWrapper> wrappers;

    ExchangeAttributeParser(final ClassLoader classLoader, List<ExchangeAttributeWrapper> wrappers) {
        this.wrappers = wrappers;
        ServiceLoader<ExchangeAttributeBuilder> loader = ServiceLoader.load(ExchangeAttributeBuilder.class, classLoader);
        final List<ExchangeAttributeBuilder> builders = new ArrayList<>();
        for (ExchangeAttributeBuilder instance : loader) {
            builders.add(instance);
        }
        //sort with highest priority first
        Collections.sort(builders, new Comparator<ExchangeAttributeBuilder>() {
            @Override
            public int compare(ExchangeAttributeBuilder o1, ExchangeAttributeBuilder o2) {
                return Integer.compare(o2.priority(), o1.priority());
            }
        });
        this.builders = Collections.unmodifiableList(builders);

    }

    /**
     * Parses the provided value string, and turns it into a list of exchange attributes.
     * <p>
     * Tokens are created according to the following rules:
     * <p>
     * %a - % followed by single character. %% is an escape for a literal %
     * %{.*}a? - % plus curly braces with any amount of content inside, followed by an optional character
     * ${.*} - $ followed by a curly braces to reference an item from the predicate context
     *
     * @param valueString
     * @return
     */
    public ExchangeAttribute parse(final String valueString) {
        final List<ExchangeAttribute> attributes = new ArrayList<>();
        int pos = 0;
        int state = 0; //0 = literal, 1 = %, 2 = %{, 3 = $, 4 = ${
        int braceDepth = 0; // Track the depth of curly braces
        for (int i = 0; i < valueString.length(); ++i) {
            char c = valueString.charAt(i);
            switch (state) {
                case 0: {
                    if (c == '%' || c == '$') {
                        if (pos != i) {
                            attributes.add(wrap(parseSingleToken(valueString.substring(pos, i))));
                            pos = i;
                        }
                        if (c == '%') {
                            state = 1;
                        } else {
                            state = 3;
                        }
                    }
                    break;
                }
                case 1: {
                    if (c == '{') {
                        state = 2;
                        braceDepth++;
                    } else if (c == '%') {
                        //literal percent
                        attributes.add(wrap(new ConstantExchangeAttribute("%")));
                        pos = i + 1;
                        state = 0;
                    } else {
                        attributes.add(wrap(parseSingleToken(valueString.substring(pos, i + 1))));
                        pos = i + 1;
                        state = 0;
                    }
                    break;
                }
                case 2: {
                    if (c == '{') {
                        braceDepth++;
                    } else if (c == '}') {
                        braceDepth--;
                        if (braceDepth == 0) {
                            attributes.add(wrap(parseSingleToken(valueString.substring(pos, i + 1))));
                            pos = i + 1;
                            state = 0;
                        }
                    }
                    break;
                }
                case 3: {
                    if (c == '{') {
                        state = 4;
                        braceDepth++;
                    } else if (c == '$') {
                        //literal dollars
                        attributes.add(wrap(new ConstantExchangeAttribute("$")));
                        pos = i + 1;
                        state = 0;
                    } else {
                        attributes.add(wrap(parseSingleToken(valueString.substring(pos, i + 1))));
                        pos = i + 1;
                        state = 0;
                    }
                    break;
                }
                case 4: {
                    if (c == '}') {
                        braceDepth--;
                        if (braceDepth == 0) {
                            attributes.add(wrap(parseSingleToken(valueString.substring(pos, i + 1))));
                            pos = i + 1;
                            state = 0;
                        }
                    } else if (c == '{') {
                        braceDepth++;
                    }
                    break;
                }
            }
        }
        if (state != 0 || braceDepth != 0) {
            throw UndertowMessages.MESSAGES.mismatchedBraces(valueString);
        }
        if (pos != valueString.length()) {
            attributes.add(wrap(parseSingleToken(valueString.substring(pos))));
        }
        if (attributes.size() == 1) {
            return attributes.get(0);
        }
        return new CompositeExchangeAttribute(attributes.toArray(new ExchangeAttribute[attributes.size()]));
    }

    public ExchangeAttribute parseSingleToken(final String token) {
        for (final ExchangeAttributeBuilder builder : builders) {
            ExchangeAttribute res = builder.build(token);
            if (res != null) {
                return res;
            }
        }
        if (token.startsWith("%")) {
            UndertowLogger.ROOT_LOGGER.unknownVariable(token);
        }
        return new ConstantExchangeAttribute(token);
    }

    private ExchangeAttribute wrap(ExchangeAttribute attribute) {
        ExchangeAttribute res = attribute;
        for(ExchangeAttributeWrapper w : wrappers) {
            res = w.wrap(res);
        }
        return res;
    }

}