001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.parser;
014
015import static org.apache.juneau.internal.IOUtils.*;
016import static org.apache.juneau.internal.StringUtils.*;
017
018import java.io.*;
019import java.nio.charset.*;
020
021import org.apache.juneau.*;
022import org.apache.juneau.internal.*;
023
024/**
025 * A wrapper around an object that a parser reads its input from.
026 * 
027 * <p>
028 * For character-based parsers, the input object can be any of the following:
029 * <ul>
030 *    <li>{@link Reader}
031 *    <li>{@link CharSequence}
032 *    <li>{@link InputStream}
033 *    <li><code><jk>byte</jk>[]</code>
034 *    <li>{@link File}
035 *    <li><code><jk>null</jk></code>
036 * </ul>
037 * 
038 * <p>
039 * For stream-based parsers, the input object can be any of the following:
040 * <ul>
041 *    <li>{@link InputStream}
042 *    <li><code><jk>byte</jk>[]</code>
043 *    <li>{@link File}
044 *    <li>{@link String} - Hex-encoded bytes.  (not BASE-64!)
045 *    <li><code><jk>null</jk></code>
046 * </ul>
047 * 
048 * <p>
049 * Note that Readers and InputStreams will NOT be automatically closed when {@link #close()} is called, but
050 * streams and readers created from other types (e.g. Files) WILL be automatically closed.
051 */
052public final class ParserPipe implements Closeable {
053
054   private final Object input;
055   final boolean debug, strict, autoCloseStreams, unbuffered;
056   private final String fileCharset, inputStreamCharset;
057
058   private String inputString;
059   private InputStream inputStream;
060   private Reader reader;
061   private ParserReader parserReader;
062   private boolean doClose;
063
064   /**
065    * Constructor.
066    * 
067    * @param input The parser input object.
068    * @param debug
069    *    If <jk>true</jk>, the input contents will be copied locally and accessible via the {@link #getInputAsString()}
070    *    method.
071    *    This allows the contents of the pipe to be accessed when a problem occurs.
072    * @param strict
073    *    If <jk>true</jk>, sets {@link CodingErrorAction#REPORT} on {@link CharsetDecoder#onMalformedInput(CodingErrorAction)}
074    *    and {@link CharsetDecoder#onUnmappableCharacter(CodingErrorAction)}.
075    *    Otherwise, sets them to {@link CodingErrorAction#REPLACE}.
076    * @param autoCloseStreams 
077    *    Automatically close {@link InputStream InputStreams} and {@link Reader Readers} when passed in as input.
078    * @param unbuffered 
079    *    If <jk>true</jk>, we read one character at a time from underlying readers when the readers are expected to be parsed
080    *    multiple times.
081    *    <br>Otherwise, we read character data into a reusable buffer.
082    * @param fileCharset
083    *    The charset to expect when reading from {@link File Files}.
084    *    Use <js>"default"</js> to specify {@link Charset#defaultCharset()}.
085    * @param inputStreamCharset
086    *    The charset to expect when reading from {@link InputStream InputStreams}.
087    *    Use <js>"default"</js> to specify {@link Charset#defaultCharset()}.
088    */
089   public ParserPipe(Object input, boolean debug, boolean strict, boolean autoCloseStreams, boolean unbuffered, String fileCharset, String inputStreamCharset) {
090      this.input = input;
091      this.debug = debug;
092      this.strict = strict;
093      this.autoCloseStreams = autoCloseStreams;
094      this.unbuffered = unbuffered;
095      this.fileCharset = fileCharset;
096      this.inputStreamCharset = inputStreamCharset;
097      if (input instanceof CharSequence)
098         this.inputString = input.toString();
099   }
100
101   /**
102    * Shortcut constructor, typically for straight string input.
103    * 
104    * <p>
105    * Equivalent to calling <code><jk>new</jk> ParserPipe(input, <jk>false</jk>, <jk>false</jk>, <jk>null</jk>, <jk>null</jk>);</code>
106    * 
107    * @param input The input object.
108    */
109   public ParserPipe(Object input) {
110      this(input, false, false, false, false, null, null);
111   }
112
113   /**
114    * Wraps the specified input object inside an input stream.
115    * 
116    * <p>
117    * Subclasses can override this method to implement their own input streams.
118    * 
119    * @return The input object wrapped in an input stream, or <jk>null</jk> if the object is null.
120    * @throws IOException If object could not be converted to an input stream.
121    */
122   public InputStream getInputStream() throws IOException {
123      if (input == null)
124         return null;
125
126      if (input instanceof InputStream) {
127         if (debug) {
128            byte[] b = readBytes((InputStream)input, 1024);
129            inputString = toHex(b);
130            inputStream = new ByteArrayInputStream(b);
131         } else {
132            inputStream = (InputStream)input;
133            doClose = autoCloseStreams;
134         }
135      } else if (input instanceof byte[]) {
136         if (debug)
137            inputString = toHex((byte[])input);
138         inputStream = new ByteArrayInputStream((byte[])input);
139         doClose = false;
140      } else if (input instanceof String) {
141         inputString = (String)input;
142         inputStream = new ByteArrayInputStream(fromHex((String)input));
143         doClose = false;
144      } else if (input instanceof File) {
145         if (debug) {
146            byte[] b = readBytes((File)input);
147            inputString = toHex(b);
148            inputStream = new ByteArrayInputStream(b);
149         } else {
150            inputStream = new FileInputStream((File)input);
151            doClose = true;
152         }
153      } else {
154         throw new IOException("Cannot convert object of type "+input.getClass().getName()+" to an InputStream.");
155      }
156
157      return inputStream;
158   }
159
160
161   /**
162    * Wraps the specified input object inside a reader.
163    * 
164    * <p>
165    * Subclasses can override this method to implement their own readers.
166    * 
167    * @return The input object wrapped in a Reader, or <jk>null</jk> if the object is null.
168    * @throws IOException If object could not be converted to a reader.
169    */
170   public Reader getReader() throws IOException {
171      if (input == null)
172         return null;
173
174      if (input instanceof Reader) {
175         if (debug) {
176            inputString = read((Reader)input);
177            reader = new StringReader(inputString);
178         } else {
179            reader = (Reader)input;
180            doClose = autoCloseStreams;
181         }
182      } else if (input instanceof CharSequence) {
183         inputString = input.toString();
184         reader = new ParserReader(this);
185         doClose = false;
186      } else if (input instanceof InputStream || input instanceof byte[]) {
187         doClose = input instanceof InputStream && autoCloseStreams;
188         InputStream is = (
189            input instanceof InputStream
190            ? (InputStream)input
191            : new ByteArrayInputStream((byte[])input)
192         );
193         CharsetDecoder cd = (
194            "default".equalsIgnoreCase(inputStreamCharset)
195            ? Charset.defaultCharset()
196            : Charset.forName(inputStreamCharset)
197         ).newDecoder();
198         if (strict) {
199            cd.onMalformedInput(CodingErrorAction.REPORT);
200            cd.onUnmappableCharacter(CodingErrorAction.REPORT);
201         } else {
202            cd.onMalformedInput(CodingErrorAction.REPLACE);
203            cd.onUnmappableCharacter(CodingErrorAction.REPLACE);
204         }
205         reader = new InputStreamReader(is, cd);
206         if (debug) {
207            inputString = read(reader);
208            reader = new StringReader(inputString);
209         }
210      } else if (input instanceof File) {
211         CharsetDecoder cd = (
212            "DEFAULT".equalsIgnoreCase(fileCharset)
213            ? Charset.defaultCharset()
214            : Charset.forName(fileCharset)
215         ).newDecoder();
216         if (strict) {
217            cd.onMalformedInput(CodingErrorAction.REPORT);
218            cd.onUnmappableCharacter(CodingErrorAction.REPORT);
219         } else {
220            cd.onMalformedInput(CodingErrorAction.REPLACE);
221            cd.onUnmappableCharacter(CodingErrorAction.REPLACE);
222         }
223         reader = new InputStreamReader(new FileInputStream((File)input), cd);
224         if (debug) {
225            inputString = read(reader);
226            reader = new StringReader(inputString);
227         }
228         doClose = true;
229      } else {
230         throw new IOException("Cannot convert object of type "+input.getClass().getName()+" to a Reader.");
231      }
232
233      return reader;
234   }
235
236   /**
237    * Returns the contents of this pipe as a buffered reader.
238    * 
239    * <p>
240    * If the reader passed into this pipe is already a buffered reader, that reader will be returned.
241    * 
242    * @return The contents of this pipe as a buffered reader.
243    * @throws Exception
244    */
245   public Reader getBufferedReader() throws Exception {
246      return IOUtils.getBufferedReader(getReader());
247   }
248
249   /**
250    * Returns the input to this parser as a plain string.
251    * 
252    * <p>
253    * This method only returns a value if {@link BeanContext#BEAN_debug} is enabled.
254    * 
255    * @return The input as a string, or <jk>null</jk> if debug mode not enabled.
256    */
257   public String getInputAsString() {
258      return inputString;
259   }
260
261   /**
262    * Converts this pipe into a {@link ParserReader}.
263    * 
264    * @return The converted pipe.
265    * @throws Exception
266    */
267   public ParserReader getParserReader() throws Exception {
268      if (input == null)
269         return null;
270      if (input instanceof ParserReader)
271         parserReader = (ParserReader)input;
272      else
273         parserReader = new ParserReader(this);
274      return parserReader;
275   }
276
277   /**
278    * Returns <jk>true</jk> if the contents passed into this pipe was a {@link CharSequence}.
279    * 
280    * @return <jk>true</jk> if the contents passed into this pipe was a {@link CharSequence}.
281    */
282   public boolean isString() {
283      return inputString != null;
284   }
285
286   @Override /* Closeable */
287   public void close() {
288      try {
289         if (doClose)
290            IOUtils.close(reader, inputStream);
291      } catch (IOException e) {
292         throw new BeanRuntimeException(e);
293      }
294   }
295}