주요글: 도커 시작하기
반응형

이번에는 파서 콤비네이터(parser combinator)를 구현해 보자. 파서 콤비네이터는 파서를 조합해서(연결해서) 새로운 파서를 만드는 기법이다. 파싱에 기초가 되는 파서를 만들고 이들 파서를 조합해서 상위 수준의 파서를 만들게 되는데, 이를 이용하면 재귀 하향 파서와 동일한 파서를 만들 수 있다.


파서의 입력과 출력


파서 콤비네이터에서 각 파서는 입력으로 토큰스트림(토큰 버퍼)을 받으면 결과로 파싱 결과를 리턴한다. 파싱 결과에는 파싱 성공 여부, 파싱을 통해 생성된 결과, 사용할 토큰스트림이 포함된다.


ParseResult result = parser.parse(tokenBuffer);

// result.isSuccess() : 성공 여부

// result.getValue() : 파싱 결과 값

// result.getTokenBuffer() : 이후 파싱에서 사용할 토큰스트림


예를 들어, 아래 문법에서 ALLOW, DENY, COMMA, IPRANGE는 개별 토큰에 매칭되는 단말 기호이다.


config : allowOrDenyDeclList orderDecl?;

allowOrDenyDeclList : allowOrDenyDecl*;

allowOrDenyDecl : allowDecl | denyDecl;

allowDecl : ALLOW IPRANGE;

denyDecl : DENY IPRANGE;

orderDecl : ORDER orderChoice;

orderChoice : allowDeny | denyAllow;;

allowDeny : ALLOW COMMA DENY;

denyAllow : DENY COMMA ALLOW; 

ORDER : 'order';

ALLOW : 'allow';

DENY : 'deny';

COMMA : ','

IPRANGE : DIGIT+ '.' DIGIT+ '.' DIGIT+ '.' DIGIT+ ( ('/' | '-') DIGIT+)?;


이들 단말 기호를 위한 파서를 TerminalParser라고 해 보자. 이 경우 TerminalParser는 다음과 같이 해당 타입의 토큰을 만나면 파싱에 성공하고, 다른 타입의 토큰을 만나면 파싱에 실패한다.


// TT_ORDER 타입 토큰을 파싱하는 파서

TerminalParser parser = new TerminalParser(TokenType.TT_ORDER);


TokenBuffer tokens = new TokenBuffer(asList(orderToken, allowToken));


ParseResult result = parser.parse(tokens);

// result.isSuccess() -> true : tokens의 첫 번째 토큰이 TT_ORDER 타입이므로 파싱 성공

// result.getValue() -> "order" : 파싱에 성공했으므로 파싱 결과 존재

// result.getTokenBuffer() -> [allowToken] : orderToken은 parser가 파싱해서 사용


ParseResult result2 = parser.parse(result.getTokenBuffer());

// result.isSuccess() -> false : 토큰버퍼의 첫 번째 토큰(allowToken)이 TT_ORDER가 아니므로 실패

// result.getValue() -> null : 파싱 실패했으므로 결과 없음

// result.getTokenBuffer() -> [allowToken]


파싱에 성공하면 토큰을 사용하므로 토큰 버퍼가 다음 토큰으로 이동하고, 파싱에 실패하면 토큰 버퍼 위치가 바뀌지 않는다.


Parser, Action, ParseResult


모든 파서 구현은 동일한 방식으로 동작한다. 즉, 토큰 버퍼를 입력으로 받고 결과로 ParseResult를 리턴한다. 따라서 파서를 위한 상위 타입을 다음과 같이 정의해 볼 수 있다.


public abstract class Parser<I,R> {


    private Action<I,R> action = null;


    public Parser(Action<I,R> action) {

        this.action = action;

    }


    public abstract ParseResult<R> parse(TokenBuffer tokenBuffer);


    protected R action(I input) {

        return action.tranform(input);

    }


}


파서는 파싱 과정에서 입력 결과물을 새로운 결과물로 변환하는데 이때 Action을 사용한다. 이 Action에 입력을 주면 결과로 변환된 값을 생성하는데, 이때 입력과 출력 타입이 각각 I와 R이 된다.

  • parse() 메서드 : 추상 메서드로 각 파서마다 알맞게 구현한다.
  • action() 메서드와 action 필드 : action을 이용해서 변환하는 기능이다. 모든 하위 파서에서 사용한다. 

Action의 정의는 다음과 같다.


public interface Action<I, R> {

    R tranform(I input);

}


ParseResult는 다음과 같다.


public class ParseResult<R> {

    private boolean success;

    private R value;

    private TokenBuffer tokenBuffer;


    public ParseResult(boolean success, R value, TokenBuffer tokenBuffer) {

        this.success = success;

        this.value = value;

        this.tokenBuffer = tokenBuffer;

    }


    public boolean isSuccess() {

        return success;

    }


    public R getValue() {

        return value;

    }


    public TokenBuffer getTokenBuffer() {

        return tokenBuffer;

    }


}


TerminalParser 구현


문법 구조에서 단말 기호를 파싱하는 TerminalParser를 만들어보자.


public class TerminalParser<R> extends Parser<String, R> {

    private TokenType type;


    public TerminalParser(TokenType type, Action<String, R> action) {

        super(action);

        this.type = type;

    }


    @Override

    public ParseResult<R> parse(TokenBuffer tokenBuffer) {

        Token token = tokenBuffer.currentToken();

        if (token.getType() == type) {

            tokenBuffer.moveNext();

            R result = action(token.getValue());

            return new ParseResult(true, result, tokenBuffer);

        } else {

            return new ParseResult(false, null, tokenBuffer);

        }

    }

}


TerminalParser는 단말 파서로 개별 토큰을 파싱한다. 예를 들어, "allow"나 "order"와 같은 토큰을 파싱할 때 TerminalParser를 사용한다. TerminalParser는 파싱 가능한 토큰 타입을 가지며, 현재 토큰이 지정한 토큰 타입과 일치하면 action()을 이용해서 토큰 값을 변환한다.


토큰 값은 String이므로, TerminalParser의 입력은 String이 된다. 이런 이유로 TerminalParser<R>은 입력이 String 타입인 Parser<String,R>을 상속받는다.


다음은 TerminalParser의 사용 예다.


TerminalParser<String> allowParser = new TerminalParser<>(TokenType.TT_ALLOW, v -> v);


TerminalParser<IpRange> ipRangeParser = 

        new TerminalParser<>(TokenType.TT_IPRANGE, v -> new IpRange(v));


TokenBuffer tokens = new TokenBuffer(

    asList(

        new Token(TokenType.TT_ALLOW, "allow"), 

        new Token(TokenType.TT_IPRANGE, "1.2.3.4")

    ));


ParseResult<String> result1 = allowParser.parse(tokens);

// result1.isSuccess() : true, result1.getValue() : "allow"


ParseResult<IpRange> result2 = ipRangeParser.parse(result1.getTokenBuffer());

// result2.isSuccess() : true, result2.getValue() : IpRange("1.2.3.4")


시퀀스 파서


다음을 보자.


orderDecl : ORDER orderChoice;


orderDecl을 파싱하는 파서는 ORDER와 orderChoice를 순서대로 파싱해야 한다. 여기서 ORDER와 orderChoice를 위한 파서가 존재한다고 가정해 보자. orderDecl 파서는 ORDER 파서와 orderChoice 파서를 차례대로 실행한 뒤 두 파서의 결과를 이용해서 새로운 결과를 만들면 된다. ORDER 파서가 파싱에 성공하고 orderChoice 파서가 파싱에 실패하면 orderDecl은 파싱에 실패한다. ORDER 파서와 orderChoice 파서가 모두 성공해야 orderDecl은 파싱에 성공한다.


SequenceParser는 순서에 맞게 한 개 이상의 파서를 차례대로 실행하고 각 파서의 결과를 모아서 새로운 결과를 만든다. n개의 파서의 결과를 사용하므로 Action의 입력값은 List가 된다. 중간에 파싱에 성공하지 못하는 파서가 존재하면 SequenceParser는 파싱에 실패한다.


SequenceParser의 구현 코드는 다음과 같다. 지네릭 타입 파라미터 때문에 코드가 다소 복잡하다.


public class SequenceParser<I,R,T> extends Parser<List<R>,T> {


    // parsers에 속한 각 Parser의 입력과 출력 타입은 서로 다를 수 있기 때문에

    // I는 parsers에 속한 각 Parser의 입력 타입의 공통 상위 타입이다.

    // 비슷하게 R은 각 Parser의 출력 타입의 공통 상위 타입이다.

    private List<? extends Parser<? extends I,? extends R>> parserList;


    public SequenceParser(List<? extends Parser<? extends I,? extends R>> parsers,

                              Action<List<R>,T> action) {

        super(action);

        if (parsers == null || parsers.size() == 0) {

            throw new IllegalArgumentException("no parsers");

        }

        parserList = parsers;

    }


    @Override

    public ParseResult<T> parse(TokenBuffer tokenBuffer) {

        int pos = tokenBuffer.currentPosition();

        Iterator<? extends Parser<? extends I,? extends R>> parserIter = parserList.iterator();

        Parser<? extends I,? extends R> firstParser = parserIter.next();

        ParseResult<? extends R> lastResult = firstParser.parse(tokenBuffer);

        if (lastResult.isSuccess()) {

            List<R> values = new ArrayList<>();

            values.add(lastResult.getValue());

            while (parserIter.hasNext() && lastResult.isSuccess()) {

                lastResult = parserIter.next().parse(tokenBuffer);

                if (lastResult.isSuccess()) {

                    values.add(lastResult.getValue());

                } else {

                    throw new MatchingTokenNotFoundException();

                }

            }

            T ret = action(values);

            return new ParseResult(true, ret, tokenBuffer);

        } else {

            tokenBuffer.resetPosition(pos);

            return new ParseResult(false, null, tokenBuffer);

        }

    }

}


SequenceParser는 n개의 Parser를 입력받는다. 이 Parser들의 입력을 I, 출력을 R이라고 했을 때, SequenceParser는 R의 List를 입력받아 T 타입의 결과를 만든다. 따라서 값을 변환해주는 Action의 입력 타입이 List<R>이고 결과 타입이 T가 된다.


parse() 코드를 보면 첫 번째 Parser를 이용해서 먼저 파싱한다. 첫 번째 Parser가 파싱에 성공하면, 이후 나머지 파서를 이용해서 차례대로 파싱을 진행한다. 첫 번째 Parser가 파싱에 성공한 이후 나머지 Parser에서 파싱에 실패하면 문법에 맞지 않다는 것을 의미하므로 익셉션을 발생시킨다. 모든 Parser가 파싱에 성공하면 action() 메서드를 이용해서 각 Parser의 결과를 담은 List 객체를 변환한다.


다음은 SequenceParser의 사용 예다.


SequenceParser<String, String, Boolean> allowDeny =

    new SequenceParser<>(asList(allowParser, commandParser, denyParser), 

    vals -> true); // Action<List<String>,Boolean>>


ParseResult<Boolean> result = allowDeny.parse(tokenBuffer);

if (result.isSuccess()) {

    Boolean value = result.getValue();

    ...

}


allowParser, commandParser, denyParser는 모두 입력과 출력이 String 타입이기 때문에 SequenceParser의 두 타입 파라미터 값으로 String, String을 주었다. 또한, 파싱 결과 타입으로 Boolean을 사용한다. 따라서, Action은 입력이 List<String>이고 출력은 Boolean이 된다.


allowParser, commandParser, denyParser가 모두 파싱에 성공하면 각각 결과 값으로 "allow", ",", "deny"를 생성한다. 따라서 Action에 전달되는 List는 ["allow", ",", "deny"]가 된다. 위 코드에서는 Action을 위한 람다식의 vals 파라미터가 List로서 ["allow", ",", "deny"] 값을 갖게 된다.


참고로 SequenceParser<String, String, Boolean> 타입은 Parser<List<String>, Boolean>의 하위 타입이므로 다음과 같이 할당 가능하다.


Parser<List<String>, Boolean> allowDeny =

    new SequenceParser<>(asList(allowParser, commandParser, denyParser), 

    vals -> true);


반복 파서


다음 문법을 보자.


allowOrDenyDeclList : allowOrDenyDecl*;

 

이 문법은 allowOrDenyDecl이 0번 이상 반복해서 출현함을 뜻한다. 이런 반복을 처리하기 위한 파서는 파싱에 실패할 때 까지 동일 파서를 반복해서 실행하면 된다. 반복 처리를 위한 RepetitionParser는 다음과 같다.


public class RepetitionParser<I,R,T> extends Parser<List<R>,T> {

    private boolean oneOrMore;

    private Parser<I,R> parser;


    public RepetitionParser(boolean oneOrMore, Parser<I,R> parser, Action<List<R>, T> action) {

        super(action);

        if (parser == null) {

            throw new IllegalArgumentException("no parser");

        }

        this.oneOrMore = oneOrMore;

        this.parser = parser;

    }


    @Override

    public ParseResult<T> parse(TokenBuffer tokenBuffer) {

        List<R> values = new ArrayList<>();

        ParseResult<R> lastResult = parser.parse(tokenBuffer);

        if (lastResult.isSuccess()) {

            values.add(lastResult.getValue());

        }

        // oneOrMore가 true면 최소 한 번은 파싱 성공 필요

        if (oneOrMore && !lastResult.isSuccess()) {

            return new ParseResult<>(false, null, tokenBuffer);

        }

        // 파싱에 실패할 때까지 반복해서 파싱

        while (lastResult.isSuccess()) {

            lastResult = parser.parse(tokenBuffer);

            if (lastResult.isSuccess()) {

                values.add(lastResult.getValue());

            }

        }

        T ret = action(values);

        return new ParseResult(true, ret, tokenBuffer);

    }

}


RepetitionParser는 동일한 내용을 반복해서 파싱하므로, 파싱할 때 사용할 파서를 인자로 받는다. parse() 메서드는 이 파서를 이용해서 파싱을 한다. oneOrMore는 최소 1번 이상 파싱에 성공해야 할지 여부를 지정한다. oneOrMore가 true면 최소 한 번 이상 파싱에 성공해야 한다. oneOrMore가 true인데 첫 번째 파싱에 실패하면 실패 결과를 리턴한다.


parse() 메서드는 최초 한 번 파싱을 수행한 뒤, while을 이용해서 파싱에 실패할 때까지 파싱을 반복해서 수행한다. while()이 종료되면 action()을 이용해서 파싱 결과 List를 변환하고, 파싱 성공 결과를 리턴한다.


선택 파서


다음을 보자.


orderChoice: allowDeny | denyAllow;


orderChoice는 allowDeny나 denyAllow 중 하나가 일치하면 파싱에 성공한다. 이를 위한 ChoiceParser는 다음과 같이 구현할 수 있다.


public class ChoiceParser<I,R,T> extends Parser<R,T> {

    private List<? extends Parser<? extends I,? extends R>> parsers;


    public ChoiceParser(List<? extends Parser<? extends I, ? extends R>> parsers,

                           Action<R,T> action) {

        super(action);

        if (parsers == null || parsers.size() == 0) {

            throw new IllegalArgumentException("no parsers");

        }

        this.parsers = parsers;

    }


    @Override

    public ParseResult<T> parse(TokenBuffer tokenBuffer) {

        int pos = tokenBuffer.currentPosition();

        ParseResult<? extends R> lastResult;

        Iterator<? extends Parser<? extends I,? extends R>> parserIter = parsers.iterator();

        do {

            Parser<? extends I,? extends R> parser = parserIter.next();

            lastResult = parser.parse(tokenBuffer);

        } while (parserIter.hasNext() && !lastResult.isSuccess());

        if (lastResult.isSuccess()) {

            T ret = action(lastResult.getValue());

            return new ParseResult<>(true, ret, tokenBuffer);

        } else {

            tokenBuffer.resetPosition(pos);

            return new ParseResult<>(false, null, tokenBuffer);

        }

    }

}


ChoiceParser는 파싱할 때 사용할 Parser 목록을 전달받는다. SequenceParser와 마찬가지로 각 파서의 입력과 출력을 위한 공통 상위 타입으로 I와 R을 사용한다. 


parse() 메서드는 parsers의 파서를 차례대로 이용해서 파싱을 시도한다. 파싱에 성공하면 do-while을 중단해서 나머지 파서를 진행하지 않는다.


do-while 종료 후 파싱에 성공한 파서가 존재하면 해당 파싱 결과를 action()으로 변환하고 성공 결과를 리턴한다. 파싱에 성공한 파서가 존재하지 않으면 실패 결과를 리턴한다.


옵션 파서


마지막으로 만들 파서는 다음을 처리하기 위한 파서이다.


config : allowOrDeny* orderDecl?;


orderDecl은 존재할 수도 있고 없을 수도 있다. config를 파싱하는데 orderDecl이 없어도 파싱에 실패하지 않는다. 이를 파싱하기 위한 OptionParser는 다음과 같다.


public class OptionParser<I,R> extends Parser<I,Optional<R>> {

    private Parser parser;


    public OptionParser(Parser<I,R> parser) {

        super(null);

        if (parser == null)

            throw new IllegalArgumentException();


        this.parser = parser;

    }


    @Override

    public ParseResult<Optional<R>> parse(TokenBuffer tokenBuffer) {

        int pos = tokenBuffer.currentPosition();

        ParseResult<R> result = parser.parse(tokenBuffer);

        if (result.isSuccess()) {

            return new ParseResult(true, Optional.ofNullable(result.getValue()), tokenBuffer);

        } else {

            tokenBuffer.resetPosition(pos);

        }

        return new ParseResult(true, Optional.empty(), tokenBuffer);

    }


}


OptionParser는 파싱 결과가 존재할 수도 있고 없을 수도 있기 때문에, 결과 값으로 Optional을 사용한다. 내부적으로 parser의 파싱 결과가 성공이든 실패든 OptionParser 자체는 성공을 결과로 리턴한다. 내부적으로 파싱에 성공하면 성공 결과 값을 갖는 Optional을 생성해서 ParserResult를 리턴하고, 그렇지 않으면 값이 없는 Optional을 이용해서 ParserResult를 리턴한다.


파서 콤비네이터를 이용한 IpFilter 파서 구현


필요한 파서를 모두 구현했다. 남은 건 이들 파서를 조합해서 IpFilter를 위한 파서를 만드는 일만 남았다. 지네릭 타입이 막 출현해서 코드가 다소 혼란스러울 수 있는데, 그런 경우 지네릭 타입 관련 부분을 지우고 코드를 보면 도움이 될 것이다.


여기서 만들 IpFilterCombiParser는 내부적으로 앞서 구현한 파서들을 조합해서 파싱을 처리한다. 이 클래스는 먼저 문법에서 단말 부분의 파싱을 위한 TerminalParser를 정의한다.


public class IpFilterCombiParser {

    // termianl

    private TerminalParser<String> allowParser

            new TerminalParser<>(TokenType.TT_ALLOW, v -> v);

    private TerminalParser<String> denyParser

            new TerminalParser<>(TokenType.TT_DENY, v -> v);

    private TerminalParser<IpRange> iprangeParser

            new TerminalParser<>(TokenType.TT_IPRANGE, v -> new IpRange(v));

    private TerminalParser<String> orderParser

            new TerminalParser<>(TokenType.TT_ORDER, v -> v);

    private TerminalParser<String> commaParser

            new TerminalParser<>(TokenType.TT_COMMA, v -> v);


각 TerminalParser는 한 개 토큰 타입에 해당하는 토큰을 파싱한다. iprangeParser는 파싱 결과로 IpRange를 생성하고 나머지는 토큰 값을 그대로 파싱 결과로 사용한다.


다음은 문법에서 아래 부분을 위한 코드이다.


allowOrDenyDeclList : allowOrDenyDecl*;

allowOrDenyDecl : allowDecl | denyDecl;

allowDecl : ALLOW IPRANGE;

denyDecl : DENY IPRANGE;


이 문법을 위한 파서는 다음과 같다.


public class IpFilterCombiParser {

    ...


    // allowDecl : ALLOW iprange;

    private SequenceParser<String, Object, Tuple2<String,IpRange>> allowDecl =

            new SequenceParser<>(

                    Arrays.asList(allowParser, iprangeParser),

                    vals -> new Tuple2<>((String)vals.get(0), (IpRange)vals.get(1))

            );


    // denyDecl : DENY iprange;

    private SequenceParser<String, Object, Tuple2<생략>> denyDecl =

            new SequenceParser<>(

                    Arrays.asList(denyParser, iprangeParser),

                    vals -> new Tuple2<>((String)vals.get(0), (IpRange)vals.get(1))

            );


    // allowOrDenyDecl : allowDecl | denyDecl

    private ChoiceParser<String, Tuple2<생략>, Tuple2<생략>> allowOrDenyDecl =

            new ChoiceParser(Arrays.asList(allowDecl, denyDecl), val -> val);


    // allowOrDenyDeclList : allowOrDenyDecl*

    private RepetitionParser<Tuple2<생략>, Tuple2<생략>, List<Tuple2<생략>>> allowOrDenyList =

            new RepetitionParser(false, allowOrDenyDecl, vals -> vals);



Tuple2는 별도로 만든 클래스로서 두 개의 값을 갖는 객체를 의미한다. 이 타입을 만든 이유는 allowDecl이나 denyDecl을 파싱한 결과로 ("allow", IpRange) 또는 ("deny", IpRange)를 생성하기 위함이다. (자바도 튜플을 지원하는 날이 오겠지.)


allowDecl은 allowParser와 iprangeParser를 차례대로 파싱하는 SequenceParser이다. allowParser의 결과는 String이고 IprangeParser의 결과는 IpRange이므로 Action에 전달되는 객체는 List<Object>이다. 따라서, Action 실행을 위한 람다식은 List의 첫 번째 값을 String으로 타입 변환하고 두 번째 값을 IpRange로 타입 변환한 뒤, 이 두 값을 이용해서 Tuple<String, IpRange> 객체를 생성하고 있다.


// allowDecl의 Action을 위한 람다식: vals은 List<Object>

vals -> new Tuple2<>((String)vals.get(0), (IpRange)vals.get(1))


allowOrDenyDecl은 allowDecl과 denyDecl 중 하나를 이용해서 파싱하고, 그 결과로 Tuple2를 생성한다. allowDecl과 denyDecl이 모두 결과로 Tuple2를 리턴하므로, allowOrDenyDecl의 Action은 성공한 파서의 결과를 그대로 리턴한다. (람다식을 보면 val -> val 인데, 이는 입력 받은 값을 그대로 리턴함을 뜻한다.)


allowOrDenyList는 allowOrDenyDecl을 반복해서 파싱한다. allowOrDenyDecl의 출력은 Tuple이므로, allowOrDenyList는 List<Tuple>을 결과로 리턴한다.


다음으로 만들 파서는 다음 문법을 처리한다.


orderDecl : ORDER orderChoice;

orderChoice : allowDeny | denyAllow;;

allowDeny : ALLOW COMMA DENY;

denyAllow : DENY COMMA ALLOW; 


이를 위한 각 파서는 다음과 같다.


    // allowDeny : ALLOW ',' DENY;

    private SequenceParser<String, String, Boolean> allowDeny =

            new SequenceParser<>(Arrays.asList(allowParser, commaParser, denyParser), 

                                       vals -> true);


    // denyAllow : DENY ',' ALLOW

    private SequenceParser<String, String, Boolean> denyAllow =

            new SequenceParser<>(Arrays.asList(denyParser, commaParser, allowParser), 

                                       vals -> false);


    // orderChoice: allowDeny | denyAllow

    private Parser<Boolean, Boolean> orderChoice =

            new ChoiceParser<>(Arrays.asList(allowDeny, denyAllow), val -> val);


    // orderDecl : ORDER orderChoice

    private SequenceParser<Object, Object, Boolean> orderDecl =

            new SequenceParser<>(Arrays.asList(orderParser, orderChoice), 

                                       vals -> (Boolean)vals.get(1));


"order allow, deny"에서 allowDeny가 파싱하는 부분은 "allow,deny" 부분이다. 이는 IpFilter가 allow를 먼저 적용함을 뜻한다. IpFilter에서 적용 순서는 boolean 타입으로 관리하므로 allowDeny는 이를 위한 파싱 결과로 boolean 타입인 true를 리턴한다. 비슷하게 denyAllow는 파싱 결과로 false를 리턴한다.


orderChoice는 allowDeny나 denyAllow 중 하나를 이용해서 파싱하고 파싱에 성공한 파서의 결과 값을 그대로 리턴한다.


orderDecl의 Action 처리 코드를 보자. 이 코드의 vals는 List<Object>인데 첫 번째 값은 "order" 문자열(orderParser의 결과값)이고 두 번째 값은 Boolean 타입 값(orderChoice의 결과값)이다. 최종적으로 필요한 값은 적용 순서를 위한 Boolean 값이므로 두 번째 원소 값을 리턴한다.


이제 남은 문법은 다음 하나다.


config : allowOrDenyList orderDecl?


이를 위한 파서는 다음과 같다.


    // orderDecl?

    private OptionParser<List<Object>, Boolean> orderDeclOpt = new OptionParser<>(orderDecl);


    // config : allowOrDeny* orderDecl?;

    private SequenceParser<Object, Object, IpFilter> configParser =

            new SequenceParser<>(

                    Arrays.asList(allowOrDenyList, orderDeclOpt),

                    (vals) -> {

                        IpFilter filter = new IpFilter();


                        List<Tuple2<String, IpRange>> ipranges

                                 (List<Tuple2<String, IpRange>>) vals.get(0);

                        ipranges.forEach(tuple -> {

                            if (tuple.e1.equals("allow")) {

                                filter.addAllowIpRange(tuple.e2);

                            } else {

                                filter.addDenyIpRange(tuple.e2);

                            }

                        });


                        Optional<Boolean> allowFirstOpt = (Optional<Boolean>) vals.get(1);

                        filter.setAllowFirst(allowFirstOpt.orElse(true));

                        return filter;

                    });


configParser의 Action을 위한 람다식은 최종적으로 IpFilter를 생성한다. 이 람다식에 전달되는 List의 첫 번째 값은 allowOrDenyList의 결과 값이고 두 번째 값은 orderDeclOpt의 결과 값이다. 


allowOrDenyList의 결과는 List<Tuple2<String, IpRange>>이므로 이 값을 읽어와 IpFilter에 허용 IpRange와 차단 IpRange로 등록한다.


orderDeclOpt의 결과는 Optional<Boolean>이다. 그런데, orderDeclOpt는 옵션 파서이므로 값이 있을 수도 있고 없을 수도 있다. 이런 이유로 위 코드에서는 Optional의 orElse() 메서드를 사용해서 값이 없는 경우 true를 사용하도록 했다.


필요한 파서는 다 만들었다. 남은 것은 configParser를 이용해서 입력을 파싱하고 결과를 생성하는 것이다. 이와 관련된 코드는 다음과 같다.


    private TokenBuffer tokenBuffer;


    private Exception occuredException;

    private IpFilter result;


    public IpFilterCombiParser(TokenBuffer tokenBuffer) {

        this.tokenBuffer = tokenBuffer;

    }


    public void parse() {

        try {

            ParseResult<IpFilter> parseResult = configParser.parse(tokenBuffer);

            if (parseResult.isSuccess()) {

                if (tokenBuffer.currentToken().getType() == TokenType.TT_EOF) {

                    this.result = parseResult.getValue();

                } else {

                    throw new MatchingTokenNotFoundException();

                }

            }

        } catch(Exception e) {

            occuredException = e;

        }

    }


    public Optional<IpFilter> getResult() {

        return Optional.ofNullable(result);

    }


    public boolean isSuccess() {

        return !isFailed();

    }


    public boolean isFailed() {

        return occuredException != null;

    }


}


다 만들었으니 사용할 차례이다.


IpFilterCombiParser parser = new IpFilterCombiParser(tokens);

parser.parse();

if (parser.isSuccess()) {

    Optional<IpFilter> filterOpt = parser.getResult();

    IpFilter filter = filterOpt.get();

}



+ Recent posts