001/*
002 * This file is part of the JDrupes non-blocking HTTP Codec
003 * Copyright (C) 2016, 2017  Michael N. Lipp
004 *
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Lesser General Public License as published
007 * by the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
013 * License for more details.
014 *
015 * You should have received a copy of the GNU Lesser General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.httpcodec.util;
020
021import java.text.ParseException;
022import java.util.Iterator;
023import java.util.NoSuchElementException;
024
025/**
026 * Splits a list of items. Delimiters are escaped if they are in
027 * double quotes.
028 */
029public class ListItemizer implements Iterator<String> {
030
031        private String unparsedValue;
032        private int position = 0;
033        private String delimiters;
034        private String pendingItem = null;
035        private ParseException pendingException = null;
036        
037        /**
038         * Generates a new itemizer.
039         * 
040         * @param list the list to be itemized
041         * @param delimiters the set of delimiter characters
042         */
043        public ListItemizer(String list, String delimiters) {
044                unparsedValue = list;
045                this.delimiters = delimiters;
046                while (true) { // Skip optional white space
047                        char ch = unparsedValue.charAt(position);
048                        if (ch != ' ' && ch != '\t') {
049                                break;
050                        }
051                        position += 1;
052                }
053                try {
054                        pendingItem = nextItem();
055                } catch (ParseException e) {
056                        pendingException = e;
057                }
058        }
059
060        /**
061         * Returns the next item from the unparsed value.
062         * 
063         * @return the next item or {@code null} if no items remain
064         * @throws ParseException if the input violates the field format
065         */
066        private String nextItem() throws ParseException {
067                // RFC 7230 3.2.6
068                boolean inDquote = false;
069                int startPosition = position;
070                try {
071                        while (true) {
072                                if (inDquote) {
073                                        char ch = unparsedValue.charAt(position);
074                                        switch (ch) {
075                                         case '\\':
076                                                 position += 2;
077                                                 continue;
078                                         case '\"':
079                                                 inDquote = false;
080                                                 // fall through
081                                         default:
082                                                 position += 1;
083                                                 continue;
084                                        }
085                                }
086                                if (position == unparsedValue.length()) {
087                                        if (position == startPosition) {
088                                                return null;
089                                        }
090                                        return unparsedValue.substring(startPosition, position);
091                                }
092                                char ch = unparsedValue.charAt(position);
093                                if (delimiters.indexOf(ch) >= 0) {
094                                        String result = unparsedValue
095                                                .substring(startPosition, position).trim();
096                                        position += 1; // Skip delimiter
097                                        while (true) { // Skip optional white space
098                                                ch = unparsedValue.charAt(position);
099                                                if (ch != ' ' && ch != '\t') {
100                                                        break;
101                                                }
102                                                position += 1;
103                                        }
104                                        return result;
105                                }
106                                switch (ch) {
107                                case '\"':
108                                        inDquote = true;
109                                        // fall through
110                                default:
111                                        position += 1;
112                                        continue;
113                                }
114                        }
115                } catch (IndexOutOfBoundsException e) {
116                        throw new ParseException(unparsedValue, position);
117                }
118        }
119
120        /* (non-Javadoc)
121         * @see java.util.Iterator#hasNext()
122         */
123        @Override
124        public boolean hasNext() {
125                return pendingItem != null;
126        }
127
128        /* (non-Javadoc)
129         * @see java.util.Iterator#next()
130         */
131        @Override
132        public String next() {
133                if (pendingException != null) {
134                        NoSuchElementException exc = new NoSuchElementException();
135                        exc.initCause(pendingException);
136                        throw exc;
137                }
138                if (pendingItem == null) {
139                        throw new NoSuchElementException("No elements left.");
140                }
141                String result = pendingItem;
142                pendingItem = null;
143                try {
144                        pendingItem = nextItem();
145                } catch (ParseException e) {
146                        pendingException = e;
147                        NoSuchElementException exc = new NoSuchElementException();
148                        exc.initCause(pendingException);
149                        throw exc;
150                }
151                return result;
152        }
153        
154}