001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2010–2011 ApexIdentity Inc.
015 * Portions Copyright 2011-2014 ForgeRock AS.
016 */
017
018package org.forgerock.openig.regex;
019
020import java.io.BufferedReader;
021import java.io.Closeable;
022import java.io.IOException;
023import java.io.Reader;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.NoSuchElementException;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029
030/**
031 * Iterates through multiple regular expression matches within a character stream.
032 */
033public class StreamPatternMatches implements Closeable {
034
035    /** The patterns to match. */
036    private final Pattern[] patterns;
037
038    /** The character stream to search. */
039    private BufferedReader input;
040
041    /** Should patterns be discarded after they yield a match. */
042    private boolean discard;
043
044    /** The string pattern match iterator to search each line of input. */
045    private StringPatternMatches matches = null;
046
047    /**
048     * Constructs a new stream pattern match iterator. If {@code discard} is {@code true},
049     * then a pattern is discarded after it is first matched.
050     *
051     * @param input the character stream to match regular expression patterns against.
052     * @param patterns a collection of regular expression patterns to match.
053     * @param discard indicates patterns be discarded after they yield a match.
054     */
055    public StreamPatternMatches(Reader input, Collection<Pattern> patterns, boolean discard) {
056        this.input = (BufferedReader) (input instanceof BufferedReader ? input : new BufferedReader(input));
057        this.patterns = patterns.toArray(new Pattern[patterns.size()]);
058        this.discard = discard;
059    }
060
061    /**
062     * Returns the next match from the character stream. Matches are returned in the order
063     * they are encountered in the character stream, then by the order they are expressed in
064     * the supplied patterns collection.
065     *
066     * @return the next {@link java.util.regex.Matcher} from the character stream.
067     * @throws IOException if an I/O exception occurs.
068     * @throws NoSuchElementException if the reader has no more matches.
069     */
070    public Matcher next() throws IOException {
071        readahead();
072        if (matches == null) {
073            throw new NoSuchElementException();
074        }
075        Matcher matcher = matches.next();
076        if (discard) {
077            Pattern pattern = matcher.pattern();
078            for (int n = 0; n < patterns.length; n++) {
079                if (patterns[n] == pattern) {
080                    patterns[n] = null;
081                    break;
082                }
083            }
084        }
085        return matcher;
086    }
087
088    /**
089     * Returns {@code true} if the character stream has more matches.
090     *
091     * @return {@code true} if the character stream has more matches.
092     * @throws IOException if an I/O exception occurs.
093     */
094    public boolean hasNext() throws IOException {
095        readahead();
096        return (matches != null && matches.hasNext());
097    }
098
099    /**
100     * Closes this character stream, as well as the the reader it its iterating over.
101     *
102     * @throws IOException if an I/O exception occurs.
103     */
104    public void close() throws IOException {
105        if (input != null) {
106            input.close();
107            input = null;
108        }
109    }
110
111    /**
112     * Throws an {@link IOException} if the stream is closed.
113     */
114    private void notClosed() throws IOException {
115        if (input == null) {
116            throw new IOException("stream is closed");
117        }
118    }
119
120    private void readahead() throws IOException {
121        notClosed();
122        while (matches == null || !matches.hasNext()) {
123            String line = input.readLine();
124            if (line == null) {
125                break;
126            }
127            matches = new StringPatternMatches(line, Arrays.asList(patterns), discard);
128        }
129    }
130}