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 044import static org.apache.camel.util.BufferCaster.cast; 045 046public final class Scanner implements Iterator<String>, Closeable { 047 048 static { 049 WHITESPACE_PATTERN = Pattern.compile("\\s+"); 050 FIND_ANY_PATTERN = Pattern.compile("(?s).*"); 051 } 052 053 private static final Map<String, Pattern> CACHE = new LinkedHashMap<String, Pattern>() { 054 @Override 055 protected boolean removeEldestEntry(Entry<String, Pattern> eldest) { 056 return size() >= 7; 057 } 058 }; 059 060 private static final Pattern WHITESPACE_PATTERN; 061 062 private static final Pattern FIND_ANY_PATTERN; 063 064 private static final int BUFFER_SIZE = 1024; 065 066 private Readable source; 067 private Pattern delimPattern; 068 private Matcher matcher; 069 private CharBuffer buf; 070 private int position; 071 private boolean inputExhausted; 072 private boolean needInput; 073 private boolean skipped; 074 private int savedPosition = -1; 075 private boolean closed; 076 private IOException lastIOException; 077 078 public Scanner(InputStream source, String charsetName, String pattern) { 079 this(new InputStreamReader(Objects.requireNonNull(source, "source"), toDecoder(charsetName)), cachePattern(pattern)); 080 } 081 082 public Scanner(File source, String charsetName, String pattern) throws FileNotFoundException { 083 this(new FileInputStream(Objects.requireNonNull(source, "source")).getChannel(), charsetName, pattern); 084 } 085 086 public Scanner(String source, String pattern) { 087 this(new StringReader(Objects.requireNonNull(source, "source")), cachePattern(pattern)); 088 } 089 090 public Scanner(String source, Pattern pattern) { 091 this(new StringReader(Objects.requireNonNull(source, "source")), pattern); 092 } 093 094 public Scanner(ReadableByteChannel source, String charsetName, String pattern) { 095 this(Channels.newReader(Objects.requireNonNull(source, "source"), toDecoder(charsetName), -1), cachePattern(pattern)); 096 } 097 098 public Scanner(Readable source, String pattern) { 099 this(Objects.requireNonNull(source, "source"), cachePattern(pattern)); 100 } 101 102 private Scanner(Readable source, Pattern pattern) { 103 this.source = source; 104 delimPattern = pattern != null ? pattern : WHITESPACE_PATTERN; 105 buf = CharBuffer.allocate(BUFFER_SIZE); 106 cast(buf).limit(0); 107 matcher = delimPattern.matcher(buf); 108 matcher.useTransparentBounds(true); 109 matcher.useAnchoringBounds(false); 110 } 111 112 private static CharsetDecoder toDecoder(String charsetName) { 113 try { 114 Charset cs = charsetName != null ? Charset.forName(charsetName) : Charset.defaultCharset(); 115 return cs.newDecoder(); 116 } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { 117 throw new IllegalArgumentException(e); 118 } 119 } 120 121 @Override 122 public boolean hasNext() { 123 checkClosed(); 124 saveState(); 125 while (!inputExhausted) { 126 if (hasTokenInBuffer()) { 127 revertState(); 128 return true; 129 } 130 readMore(); 131 } 132 boolean result = hasTokenInBuffer(); 133 revertState(); 134 return result; 135 } 136 137 @Override 138 public String next() { 139 checkClosed(); 140 while (true) { 141 String token = getCompleteTokenInBuffer(); 142 if (token != null) { 143 skipped = false; 144 return token; 145 } 146 if (needInput) { 147 readMore(); 148 } else { 149 throwFor(); 150 } 151 } 152 } 153 154 private void saveState() { 155 savedPosition = position; 156 } 157 158 private void revertState() { 159 position = savedPosition; 160 savedPosition = -1; 161 skipped = false; 162 } 163 164 private void readMore() { 165 if (buf.limit() == buf.capacity()) { 166 expandBuffer(); 167 } 168 int p = buf.position(); 169 cast(buf).position(buf.limit()); 170 cast(buf).limit(buf.capacity()); 171 int n; 172 try { 173 n = source.read(buf); 174 } catch (IOException ioe) { 175 lastIOException = ioe; 176 n = -1; 177 } 178 if (n == -1) { 179 inputExhausted = true; 180 needInput = false; 181 } else if (n > 0) { 182 needInput = false; 183 } 184 cast(buf).limit(buf.position()); 185 cast(buf).position(p); 186 } 187 188 private void expandBuffer() { 189 int offset = savedPosition == -1 ? position : savedPosition; 190 cast(buf).position(offset); 191 if (offset > 0) { 192 buf.compact(); 193 translateSavedIndexes(offset); 194 position -= offset; 195 cast(buf).flip(); 196 } else { 197 int newSize = buf.capacity() * 2; 198 CharBuffer newBuf = CharBuffer.allocate(newSize); 199 newBuf.put(buf); 200 cast(newBuf).flip(); 201 translateSavedIndexes(offset); 202 position -= offset; 203 buf = newBuf; 204 matcher.reset(buf); 205 } 206 } 207 208 private void translateSavedIndexes(int offset) { 209 if (savedPosition != -1) { 210 savedPosition -= offset; 211 } 212 } 213 214 private void throwFor() { 215 skipped = false; 216 if (inputExhausted && position == buf.limit()) { 217 throw new NoSuchElementException(); 218 } else { 219 throw new InputMismatchException(); 220 } 221 } 222 223 private boolean hasTokenInBuffer() { 224 matcher.usePattern(delimPattern); 225 matcher.region(position, buf.limit()); 226 if (matcher.lookingAt()) { 227 position = matcher.end(); 228 } 229 return position != buf.limit(); 230 } 231 232 private String getCompleteTokenInBuffer() { 233 matcher.usePattern(delimPattern); 234 if (!skipped) { 235 matcher.region(position, buf.limit()); 236 if (matcher.lookingAt()) { 237 if (matcher.hitEnd() && !inputExhausted) { 238 needInput = true; 239 return null; 240 } 241 skipped = true; 242 position = matcher.end(); 243 } 244 } 245 if (position == buf.limit()) { 246 if (inputExhausted) { 247 return null; 248 } 249 needInput = true; 250 return null; 251 } 252 matcher.region(position, buf.limit()); 253 boolean foundNextDelim = matcher.find(); 254 if (foundNextDelim && (matcher.end() == position)) { 255 foundNextDelim = matcher.find(); 256 } 257 if (foundNextDelim) { 258 if (matcher.requireEnd() && !inputExhausted) { 259 needInput = true; 260 return null; 261 } 262 int tokenEnd = matcher.start(); 263 matcher.usePattern(FIND_ANY_PATTERN); 264 matcher.region(position, tokenEnd); 265 if (matcher.matches()) { 266 String s = matcher.group(); 267 position = matcher.end(); 268 return s; 269 } else { 270 return null; 271 } 272 } 273 if (inputExhausted) { 274 matcher.usePattern(FIND_ANY_PATTERN); 275 matcher.region(position, buf.limit()); 276 if (matcher.matches()) { 277 String s = matcher.group(); 278 position = matcher.end(); 279 return s; 280 } 281 return null; 282 } 283 needInput = true; 284 return null; 285 } 286 287 private void checkClosed() { 288 if (closed) { 289 throw new IllegalStateException(); 290 } 291 } 292 293 @Override 294 public void close() throws IOException { 295 if (!closed) { 296 closed = true; 297 if (source instanceof Closeable) { 298 try { 299 ((Closeable) source).close(); 300 } catch (IOException e) { 301 lastIOException = e; 302 } 303 } 304 } 305 if (lastIOException != null) { 306 throw lastIOException; 307 } 308 } 309 310 private static Pattern cachePattern(String pattern) { 311 if (pattern == null) { 312 return null; 313 } 314 synchronized (CACHE) { 315 return CACHE.computeIfAbsent(pattern, Pattern::compile); 316 } 317 } 318 319}