001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.camel.util; 018 019import java.io.Closeable; 020import java.io.File; 021import java.io.FileInputStream; 022import java.io.FileNotFoundException; 023import java.io.IOException; 024import java.io.InputStream; 025import java.io.InputStreamReader; 026import java.io.StringReader; 027import java.nio.CharBuffer; 028import java.nio.channels.Channels; 029import java.nio.channels.ReadableByteChannel; 030import java.nio.charset.Charset; 031import java.nio.charset.CharsetDecoder; 032import java.nio.charset.IllegalCharsetNameException; 033import java.nio.charset.UnsupportedCharsetException; 034import java.util.InputMismatchException; 035import java.util.Iterator; 036import java.util.LinkedHashMap; 037import java.util.Map; 038import java.util.Map.Entry; 039import java.util.NoSuchElementException; 040import java.util.Objects; 041import java.util.regex.Matcher; 042import java.util.regex.Pattern; 043 044public final class Scanner implements Iterator<String>, Closeable { 045 046 private static final Map<String, Pattern> CACHE = new LinkedHashMap<String, Pattern>() { 047 @Override 048 protected boolean removeEldestEntry(Entry<String, Pattern> eldest) { 049 return size() >= 7; 050 } 051 }; 052 053 private static final String WHITESPACE_PATTERN = "\\s+"; 054 055 private static final String FIND_ANY_PATTERN = "(?s).*"; 056 057 private static final int BUFFER_SIZE = 1024; 058 059 private Readable source; 060 private Pattern delimPattern; 061 private Matcher matcher; 062 private CharBuffer buf; 063 private int position; 064 private boolean inputExhausted; 065 private boolean needInput; 066 private boolean skipped; 067 private int savedPosition = -1; 068 private boolean closed; 069 private IOException lastIOException; 070 071 public Scanner(InputStream source, String charsetName, String pattern) { 072 this(new InputStreamReader(Objects.requireNonNull(source, "source"), toDecoder(charsetName)), cachePattern(pattern)); 073 } 074 075 public Scanner(File source, String charsetName, String pattern) throws FileNotFoundException { 076 this(new FileInputStream(Objects.requireNonNull(source, "source")).getChannel(), charsetName, pattern); 077 } 078 079 public Scanner(String source, String pattern) { 080 this(new StringReader(Objects.requireNonNull(source, "source")), cachePattern(pattern)); 081 } 082 083 public Scanner(ReadableByteChannel source, String charsetName, String pattern) { 084 this(Channels.newReader(Objects.requireNonNull(source, "source"), toDecoder(charsetName), -1), cachePattern(pattern)); 085 } 086 087 public Scanner(Readable source, String pattern) { 088 this(Objects.requireNonNull(source, "source"), cachePattern(pattern)); 089 } 090 091 private Scanner(Readable source, Pattern pattern) { 092 this.source = source; 093 delimPattern = pattern != null ? pattern : cachePattern(WHITESPACE_PATTERN); 094 buf = CharBuffer.allocate(BUFFER_SIZE); 095 buf.limit(0); 096 matcher = delimPattern.matcher(buf); 097 matcher.useTransparentBounds(true); 098 matcher.useAnchoringBounds(false); 099 } 100 101 private static CharsetDecoder toDecoder(String charsetName) { 102 try { 103 Charset cs = charsetName != null ? Charset.forName(charsetName) : Charset.defaultCharset(); 104 return cs.newDecoder(); 105 } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { 106 throw new IllegalArgumentException(e); 107 } 108 } 109 110 public boolean hasNext() { 111 checkClosed(); 112 saveState(); 113 while (!inputExhausted) { 114 if (hasTokenInBuffer()) { 115 revertState(); 116 return true; 117 } 118 readMore(); 119 } 120 boolean result = hasTokenInBuffer(); 121 revertState(); 122 return result; 123 } 124 125 public String next() { 126 checkClosed(); 127 while (true) { 128 String token = getCompleteTokenInBuffer(); 129 if (token != null) { 130 skipped = false; 131 return token; 132 } 133 if (needInput) { 134 readMore(); 135 } else { 136 throwFor(); 137 } 138 } 139 } 140 141 private void saveState() { 142 savedPosition = position; 143 } 144 145 private void revertState() { 146 position = savedPosition; 147 savedPosition = -1; 148 skipped = false; 149 } 150 151 private void readMore() { 152 if (buf.limit() == buf.capacity()) { 153 expandBuffer(); 154 } 155 int p = buf.position(); 156 buf.position(buf.limit()); 157 buf.limit(buf.capacity()); 158 int n; 159 try { 160 n = source.read(buf); 161 } catch (IOException ioe) { 162 lastIOException = ioe; 163 n = -1; 164 } 165 if (n == -1) { 166 inputExhausted = true; 167 needInput = false; 168 } else if (n > 0) { 169 needInput = false; 170 } 171 buf.limit(buf.position()); 172 buf.position(p); 173 } 174 175 private void expandBuffer() { 176 int offset = savedPosition == -1 ? position : savedPosition; 177 buf.position(offset); 178 if (offset > 0) { 179 buf.compact(); 180 translateSavedIndexes(offset); 181 position -= offset; 182 buf.flip(); 183 } else { 184 int newSize = buf.capacity() * 2; 185 CharBuffer newBuf = CharBuffer.allocate(newSize); 186 newBuf.put(buf); 187 newBuf.flip(); 188 translateSavedIndexes(offset); 189 position -= offset; 190 buf = newBuf; 191 matcher.reset(buf); 192 } 193 } 194 195 private void translateSavedIndexes(int offset) { 196 if (savedPosition != -1) { 197 savedPosition -= offset; 198 } 199 } 200 201 private void throwFor() { 202 skipped = false; 203 if (inputExhausted && position == buf.limit()) { 204 throw new NoSuchElementException(); 205 } else { 206 throw new InputMismatchException(); 207 } 208 } 209 210 private boolean hasTokenInBuffer() { 211 matcher.usePattern(delimPattern); 212 matcher.region(position, buf.limit()); 213 if (matcher.lookingAt()) { 214 position = matcher.end(); 215 } 216 return position != buf.limit(); 217 } 218 219 private String getCompleteTokenInBuffer() { 220 matcher.usePattern(delimPattern); 221 if (!skipped) { 222 matcher.region(position, buf.limit()); 223 if (matcher.lookingAt()) { 224 if (matcher.hitEnd() && !inputExhausted) { 225 needInput = true; 226 return null; 227 } 228 skipped = true; 229 position = matcher.end(); 230 } 231 } 232 if (position == buf.limit()) { 233 if (inputExhausted) { 234 return null; 235 } 236 needInput = true; 237 return null; 238 } 239 matcher.region(position, buf.limit()); 240 boolean foundNextDelim = matcher.find(); 241 if (foundNextDelim && (matcher.end() == position)) { 242 foundNextDelim = matcher.find(); 243 } 244 if (foundNextDelim) { 245 if (matcher.requireEnd() && !inputExhausted) { 246 needInput = true; 247 return null; 248 } 249 int tokenEnd = matcher.start(); 250 matcher.usePattern(cachePattern(FIND_ANY_PATTERN)); 251 matcher.region(position, tokenEnd); 252 if (matcher.matches()) { 253 String s = matcher.group(); 254 position = matcher.end(); 255 return s; 256 } else { 257 return null; 258 } 259 } 260 if (inputExhausted) { 261 matcher.usePattern(cachePattern(FIND_ANY_PATTERN)); 262 matcher.region(position, buf.limit()); 263 if (matcher.matches()) { 264 String s = matcher.group(); 265 position = matcher.end(); 266 return s; 267 } 268 return null; 269 } 270 needInput = true; 271 return null; 272 } 273 274 private void checkClosed() { 275 if (closed) { 276 throw new IllegalStateException(); 277 } 278 } 279 280 public void close() throws IOException { 281 if (!closed) { 282 closed = true; 283 if (source instanceof Closeable) { 284 try { 285 ((Closeable) source).close(); 286 } catch (IOException e) { 287 lastIOException = e; 288 } 289 } 290 } 291 if (lastIOException != null) { 292 throw lastIOException; 293 } 294 } 295 296 private static Pattern cachePattern(String pattern) { 297 if (pattern == null) { 298 return null; 299 } 300 return CACHE.computeIfAbsent(pattern, Pattern::compile); 301 } 302 303}